From f957265b84f5ac95203c07cb90ba1b94b0bdb0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 6 Jun 2026 05:04:16 +0200 Subject: [PATCH 1/2] feat: add immutability assertions and filters for types Implements #329: IsImmutable / IsNotImmutable for single types, AreImmutable / AreNotImmutable for type collections (including IAsyncEnumerable overloads) and WhichAreImmutable / WhichAreNotImmutable filters. A type is considered 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; init-only setters count as immutable, static members are ignored. Failure messages list the offending mutable members for actionable feedback. --- README.md | 5 + .../Filters/TypeFilters.WhichAreImmutable.cs | 34 ++++ .../Helpers/TypeHelpers.cs | 26 +++ .../ThatType.IsImmutable.cs | 73 ++++++++ .../ThatTypes.AreImmutable.cs | 148 +++++++++++++++ .../Expected/aweXpect.Reflection_net10.0.txt | 8 + .../Expected/aweXpect.Reflection_net8.0.txt | 8 + .../aweXpect.Reflection_netstandard2.0.txt | 6 + .../TypeFilters.WhichAreImmutable.Tests.cs | 24 +++ .../TypeFilters.WhichAreNotImmutable.Tests.cs | 24 +++ .../Types/ImmutabilityTestTypes.cs | 51 ++++++ .../ThatType.IsImmutable.Tests.cs | 168 ++++++++++++++++++ .../ThatType.IsNotImmutable.Tests.cs | 107 +++++++++++ .../ThatTypes.AreImmutable.Tests.cs | 139 +++++++++++++++ .../ThatTypes.AreNotImmutable.Tests.cs | 139 +++++++++++++++ 15 files changed, 960 insertions(+) create mode 100644 Source/aweXpect.Reflection/Filters/TypeFilters.WhichAreImmutable.cs create mode 100644 Source/aweXpect.Reflection/ThatType.IsImmutable.cs create mode 100644 Source/aweXpect.Reflection/ThatTypes.AreImmutable.cs create mode 100644 Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreImmutable.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichAreNotImmutable.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatTypes.AreImmutable.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatTypes.AreNotImmutable.Tests.cs 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..0bff73a4 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs @@ -0,0 +1,51 @@ +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; + public 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; 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..8d39ca45 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs @@ -0,0 +1,168 @@ +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 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() + => + [ + typeof(ImmutableClass), + typeof(ImmutableClassWithInitProperty), + typeof(ImmutableDerivedClass), + typeof(PublicRecord), + typeof(PublicSealedClass), + ]; + } + + 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..c2799f6f --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs @@ -0,0 +1,107 @@ +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() + => + [ + typeof(ClassWithMutableField), + typeof(ClassWithSettableProperty), + typeof(ClassWithMutableFieldAndSettableProperty), + typeof(MutableBaseClass), + typeof(ClassInheritingMutableField), + ]; + } + + 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(); + } + } + } +} From ba61c36e93f9328f5d59ab7780d993326696e9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 6 Jun 2026 08:59:09 +0200 Subject: [PATCH 2/2] test: extend immutability test matrix and fix Sonar findings Pin behavior for structs (mutable and readonly), record structs (positional with set vs readonly with init), positional records, enums, interfaces (settable vs get-only properties), static classes, indexers with setters, properties with private setters, inherited non-private mutable fields and open generic type definitions. Fix Sonar findings: make the static fixture field private (S2223) and replace the TheoryData collection expressions with object initializers to avoid zero-length array allocations (CA1825). --- .../Types/ImmutabilityTestTypes.cs | 66 ++++++++++- .../ThatType.IsImmutable.Tests.cs | 104 ++++++++++++++++-- .../ThatType.IsNotImmutable.Tests.cs | 25 +++-- 3 files changed, 176 insertions(+), 19 deletions(-) diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs index 0bff73a4..ff2c3fdb 100644 --- a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ImmutabilityTestTypes.cs @@ -4,7 +4,7 @@ public class ImmutableClass { public const int ConstantField = 4; - public static int StaticMutableField = 1; + private static int _staticMutableField = 1; public readonly int ReadOnlyField = 2; private readonly string _privateReadOnlyField = ""; public static int StaticSettableProperty { get; set; } @@ -49,3 +49,67 @@ public class MutableBaseClass } 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 index 8d39ca45..40f36b49 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsImmutable.Tests.cs @@ -102,6 +102,86 @@ 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() { @@ -120,15 +200,21 @@ but it was """); } - public static TheoryData ImmutableTypes() - => - [ - typeof(ImmutableClass), - typeof(ImmutableClassWithInitProperty), - typeof(ImmutableDerivedClass), - typeof(PublicRecord), - typeof(PublicSealedClass), - ]; + 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 diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs index c2799f6f..8cbe94cc 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.IsNotImmutable.Tests.cs @@ -57,15 +57,22 @@ but it was """); } - public static TheoryData MutableTypes() - => - [ - typeof(ClassWithMutableField), - typeof(ClassWithSettableProperty), - typeof(ClassWithMutableFieldAndSettableProperty), - typeof(MutableBaseClass), - typeof(ClassInheritingMutableField), - ]; + 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