diff --git a/README.md b/README.md index 4d4d14b1..b17c3ccf 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,7 @@ outside the namespace. | assignable to | `.WhichAreAssignableTo()` | `.IsAssignableTo()` | `.AreAssignableTo()` | | assignable from | `.WhichAreAssignableFrom()` | `.IsAssignableFrom()` | `.AreAssignableFrom()` | | instantiable | `.WhichAreInstantiable()` | `.IsInstantiable()` | `.AreInstantiable()` | +| immutable | `.WhichAreImmutable()` | `.IsImmutable()` | `.AreImmutable()` | | default constructor | `.WhichHaveADefaultConstructor()` | `.HasADefaultConstructor()` | `.HaveADefaultConstructor()` | | custom predicate | `.Which(t => …)` | `.Satisfies(t => …)` | `.All().Satisfy(t => …)` | @@ -323,6 +324,10 @@ an open generic type definition. *Default constructor* checks for an accessible (value types always have one); this is independent of instantiability (e.g. a type with only a parameterized constructor is instantiable but has no default constructor). +A type is *immutable* when all instance fields (including inherited ones) are `readonly` and all instance +properties (including inherited ones) have no setter or an `init`-only setter. Static members do not affect +immutability. Failure messages list the offending mutable members for actionable feedback. + > **Negation:** every kind/modifier row above has a negated form. Most use `WhichAreNot…` on filters and > `IsNot…` / `AreNot…` on assertions (e.g. `WhichAreNotSealed()`, `IsNotAClass()`, `AreNotStatic()`, > `IsNotInstantiable()`). The *default constructor* row uses `WhichDoNotHaveADefaultConstructor()`, diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichAreImmutable.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichAreImmutable.cs new file mode 100644 index 00000000..a1022f92 --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichAreImmutable.cs @@ -0,0 +1,34 @@ +using System; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class TypeFilters +{ + /// + /// Filters for types that are immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static Filtered.Types WhichAreImmutable(this Filtered.Types @this) + => @this.Which(Filter.Prefix( + type => type.IsImmutable(), + "immutable ")); + + /// + /// Filters for types that are not immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static Filtered.Types WhichAreNotImmutable(this Filtered.Types @this) + => @this.Which(Filter.Prefix( + type => !type.IsImmutable(), + "mutable ")); +} diff --git a/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs b/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs index dd74f308..3f77f9db 100644 --- a/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs +++ b/Source/aweXpect.Reflection/Helpers/TypeHelpers.cs @@ -1311,6 +1311,32 @@ public static bool IsReallyAbstract(this Type? type) public static bool IsReallyInstantiable(this Type? type) => type is { IsAbstract: false, IsGenericTypeDefinition: false, }; + /// + /// Gets a value indicating whether the is immutable. + /// + /// The . + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static bool IsImmutable(this Type? type) + => type is not null && type.GetMutableMembers().Length == 0; + + /// + /// Gets the mutable instance members of the : fields (including inherited ones) + /// that are not and properties (including inherited ones) with a regular + /// (non-init) setter. + /// + /// The . + public static MemberInfo[] GetMutableMembers(this Type type) + => type.GetDeclaredFields(MemberScope.IncludingInherited) + .Where(field => field is { IsStatic: false, IsInitOnly: false, }) + .OfType() + .Concat(type.GetDeclaredProperties(MemberScope.IncludingInherited) + .Where(property => !property.IsReallyStatic() && property.HasSetter())) + .ToArray(); + /// /// Gets a value indicating whether the has an accessible parameterless (default) constructor. /// diff --git a/Source/aweXpect.Reflection/ThatType.IsImmutable.cs b/Source/aweXpect.Reflection/ThatType.IsImmutable.cs new file mode 100644 index 00000000..feb630df --- /dev/null +++ b/Source/aweXpect.Reflection/ThatType.IsImmutable.cs @@ -0,0 +1,73 @@ +using System; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; + +namespace aweXpect.Reflection; + +public static partial class ThatType +{ + /// + /// Verifies that the is immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult> IsImmutable( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsImmutableConstraint(it, grammars)), + subject); + + /// + /// Verifies that the is not immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult> IsNotImmutable( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsImmutableConstraint(it, grammars).Invert()), + subject); + + private sealed class IsImmutableConstraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + public ConstraintResult IsMetBy(Type? actual) + { + Actual = actual; + Outcome = actual.IsImmutable() ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("is immutable"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + // The mutable members are only needed for this failure message, so they are collected lazily here + // instead of on every (typically succeeding) evaluation. + stringBuilder.Append(It).Append(" was mutable "); + Formatter.Format(stringBuilder, Actual); + stringBuilder.Append(" with mutable members "); + Formatter.Format(stringBuilder, Actual!.GetMutableMembers(), FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("is not immutable"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" was immutable "); + Formatter.Format(stringBuilder, Actual); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatTypes.AreImmutable.cs b/Source/aweXpect.Reflection/ThatTypes.AreImmutable.cs new file mode 100644 index 00000000..27adfd22 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatTypes.AreImmutable.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; +#if NET8_0_OR_GREATER +using System.Threading; +using System.Threading.Tasks; +#endif + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Reflection; + +public static partial class ThatTypes +{ + /// + /// Verifies that all items in the filtered collection of are immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult, IThat>> AreImmutable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreImmutableConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult, IThat>> AreImmutable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreImmutableConstraint(it, grammars)), + subject); +#endif + + /// + /// Verifies that all items in the filtered collection of are not immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult, IThat>> AreNotImmutable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotImmutableConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are not immutable. + /// + /// + /// A type is considered immutable when all instance fields (including inherited ones) are + /// and all instance properties (including inherited ones) have no setter + /// or an init-only setter. + /// + public static AndOrResult, IThat>> AreNotImmutable( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotImmutableConstraint(it, grammars)), + subject); +#endif + + private sealed class AreImmutableConstraint(string it, ExpectationGrammars grammars) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, type => type.IsImmutable()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, type => type.IsImmutable()); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are all immutable"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained mutable types "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are not all immutable"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained immutable types "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } + + private sealed class AreNotImmutableConstraint(string it, ExpectationGrammars grammars) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, type => !type.IsImmutable()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, type => !type.IsImmutable()); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are all not immutable"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained immutable types "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("also contain an immutable type"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained mutable types "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt index 99151c76..275c965c 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt @@ -1674,6 +1674,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult IsGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAClass(this aweXpect.Core.IThat subject) { } @@ -1692,6 +1693,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } @@ -1727,6 +1729,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult> AreGeneric(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult> AreGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreImmutable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInterfaces(this aweXpect.Core.IThat> subject) { } @@ -1755,6 +1759,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotGeneric(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotImmutable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInterfaces(this aweXpect.Core.IThat> subject) { } @@ -1900,6 +1906,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.TypeFilters.GenericTypes WhichAreGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } @@ -1916,6 +1923,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt index 28c2442d..45f98103 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt @@ -1674,6 +1674,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult IsGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAClass(this aweXpect.Core.IThat subject) { } @@ -1692,6 +1693,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } @@ -1727,6 +1729,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult> AreGeneric(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult> AreGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreImmutable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInterfaces(this aweXpect.Core.IThat> subject) { } @@ -1755,6 +1759,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotGeneric(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotImmutable(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInterfaces(this aweXpect.Core.IThat> subject) { } @@ -1900,6 +1906,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.TypeFilters.GenericTypes WhichAreGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } @@ -1916,6 +1923,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt index 0126d718..a3844ac9 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt @@ -1398,6 +1398,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult IsGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotAClass(this aweXpect.Core.IThat subject) { } @@ -1416,6 +1417,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject, System.Type type) { } public static aweXpect.Results.AndOrResult> IsNotAssignableTo(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotGeneric(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotImmutable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotInstantiable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNested(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } @@ -1440,6 +1442,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreEnums(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.Results.GenericArgumentCollectionResult> AreGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreInterfaces(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNested(this aweXpect.Core.IThat> subject) { } @@ -1454,6 +1457,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotEnums(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotExceptions(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotGeneric(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotImmutable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInstantiable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotInterfaces(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNested(this aweXpect.Core.IThat> subject) { } @@ -1536,6 +1540,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.TypeFilters.GenericTypes WhichAreGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } @@ -1552,6 +1557,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotEnums(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotExceptions(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotGeneric(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotImmutable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInstantiable(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInterfaces(this aweXpect.Reflection.Collections.Filtered.Types @this) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichAreNotInternal(this aweXpect.Reflection.Collections.Filtered.Types @this) { } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreImmutable.Tests.cs new file mode 100644 index 00000000..4a41c212 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreImmutable.Tests.cs @@ -0,0 +1,24 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class TypeFilters +{ + public sealed class WhichAreImmutable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForImmutableTypes() + { + Filtered.Types types = In.AssemblyContaining() + .Types().WhichAreImmutable(); + + await That(types).AreImmutable().And.IsNotEmpty(); + await That(types.GetDescription()) + .IsEqualTo("immutable types in assembly").AsPrefix(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreNotImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreNotImmutable.Tests.cs new file mode 100644 index 00000000..641d2778 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreNotImmutable.Tests.cs @@ -0,0 +1,24 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Types; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class TypeFilters +{ + public sealed class WhichAreNotImmutable + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForMutableTypes() + { + Filtered.Types types = In.AssemblyContaining() + .Types().WhichAreNotImmutable(); + + await That(types).AreNotImmutable().And.IsNotEmpty(); + await That(types.GetDescription()) + .IsEqualTo("mutable types in assembly").AsPrefix(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs new file mode 100644 index 00000000..ff2c3fdb --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs @@ -0,0 +1,115 @@ +namespace aweXpect.Reflection.Tests.TestHelpers.Types; + +#pragma warning disable CS0414 // Field is assigned but its value is never used +public class ImmutableClass +{ + public const int ConstantField = 4; + private static int _staticMutableField = 1; + public readonly int ReadOnlyField = 2; + private readonly string _privateReadOnlyField = ""; + public static int StaticSettableProperty { get; set; } + public int GetOnlyProperty { get; } + public string ComputedProperty => _privateReadOnlyField; +} +#pragma warning restore CS0414 + +public class ImmutableClassWithInitProperty +{ + public int Value { get; init; } +} + +public class ImmutableDerivedClass : ImmutableClass +{ + public readonly int DerivedReadOnlyField = 3; +} + +public class ClassWithMutableField +{ + public int Value = 1; +} + +public class ClassWithSettableProperty +{ + public int Value { get; set; } +} + +public class ClassWithMutableFieldAndSettableProperty +{ + public int Field = 1; + public int Property { get; set; } +} + +public class MutableBaseClass +{ + private int _value = 1; + + public int GetValue() => _value; + + public void SetValue(int value) => _value = value; +} + +public class ClassInheritingMutableField : MutableBaseClass; + +public class MutableBaseClassWithProtectedField +{ + protected int ProtectedValue = 1; +} + +public class ClassInheritingProtectedMutableField : MutableBaseClassWithProtectedField; + +public class ClassWithSettableIndexer +{ + private readonly int[] _values = new int[10]; + + public int this[int index] + { + get => _values[index]; + set => _values[index] = value; + } +} + +public class ClassWithPrivateSettableProperty +{ + public int Value { get; private set; } +} + +public struct MutableStruct +{ + public int Value; +} + +public readonly struct ImmutableReadOnlyStruct +{ + public readonly int Value; + + public ImmutableReadOnlyStruct(int value) + { + Value = value; + } +} + +public record struct MutableRecordStruct(int Value); + +public readonly record struct ImmutableRecordStruct(int Value); + +public record PositionalRecord(int Value); + +public interface IImmutableInterface +{ + int Value { get; } +} + +public interface IMutableInterface +{ + int Value { get; set; } +} + +public class GenericImmutableClass +{ + public readonly T Value = default!; +} + +public class GenericMutableClass +{ + public T Value = default!; +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs new file mode 100644 index 00000000..40f36b49 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs @@ -0,0 +1,254 @@ +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatType +{ + public sealed class IsImmutable + { + public sealed class Tests + { + [Theory] + [MemberData(nameof(ImmutableTypes))] + public async Task WhenTypeIsImmutable_ShouldSucceed(Type subject) + { + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeHasMutableField_ShouldFail() + { + Type subject = typeof(ClassWithMutableField); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithMutableField with mutable members [ + int ClassWithMutableField.Value + ] + """); + } + + [Fact] + public async Task WhenTypeHasSettableProperty_ShouldFail() + { + Type subject = typeof(ClassWithSettableProperty); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithSettableProperty with mutable members [ + public int ClassWithSettableProperty.Value { get; set; } + ] + """); + } + + [Fact] + public async Task WhenTypeHasMultipleMutableMembers_ShouldFail() + { + Type subject = typeof(ClassWithMutableFieldAndSettableProperty); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithMutableFieldAndSettableProperty with mutable members [ + int ClassWithMutableFieldAndSettableProperty.Field, + public int ClassWithMutableFieldAndSettableProperty.Property { get; set; } + ] + """); + } + + [Fact] + public async Task WhenTypeInheritsMutableField_ShouldFail() + { + Type subject = typeof(ClassInheritingMutableField); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassInheritingMutableField with mutable members [ + int MutableBaseClass._value + ] + """); + } + + [Fact] + public async Task WhenTypeHasSettableIndexer_ShouldFail() + { + Type subject = typeof(ClassWithSettableIndexer); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithSettableIndexer with mutable members [ + public int ClassWithSettableIndexer.Item { get; set; } + ] + """); + } + + [Fact] + public async Task WhenTypeHasPropertyWithPrivateSetter_ShouldFail() + { + Type subject = typeof(ClassWithPrivateSettableProperty); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithPrivateSettableProperty with mutable members [ + public int ClassWithPrivateSettableProperty.Value { get; private set; } + ] + """); + } + + [Fact] + public async Task WhenTypeInheritsProtectedMutableField_ShouldFail() + { + Type subject = typeof(ClassInheritingProtectedMutableField); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassInheritingProtectedMutableField with mutable members [ + int MutableBaseClassWithProtectedField.ProtectedValue + ] + """); + } + + [Fact] + public async Task WhenTypeIsPositionalRecordStruct_ShouldFail() + { + Type subject = typeof(MutableRecordStruct); + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable MutableRecordStruct with mutable members [ + public int MutableRecordStruct.Value { get; set; } + ] + """); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + { + await That(subject).IsImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is immutable, + but it was + """); + } + + public static TheoryData ImmutableTypes() => new() + { + typeof(ImmutableClass), + typeof(ImmutableClassWithInitProperty), + typeof(ImmutableDerivedClass), + typeof(PublicRecord), + typeof(PublicSealedClass), + typeof(PositionalRecord), + typeof(ImmutableReadOnlyStruct), + typeof(ImmutableRecordStruct), + typeof(IImmutableInterface), + typeof(PublicEnum), + typeof(PublicStaticClass), + typeof(GenericImmutableClass<>), + }; + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenTypeIsImmutable_ShouldFail() + { + Type subject = typeof(ImmutableClass); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsImmutable()); + } + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not immutable, + but it was immutable ImmutableClass + """); + } + + [Fact] + public async Task WhenTypeIsMutable_ShouldSucceed() + { + Type subject = typeof(ClassWithMutableField); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsImmutable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs new file mode 100644 index 00000000..8cbe94cc --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs @@ -0,0 +1,114 @@ +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatType +{ + public sealed class IsNotImmutable + { + public sealed class Tests + { + [Theory] + [MemberData(nameof(MutableTypes))] + public async Task WhenTypeIsMutable_ShouldSucceed(Type subject) + { + async Task Act() + { + await That(subject).IsNotImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeIsImmutable_ShouldFail() + { + Type subject = typeof(ImmutableClass); + + async Task Act() + { + await That(subject).IsNotImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not immutable, + but it was immutable ImmutableClass + """); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + { + await That(subject).IsNotImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not immutable, + but it was + """); + } + + public static TheoryData MutableTypes() => new() + { + typeof(ClassWithMutableField), + typeof(ClassWithSettableProperty), + typeof(ClassWithMutableFieldAndSettableProperty), + typeof(MutableBaseClass), + typeof(ClassInheritingMutableField), + typeof(MutableBaseClassWithProtectedField), + typeof(ClassInheritingProtectedMutableField), + typeof(ClassWithSettableIndexer), + typeof(ClassWithPrivateSettableProperty), + typeof(MutableStruct), + typeof(MutableRecordStruct), + typeof(IMutableInterface), + typeof(GenericMutableClass<>), + }; + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenTypeIsImmutable_ShouldSucceed() + { + Type subject = typeof(ImmutableClass); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotImmutable()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeIsMutable_ShouldFail() + { + Type subject = typeof(ClassWithMutableField); + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotImmutable()); + } + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is immutable, + but it was mutable ClassWithMutableField with mutable members [ + int ClassWithMutableField.Value + ] + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.AreImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.AreImmutable.Tests.cs new file mode 100644 index 00000000..5b0b8d2d --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.AreImmutable.Tests.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatTypes +{ + public sealed class AreImmutable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllTypesAreImmutable_ShouldSucceed() + { + IEnumerable subject = new[] + { + typeof(ImmutableClass), typeof(ImmutableClassWithInitProperty), + }; + + async Task Act() + { + await That(subject).AreImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypesAreMutable_ShouldFail() + { + IEnumerable subject = new[] + { + typeof(ImmutableClass), typeof(ClassWithMutableField), + }; + + async Task Act() + { + await That(subject).AreImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all immutable, + but it contained mutable types [ + ClassWithMutableField + ] + """); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task WhenAsyncEnumerableAllTypesAreImmutable_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(ImmutableClass), typeof(ImmutableClassWithInitProperty), + }.ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAsyncEnumerableSomeTypesAreMutable_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(ImmutableClass), typeof(ClassWithMutableField), + }.ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all immutable, + but it contained mutable types [ + ClassWithMutableField + ] + """); + } +#endif + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllTypesAreImmutable_ShouldFail() + { + IEnumerable subject = new[] + { + typeof(ImmutableClass), + }; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreImmutable()); + } + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + are not all immutable, + but it only contained immutable types [ + ImmutableClass + ] + """); + } + + [Fact] + public async Task WhenSomeTypesAreMutable_ShouldSucceed() + { + IEnumerable subject = new[] + { + typeof(ImmutableClass), typeof(ClassWithMutableField), + }; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreImmutable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.AreNotImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.AreNotImmutable.Tests.cs new file mode 100644 index 00000000..69e8375f --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.AreNotImmutable.Tests.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatTypes +{ + public sealed class AreNotImmutable + { + public sealed class Tests + { + [Fact] + public async Task WhenAllTypesAreMutable_ShouldSucceed() + { + IEnumerable subject = new[] + { + typeof(ClassWithMutableField), typeof(ClassWithSettableProperty), + }; + + async Task Act() + { + await That(subject).AreNotImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypesAreImmutable_ShouldFail() + { + IEnumerable subject = new[] + { + typeof(ClassWithMutableField), typeof(ImmutableClass), + }; + + async Task Act() + { + await That(subject).AreNotImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not immutable, + but it contained immutable types [ + ImmutableClass + ] + """); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task WhenAsyncEnumerableAllTypesAreMutable_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithMutableField), typeof(ClassWithSettableProperty), + }.ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotImmutable(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAsyncEnumerableSomeTypesAreImmutable_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(ClassWithMutableField), typeof(ImmutableClass), + }.ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotImmutable(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not immutable, + but it contained immutable types [ + ImmutableClass + ] + """); + } +#endif + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllTypesAreMutable_ShouldFail() + { + IEnumerable subject = new[] + { + typeof(ClassWithMutableField), + }; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotImmutable()); + } + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + also contain an immutable type, + but it only contained mutable types [ + ClassWithMutableField + ] + """); + } + + [Fact] + public async Task WhenSomeTypesAreImmutable_ShouldSucceed() + { + IEnumerable subject = new[] + { + typeof(ClassWithMutableField), typeof(ImmutableClass), + }; + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotImmutable()); + } + + await That(Act).DoesNotThrow(); + } + } + } +}