From 16d445517f824b40cc9a6b239e0ec7e1fa049f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 6 Jun 2026 05:20:06 +0200 Subject: [PATCH 1/2] feat: add `HasDependenciesOutside` family to find types with dependencies outside an allowed set Implements the positive counterpart of the `DependOnlyOn` family (closes #328): - `ThatType.HasDependenciesOutside(...)` (namespaces and `Filtered.Types` targets) - `ThatTypes.HaveDependenciesOutside(...)` (sync and async enumerables) - `TypeFilters.WhichHaveDependenciesOutside(...)` The allowed set follows the same rules as `DependsOnlyOn`: sub-namespaces are included, the own namespace and framework assemblies never count as outside, and the results are chainable with `.OrOn(...)`, `.ExcludingSubNamespaces()` and `.ExcludingOwnSubNamespaces()`. Negated assertions report the offending dependencies via the shared `DependencyViolationRenderer`. --- README.md | 32 +- .../Collections/Filtered.Types.cs | 122 ++++++ ...ypeFilters.WhichHaveDependenciesOutside.cs | 69 +++ .../Helpers/DependencyViolationRenderer.cs | 4 +- .../NamespaceDependencyOutsideResult.cs | 65 +++ .../Results/TypeSetDependencyOutsideResult.cs | 48 +++ .../ThatType.HasDependenciesOutside.cs | 155 +++++++ .../ThatTypes.HaveDependenciesOutside.cs | 259 +++++++++++ .../Expected/aweXpect.Reflection_net10.0.txt | 30 ++ .../Expected/aweXpect.Reflection_net8.0.txt | 30 ++ .../aweXpect.Reflection_netstandard2.0.txt | 28 ++ ...ters.WhichHaveDependenciesOutside.Tests.cs | 158 +++++++ .../ThatType.HasDependenciesOutside.Tests.cs | 405 ++++++++++++++++++ ...ThatTypes.HaveDependenciesOutside.Tests.cs | 377 ++++++++++++++++ 14 files changed, 1772 insertions(+), 10 deletions(-) create mode 100644 Source/aweXpect.Reflection/Filters/TypeFilters.WhichHaveDependenciesOutside.cs create mode 100644 Source/aweXpect.Reflection/Results/NamespaceDependencyOutsideResult.cs create mode 100644 Source/aweXpect.Reflection/Results/TypeSetDependencyOutsideResult.cs create mode 100644 Source/aweXpect.Reflection/ThatType.HasDependenciesOutside.cs create mode 100644 Source/aweXpect.Reflection/ThatTypes.HaveDependenciesOutside.cs create mode 100644 Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichHaveDependenciesOutside.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs create mode 100644 Tests/aweXpect.Reflection.Tests/ThatTypes.HaveDependenciesOutside.Tests.cs diff --git a/README.md b/README.md index 4d4d14b1..7c6022a2 100644 --- a/README.md +++ b/README.md @@ -761,6 +761,7 @@ The dependency filters and assertions follow the familiar filter/assert pairing: | depends on namespace | `.WhichDependOn("x", …)` | `.DependsOn("x", …)` | `.DependOn("x", …)` | | does not depend on | `.WhichDoNotDependOn("x", …)`| `.DoesNotDependOn("x", …)` | `.DoNotDependOn("x", …)` | | depends only on set | `.WhichDependOnlyOn("x", …)` | `.DependsOnlyOn("x", …)`| `.DependOnlyOn("x", …)`| +| has dependencies outside set | `.WhichHaveDependenciesOutside("x", …)` | `.HasDependenciesOutside("x", …)` | `.HaveDependenciesOutside("x", …)` | ```csharp // Presentation must not reference the data layer @@ -816,14 +817,28 @@ await Expect.That(types).DoNotDependOn("MyApp.Data").ExcludingSubNamespaces(); ``` For `DependsOnlyOn` a type's own namespace is always allowed, and by default so are its sub-namespaces. Use -`.ExcludingOwnSubNamespaces()` (only available on the *only-on* family) to also forbid references into a -type's own sub-namespaces: +`.ExcludingOwnSubNamespaces()` (only available on the *only-on* and *outside* families) to also forbid +references into a type's own sub-namespaces: ```csharp await Expect.That(Types.InNamespace("MyApp.Domain")) .DependOnlyOn("MyApp.Domain").ExcludingSubNamespaces().ExcludingOwnSubNamespaces(); ``` +`HasDependenciesOutside` is the **positive counterpart** of `DependsOnlyOn` for finding the violators of an +allowed set — without a double-negated "does not depend only on". The allowed set follows the same rules +(sub-namespaces included, the own namespace and framework assemblies never count as outside, the same +chainable refinements): + +```csharp +// Select the current violators of an architecture rule (e.g. for a baseline) +In.AllLoadedAssemblies().Types().WhichHaveDependenciesOutside("MyApp.Application", "MyApp.Domain") + +// Assert that a legacy module still has its known external dependencies +await Expect.That(Types.InNamespace("MyApp.Legacy")) + .HaveDependenciesOutside("MyApp.Application", "MyApp.Domain"); +``` + `DependsOn` and `DoesNotDependOn` (single types only) also accept a **specific type** via `()` or `(Type)`, with `.OrOn()` / `.OrOn(Type)` to widen: @@ -831,7 +846,7 @@ await Expect.That(Types.InNamespace("MyApp.Domain")) await Expect.That(typeof(MyDomainType)).DoesNotDependOn().OrOn(); ``` -All three dependency families additionally accept a reusable `Filtered.Types` selection as target; see +All dependency families additionally accept a reusable `Filtered.Types` selection as target; see [Layers as type selections](#layers-as-type-selections). > **Framework dependencies are ignored unless you name one explicitly.** `DependOnlyOn` ignores dependencies @@ -908,15 +923,16 @@ Filtered.Types repositories = Types.InNamespace("MyApp.Data").WithName("Reposi ``` The dependency assertions and filters accept such a selection as a **target**, alongside the namespace and -specific-type forms: `DependsOn` / `DoesNotDependOn` / `DependsOnlyOn` (and the plural `DependOn` / -`DoNotDependOn` / `DependOnlyOn` and the `WhichDependOn` / `WhichDoNotDependOn` / `WhichDependOnlyOn` +specific-type forms: `DependsOn` / `DoesNotDependOn` / `DependsOnlyOn` / `HasDependenciesOutside` (and the +plural `DependOn` / `DoNotDependOn` / `DependOnlyOn` / `HaveDependenciesOutside` and the `WhichDependOn` / +`WhichDoNotDependOn` / `WhichDependOnlyOn` / `WhichHaveDependenciesOutside` filters) take one or more `Filtered.Types` arguments. Each target selection is resolved once per assertion; a dependency matches when it is a member of the union of the resolved selections. Matching is by type identity, where a generic type definition in the selection (e.g. a scanned `Repository<>`) matches any of its constructions. -Multiple targets and `.OrOn(…)` mean *any of*; for the *only-on* family the union is the allowed set, while -the own-namespace and framework rules apply unchanged, including the `.ExcludingOwnSubNamespaces()` opt-out -(an empty selection thus allows only the own namespace +Multiple targets and `.OrOn(…)` mean *any of*; for the *only-on* and *outside* families the union is the +allowed set, while the own-namespace and framework rules apply unchanged, including the +`.ExcludingOwnSubNamespaces()` opt-out (an empty selection thus allows only the own namespace and framework dependencies). A selection is an explicit target, so framework types contained in it are matched normally by `DependsOn` / `DoesNotDependOn`. diff --git a/Source/aweXpect.Reflection/Collections/Filtered.Types.cs b/Source/aweXpect.Reflection/Collections/Filtered.Types.cs index 0b3c5a89..9a20b22c 100644 --- a/Source/aweXpect.Reflection/Collections/Filtered.Types.cs +++ b/Source/aweXpect.Reflection/Collections/Filtered.Types.cs @@ -484,6 +484,76 @@ public NamespaceDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() } } + /// + /// A filtered collection of from a namespace-based has-dependencies-outside + /// filter, allowing to widen the allowed namespaces and to opt out of sub-namespace matching — for the + /// allowed namespaces and for the type's own namespace. + /// + /// + /// Like all filtered collections, this is an immutable value object: , + /// and do not mutate this + /// instance but rebuild a fresh filter from the original base collection, so deriving multiple views from + /// the same instance cannot corrupt each other. + /// + public sealed class NamespaceDependencyOutsideFilterResult : Types + { + private readonly Func _build; + private readonly NamespaceDependencyOptions _options; + + internal NamespaceDependencyOutsideFilterResult( + NamespaceDependencyOptions options, + Func build) + : base(build(options)) + { + _options = options; + _build = build; + } + + /// + /// Widens the allowed set by the given (including sub-namespaces unless + /// is used), so that dependencies on them no longer count as + /// outside. + /// + public NamespaceDependencyOutsideFilterResult OrOn(params IEnumerable namespaces) + { + NamespaceDependencyOptions widened = _options.Copy(); + widened.OrOn(namespaces); + return new NamespaceDependencyOutsideFilterResult(widened, _build); + } + + /// + /// Excludes sub-namespaces of the allowed namespaces from matching for the whole filter (including any + /// additions), so that dependencies on them count as outside. + /// + /// + /// Without this call, a namespace matches itself and all its sub-namespaces (so Foo.Bar includes + /// Foo.Bar.Baz but not Foo.BarBaz). + /// + /// The type's own namespace never counts as outside, and neither do its sub-namespaces unless + /// is also used. + /// + public NamespaceDependencyOutsideFilterResult ExcludingSubNamespaces() + { + NamespaceDependencyOptions refined = _options.Copy(); + refined.ExcludingSubNamespaces(); + return new NamespaceDependencyOutsideFilterResult(refined, _build); + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo + /// type referencing Foo.Bar is selected unless Foo.Bar is explicitly allowed). + /// + /// + /// The type's own namespace itself never counts as outside. + /// + public NamespaceDependencyOutsideFilterResult ExcludingOwnSubNamespaces() + { + NamespaceDependencyOptions refined = _options.Copy(); + refined.ExcludingOwnSubNamespaces(); + return new NamespaceDependencyOutsideFilterResult(refined, _build); + } + } + /// /// A filtered collection of within a namespace, that also allows clarifying /// the assembly source once (it defaults to all loaded assemblies). @@ -628,5 +698,57 @@ public TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() return new TypeSetDependencyOnlyOnFilterResult(refined, _build); } } + + /// + /// A filtered collection of from a has-dependencies-outside filter whose + /// allowed targets are filtered collections of types, allowing to widen the allowed collections and to + /// opt out of the implicit allowance of the type's own sub-namespaces. + /// + /// + /// Like all filtered collections, this is an immutable value object: and + /// do not mutate this instance but rebuild a fresh filter from + /// the original base collection, so deriving multiple views from the same instance cannot corrupt each + /// other. + /// + public sealed class TypeSetDependencyOutsideFilterResult : Types + { + private readonly Func _build; + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyOutsideFilterResult( + TypeSetDependencyOptions options, + Func build) + : base(build(options)) + { + _options = options; + _build = build; + } + + /// + /// Widens the allowed set by the given , so that dependencies on their + /// types no longer count as outside. + /// + public TypeSetDependencyOutsideFilterResult OrOn(params Filtered.Types[] targets) + { + TypeSetDependencyOptions widened = _options.Copy(); + widened.OrOn(targets); + return new TypeSetDependencyOutsideFilterResult(widened, _build); + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo + /// type referencing Foo.Bar is selected unless Foo.Bar types are part of an allowed + /// collection). + /// + /// + /// The type's own namespace itself never counts as outside. + /// + public TypeSetDependencyOutsideFilterResult ExcludingOwnSubNamespaces() + { + TypeSetDependencyOptions refined = _options.Copy(); + refined.ExcludingOwnSubNamespaces(); + return new TypeSetDependencyOutsideFilterResult(refined, _build); + } + } } } diff --git a/Source/aweXpect.Reflection/Filters/TypeFilters.WhichHaveDependenciesOutside.cs b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichHaveDependenciesOutside.cs new file mode 100644 index 00000000..ac445a1c --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/TypeFilters.WhichHaveDependenciesOutside.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using aweXpect.Customization; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; +using aweXpect.Reflection.Options; + +namespace aweXpect.Reflection; + +public static partial class TypeFilters +{ + /// + /// Filter for types which have at least one dependency (a type referenced in their signature) outside the + /// allowed (including sub-namespaces), their own namespace and framework + /// assemblies — the positive counterpart of + /// for finding violators of an + /// allowed set. + /// + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static Filtered.Types.NamespaceDependencyOutsideFilterResult WhichHaveDependenciesOutside( + this Filtered.Types @this, params IEnumerable namespaces) + => new(new NamespaceDependencyOptions(namespaces), + options => @this.Which(Filter.Suffix( + type => type.HasDependencyNamespaceViolations(options), + () => $"which have dependencies outside {options.Describe()} "))); + + /// + /// Filter for types which have at least one dependency (a type referenced in their signature) outside the + /// allowed set formed by the filtered collections of types and + /// , their own namespace and framework assemblies — the positive counterpart + /// of for finding + /// violators of an allowed set. + /// + /// + /// The target collections are resolved once per filter; a dependency is inside the allowed set when it is + /// a member of the union of the resolved collections (by identity; a generic type + /// definition in a collection matches any construction of it). A type's own namespace never counts as + /// outside, including its sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static Filtered.Types.TypeSetDependencyOutsideFilterResult WhichHaveDependenciesOutside( + this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional) + => new(new TypeSetDependencyOptions(target, additional), + options => @this.Which(Filter.Suffix( + async type => + { + ResolvedTypeSet allowed = await options.Resolve(); + return type.HasDependencyTypeSetViolations(allowed); + }, + // The parentheses delimit the target description (which ends in the target's source scope, + // e.g. "in all loaded assemblies") from the subject collection's own source suffix. + () => $"which have dependencies outside ({options.Describe()}) "))); +} diff --git a/Source/aweXpect.Reflection/Helpers/DependencyViolationRenderer.cs b/Source/aweXpect.Reflection/Helpers/DependencyViolationRenderer.cs index d42638cb..d34d1a6d 100644 --- a/Source/aweXpect.Reflection/Helpers/DependencyViolationRenderer.cs +++ b/Source/aweXpect.Reflection/Helpers/DependencyViolationRenderer.cs @@ -6,8 +6,8 @@ namespace aweXpect.Reflection.Helpers; /// -/// Renders the grouped failure output of the depend-only-on constraints: one indented line per failing -/// item, each followed by its list of disallowed dependencies. +/// Renders the grouped failure output of the depend-only-on and has-dependencies-outside constraints: one +/// indented line per failing item, each followed by its list of dependencies outside the allowed set. /// /// /// Shared between the assembly-level and the type-level constraints, so that the formatting (indentation, diff --git a/Source/aweXpect.Reflection/Results/NamespaceDependencyOutsideResult.cs b/Source/aweXpect.Reflection/Results/NamespaceDependencyOutsideResult.cs new file mode 100644 index 00000000..3985e932 --- /dev/null +++ b/Source/aweXpect.Reflection/Results/NamespaceDependencyOutsideResult.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using aweXpect.Core; +using aweXpect.Reflection.Options; +using aweXpect.Results; + +namespace aweXpect.Reflection.Results; + +/// +/// The result of a namespace-based has-dependencies-outside assertion, allowing to widen the allowed +/// namespaces and to opt out of sub-namespace matching — for the allowed namespaces and for the type's own +/// namespace. +/// +public sealed class NamespaceDependencyOutsideResult + : AndOrResult> +{ + private readonly NamespaceDependencyOptions _options; + + internal NamespaceDependencyOutsideResult( + ExpectationBuilder expectationBuilder, + IThat subject, + NamespaceDependencyOptions options) + : base(expectationBuilder, subject) + => _options = options; + + /// + /// Widens the allowed set by the given (including sub-namespaces unless + /// is used), so that dependencies on them no longer count as outside. + /// + public NamespaceDependencyOutsideResult OrOn(params IEnumerable namespaces) + { + _options.OrOn(namespaces); + return this; + } + + /// + /// Excludes sub-namespaces of the allowed namespaces from matching for the whole expression (including any + /// additions), so that dependencies on them count as outside. + /// + /// + /// Without this call, a namespace matches itself and all its sub-namespaces (so Foo.Bar includes + /// Foo.Bar.Baz but not Foo.BarBaz). + /// + /// The type's own namespace never counts as outside, and neither do its sub-namespaces unless + /// is also used. + /// + public NamespaceDependencyOutsideResult ExcludingSubNamespaces() + { + _options.ExcludingSubNamespaces(); + return this; + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo type + /// referencing Foo.Bar has a dependency outside the allowed set unless Foo.Bar is explicitly + /// allowed). + /// + /// + /// The type's own namespace itself never counts as outside. + /// + public NamespaceDependencyOutsideResult ExcludingOwnSubNamespaces() + { + _options.ExcludingOwnSubNamespaces(); + return this; + } +} diff --git a/Source/aweXpect.Reflection/Results/TypeSetDependencyOutsideResult.cs b/Source/aweXpect.Reflection/Results/TypeSetDependencyOutsideResult.cs new file mode 100644 index 00000000..c8af0999 --- /dev/null +++ b/Source/aweXpect.Reflection/Results/TypeSetDependencyOutsideResult.cs @@ -0,0 +1,48 @@ +using aweXpect.Core; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Options; +using aweXpect.Results; + +namespace aweXpect.Reflection.Results; + +/// +/// The result of a has-dependencies-outside assertion whose allowed targets are filtered collections of +/// types, allowing to widen the allowed collections and to opt out of the implicit allowance of the type's +/// own sub-namespaces. +/// +public sealed class TypeSetDependencyOutsideResult + : AndOrResult> +{ + private readonly TypeSetDependencyOptions _options; + + internal TypeSetDependencyOutsideResult( + ExpectationBuilder expectationBuilder, + IThat subject, + TypeSetDependencyOptions options) + : base(expectationBuilder, subject) + => _options = options; + + /// + /// Widens the allowed set by the given , so that dependencies on their types no + /// longer count as outside. + /// + public TypeSetDependencyOutsideResult OrOn(params Filtered.Types[] targets) + { + _options.OrOn(targets); + return this; + } + + /// + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a Foo type + /// referencing Foo.Bar has a dependency outside the allowed set unless Foo.Bar types are part + /// of an allowed collection). + /// + /// + /// The type's own namespace itself never counts as outside. + /// + public TypeSetDependencyOutsideResult ExcludingOwnSubNamespaces() + { + _options.ExcludingOwnSubNamespaces(); + return this; + } +} diff --git a/Source/aweXpect.Reflection/ThatType.HasDependenciesOutside.cs b/Source/aweXpect.Reflection/ThatType.HasDependenciesOutside.cs new file mode 100644 index 00000000..f8fbe7b8 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatType.HasDependenciesOutside.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Customization; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; +using aweXpect.Reflection.Options; +using aweXpect.Reflection.Results; + +namespace aweXpect.Reflection; + +public static partial class ThatType +{ + /// + /// Verifies that the has at least one dependency (a type referenced in its signature) + /// outside the allowed (including sub-namespaces), its own namespace and + /// framework assemblies — the positive counterpart of + /// for finding violators of an allowed set. + /// + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static NamespaceDependencyOutsideResult HasDependenciesOutside( + this IThat subject, params IEnumerable namespaces) + { + NamespaceDependencyOptions options = new(namespaces); + return new NamespaceDependencyOutsideResult(subject.Get().ExpectationBuilder + .AddConstraint((it, grammars) + => new HasDependenciesOutsideConstraint(it, grammars, options)), + subject, + options); + } + + /// + /// Verifies that the has at least one dependency (a type referenced in its signature) + /// outside the allowed set formed by the filtered collections of types and + /// , its own namespace and framework assemblies — the positive counterpart of + /// for finding violators of an + /// allowed set. + /// + /// + /// The target collections are resolved once per assertion; a dependency is inside the allowed set when it + /// is a member of the union of the resolved collections (by identity; a generic type + /// definition in a collection matches any construction of it). The type's own namespace never counts as + /// outside, including its sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static TypeSetDependencyOutsideResult HasDependenciesOutside( + this IThat subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOutsideResult(subject.Get().ExpectationBuilder + .AddConstraint((it, grammars) + => new HasDependenciesOutsideTypeSetConstraint(it, grammars, options)), + subject, + options); + } + + private sealed class HasDependenciesOutsideConstraint( + string it, + ExpectationGrammars grammars, + NamespaceDependencyOptions options) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + private IReadOnlyList _violations = []; + + public ConstraintResult IsMetBy(Type? actual) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + _violations = actual.GetDependencyNamespaceViolations(options); + Outcome = _violations.Count > 0 ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("has dependencies outside ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(It).Append(" only depended on the allowed namespaces"); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not have dependencies outside ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" also depended on "); + Formatter.Format(stringBuilder, _violations); + } + } + + private sealed class HasDependenciesOutsideTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : ConstraintResult.WithNotNullValue(it, grammars), + IAsyncConstraint + { + private IReadOnlyList _violations = []; + + public async Task IsMetBy(Type? actual, CancellationToken cancellationToken) + { + Actual = actual; + if (actual is null) + { + Outcome = Outcome.Failure; + return this; + } + + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + _violations = actual.GetDependencyTypeSetViolations(allowed); + Outcome = _violations.Count > 0 ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("has dependencies outside ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(It).Append(" only depended on the allowed types"); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("does not have dependencies outside ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" also depended on "); + Formatter.Format(stringBuilder, _violations); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatTypes.HaveDependenciesOutside.cs b/Source/aweXpect.Reflection/ThatTypes.HaveDependenciesOutside.cs new file mode 100644 index 00000000..42bec02f --- /dev/null +++ b/Source/aweXpect.Reflection/ThatTypes.HaveDependenciesOutside.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Customization; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; +using aweXpect.Reflection.Options; +using aweXpect.Reflection.Results; + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Reflection; + +public static partial class ThatTypes +{ + /// + /// Verifies that all items in the filtered collection of have at least one dependency + /// (a type referenced in their signature) outside the allowed (including + /// sub-namespaces), their own namespace and framework assemblies — the positive counterpart of + /// for finding violators of an + /// allowed set. + /// + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static NamespaceDependencyOutsideResult> HaveDependenciesOutside( + this IThat> subject, params IEnumerable namespaces) + { + NamespaceDependencyOptions options = new(namespaces); + return new NamespaceDependencyOutsideResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new HaveDependenciesOutsideConstraint(it, grammars, options)), + subject, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of have at least one dependency + /// (a type referenced in their signature) outside the allowed (including + /// sub-namespaces), their own namespace and framework assemblies — the positive counterpart of + /// for finding violators + /// of an allowed set. + /// + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static NamespaceDependencyOutsideResult> HaveDependenciesOutside( + this IThat> subject, params IEnumerable namespaces) + { + NamespaceDependencyOptions options = new(namespaces); + return new NamespaceDependencyOutsideResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new HaveDependenciesOutsideConstraint(it, grammars, options)), + subject, + options); + } +#endif + + /// + /// Verifies that all items in the filtered collection of have at least one dependency + /// (a type referenced in their signature) outside the allowed set formed by the filtered collections of + /// types and , their own namespace and framework + /// assemblies — the positive counterpart of + /// for finding + /// violators of an allowed set. + /// + /// + /// The target collections are resolved once per assertion; a dependency is inside the allowed set when it + /// is a member of the union of the resolved collections (by identity; a generic type + /// definition in a collection matches any construction of it). A type's own namespace never counts as + /// outside, including its sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static TypeSetDependencyOutsideResult> HaveDependenciesOutside( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOutsideResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new HaveDependenciesOutsideTypeSetConstraint(it, grammars, options)), + subject, + options); + } + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of have at least one dependency + /// (a type referenced in their signature) outside the allowed set formed by the filtered collections of + /// types and , their own namespace and framework + /// assemblies — the positive counterpart of + /// for + /// finding violators of an allowed set. + /// + /// + /// The target collections are resolved once per assertion; a dependency is inside the allowed set when it + /// is a member of the union of the resolved collections (by identity; a generic type + /// definition in a collection matches any construction of it). A type's own namespace never counts as + /// outside, including its sub-namespaces unless + /// is used. + /// + /// Dependencies on types whose assembly name matches one of the + /// at a + /// name-segment boundary (System covers System.Text.Json, but not + /// SystemsBiology.Core) are ignored, so that framework dependencies never count as outside the + /// allowed set. The default prefixes include Microsoft, so e.g. a dependency on + /// Microsoft.EntityFrameworkCore is also ignored; customize the prefixes to make such a dependency + /// count. + /// + public static TypeSetDependencyOutsideResult> HaveDependenciesOutside( + this IThat> subject, Filtered.Types target, params Filtered.Types[] additional) + { + TypeSetDependencyOptions options = new(target, additional); + return new TypeSetDependencyOutsideResult>(subject.Get().ExpectationBuilder + .AddConstraint>((it, grammars) + => new HaveDependenciesOutsideTypeSetConstraint(it, grammars, options)), + subject, + options); + } +#endif + + private sealed class HaveDependenciesOutsideConstraint( + string it, + ExpectationGrammars grammars, + NamespaceDependencyOptions options) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { + private readonly Dictionary> _violations = new(); + + private bool HasDependencyOutsideAllowed(Type? type) + { + if (type is null) + { + return false; + } + + IReadOnlyList violations = type.GetDependencyNamespaceViolations(options); + if (violations.Count > 0) + { + _violations[type] = violations; + } + + return violations.Count > 0; + } + +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, CancellationToken cancellationToken) + => await SetAsyncValue(actual, HasDependencyOutsideAllowed); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, HasDependencyOutsideAllowed); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("all have dependencies outside ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained types depending only on the allowed namespaces "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all have dependencies outside ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => DependencyViolationRenderer.AppendItemsWithDisallowedDependencies(stringBuilder, it, + " only contained types with dependencies outside the allowed namespaces ", Matching, _violations, + indentation); + } + + private sealed class HaveDependenciesOutsideTypeSetConstraint( + string it, + ExpectationGrammars grammars, + TypeSetDependencyOptions options) + : CollectionConstraintResult(grammars), + IAsyncConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { + private readonly Dictionary> _violations = new(); + + private bool HasDependencyOutsideAllowed(Type? type, ResolvedTypeSet allowed) + { + if (type is null) + { + return false; + } + + IReadOnlyList violations = type.GetDependencyTypeSetViolations(allowed); + if (violations.Count > 0) + { + _violations[type] = violations; + } + + return violations.Count > 0; + } + +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + return await SetAsyncValue(actual, type => HasDependencyOutsideAllowed(type, allowed)); + } +#endif + + public async Task IsMetBy(IEnumerable actual, CancellationToken cancellationToken) + { + ResolvedTypeSet allowed = await options.Resolve(cancellationToken); + return SetValue(actual, type => HasDependencyOutsideAllowed(type, allowed)); + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("all have dependencies outside ").Append(options.Describe()); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained types depending only on the allowed types "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("not all have dependencies outside ").Append(options.Describe()); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => DependencyViolationRenderer.AppendItemsWithDisallowedDependencies(stringBuilder, it, + " only contained types with dependencies outside the allowed types ", Matching, _violations, + 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..10b09dd8 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 @@ -1646,6 +1646,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.HasAttributeResult Has(this aweXpect.Core.IThat subject, System.Func predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> HasADefaultConstructor(this aweXpect.Core.IThat subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasImplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } @@ -1853,6 +1855,10 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, bool inherit = false) { } @@ -1963,6 +1969,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } @@ -2227,6 +2235,12 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public class StringEqualityResult : aweXpect.Reflection.Collections.Filtered.Types { public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult IgnoringCase(bool ignoreCase = true) { } @@ -2251,6 +2265,11 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IAsyncEnumerable @@ -2490,6 +2509,12 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public sealed class NamespaceDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.NamespaceDependencyResult ExcludingSubNamespaces() { } @@ -2536,6 +2561,11 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } 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..1ff32665 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 @@ -1646,6 +1646,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.HasAttributeResult Has(this aweXpect.Core.IThat subject, System.Func predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> HasADefaultConstructor(this aweXpect.Core.IThat subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasImplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } @@ -1853,6 +1855,10 @@ namespace aweXpect.Reflection where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, bool inherit = false) { } @@ -1963,6 +1969,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } @@ -2227,6 +2235,12 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public class StringEqualityResult : aweXpect.Reflection.Collections.Filtered.Types { public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult IgnoringCase(bool ignoreCase = true) { } @@ -2251,6 +2265,11 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IAsyncEnumerable @@ -2490,6 +2509,12 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public sealed class NamespaceDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.NamespaceDependencyResult ExcludingSubNamespaces() { } @@ -2536,6 +2561,11 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } 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..6f46a4dc 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 @@ -1370,6 +1370,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.HasAttributeResult Has(this aweXpect.Core.IThat subject, System.Func predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult> HasADefaultConstructor(this aweXpect.Core.IThat subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult HasDependenciesOutside(this aweXpect.Core.IThat subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasExplicitConversionOperator(this aweXpect.Core.IThat subject, bool inherit = false) { } public static aweXpect.Results.AndOrResult> HasImplicitConversionOperator(this aweXpect.Core.IThat subject, System.Type source, System.Type target, bool inherit = false) { } @@ -1503,6 +1505,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Results.HaveAttributeResult> Have(this aweXpect.Core.IThat> subject, System.Func predicate, bool inherit = true, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") where TAttribute : System.Attribute { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveADefaultConstructor(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Reflection.Results.NamespaceDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Results.TypeSetDependencyOutsideResult> HaveDependenciesOutside(this aweXpect.Core.IThat> subject, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveExplicitConversionOperator(this aweXpect.Core.IThat> subject, bool inherit = false) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> HaveImplicitConversionOperator(this aweXpect.Core.IThat> subject, System.Type source, System.Type target, bool inherit = false) { } @@ -1599,6 +1603,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichDoNotInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichHaveADefaultConstructor(this aweXpect.Reflection.Collections.Filtered.Types @this) { } + public static aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Collections.Generic.IEnumerable namespaces) { } + public static aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult WhichHaveDependenciesOutside(this aweXpect.Reflection.Collections.Filtered.Types @this, aweXpect.Reflection.Collections.Filtered.Types target, params aweXpect.Reflection.Collections.Filtered.Types[] additional) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type interfaceType, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichImplement(this aweXpect.Reflection.Collections.Filtered.Types @this, bool forceDirect = false) { } public static aweXpect.Reflection.Collections.Filtered.Types WhichInheritFrom(this aweXpect.Reflection.Collections.Filtered.Types @this, System.Type baseType, bool forceDirect = false) { } @@ -1863,6 +1869,12 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOnlyOnFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyOutsideFilterResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public class StringEqualityResult : aweXpect.Reflection.Collections.Filtered.Types { public aweXpect.Reflection.Collections.Filtered.Types.StringEqualityResult IgnoringCase(bool ignoreCase = true) { } @@ -1887,6 +1899,11 @@ namespace aweXpect.Reflection.Collections public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOnlyOnFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideFilterResult : aweXpect.Reflection.Collections.Filtered.Types + { + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Collections.Filtered.Types.TypeSetDependencyOutsideFilterResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } } } public abstract class Filtered : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable @@ -2125,6 +2142,12 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult ExcludingSubNamespaces() { } public aweXpect.Reflection.Results.NamespaceDependencyOnlyOnResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } } + public sealed class NamespaceDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult ExcludingSubNamespaces() { } + public aweXpect.Reflection.Results.NamespaceDependencyOutsideResult OrOn(System.Collections.Generic.IEnumerable namespaces) { } + } public sealed class NamespaceDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.NamespaceDependencyResult ExcludingSubNamespaces() { } @@ -2171,6 +2194,11 @@ namespace aweXpect.Reflection.Results public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult ExcludingOwnSubNamespaces() { } public aweXpect.Reflection.Results.TypeSetDependencyOnlyOnResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } } + public sealed class TypeSetDependencyOutsideResult : aweXpect.Results.AndOrResult> + { + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult ExcludingOwnSubNamespaces() { } + public aweXpect.Reflection.Results.TypeSetDependencyOutsideResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } + } public sealed class TypeSetDependencyResult : aweXpect.Results.AndOrResult> { public aweXpect.Reflection.Results.TypeSetDependencyResult OrOn(params aweXpect.Reflection.Collections.Filtered.Types[] targets) { } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichHaveDependenciesOutside.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichHaveDependenciesOutside.Tests.cs new file mode 100644 index 00000000..f7b658ea --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/TypeFilters.WhichHaveDependenciesOutside.Tests.cs @@ -0,0 +1,158 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class TypeFilters +{ + public sealed class WhichHaveDependenciesOutside + { + private const string ConsumersNamespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers"; + private const string Layer1Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer1"; + private const string Layer2Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2"; + + public sealed class Tests + { + [Fact] + public async Task ShouldFilterForTypesWithDependenciesOutsideAllowedNamespaces() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace); + + await That(types).Contains(typeof(Layer1AndLayer2)); + await That(types).Contains(typeof(OnlyLayer2)); + await That(types).DoesNotContain(typeof(OnlyLayer1)); + await That(types).DoesNotContain(typeof(FrameworkConsumer)); + await That(types).DoesNotContain(typeof(ReferencesOwnNamespace)); + } + + [Fact] + public async Task ShouldIncludeFilterInDescription() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace); + + await That(types.GetDescription()) + .Contains($"which have dependencies outside namespace \"{Layer1Namespace}\""); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFilterOutTypesDependingOnEither() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace) + .OrOn(Layer2Namespace); + + await That(types).DoesNotContain(typeof(Layer1AndLayer2)); + await That(types).DoesNotContain(typeof(OnlyLayer2)); + } + + [Fact] + public async Task WhenNotExcludingSubNamespaces_SubNamespaceDependencyStaysInside() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace); + + await That(types).DoesNotContain(typeof(ViaSubNamespace)); + } + + [Fact] + public async Task WhenExcludingSubNamespaces_SubNamespaceDependencyCountsAsOutside() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace) + .ExcludingSubNamespaces(); + + await That(types).Contains(typeof(ViaSubNamespace)); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_ShouldSelectTypesReferencingOwnSubNamespace() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace) + .ExcludingOwnSubNamespaces(); + + await That(types).Contains(typeof(ReferencesOwnSubNamespace)); + } + + [Fact] + public async Task WhenNotExcludingOwnSubNamespaces_OwnSubNamespaceStaysInside() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Layer1Namespace); + + await That(types).DoesNotContain(typeof(ReferencesOwnSubNamespace)); + } + + [Fact] + public async Task ExcludingOwnSubNamespaces_ShouldNotAffectOriginalFilter() + { + Filtered.Types.NamespaceDependencyOutsideFilterResult original = + Types.InNamespace(ConsumersNamespace).WhichHaveDependenciesOutside(Layer1Namespace); + _ = original.ExcludingOwnSubNamespaces(); + + await That(original).DoesNotContain(typeof(ReferencesOwnSubNamespace)); + } + } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task ShouldFilterForTypesWithDependenciesOutsideTargetCollection() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(types).Contains(typeof(Layer1AndLayer2)); + await That(types).Contains(typeof(OnlyLayer2)); + await That(types).DoesNotContain(typeof(OnlyLayer1)); + await That(types).DoesNotContain(typeof(FrameworkConsumer)); + await That(types).DoesNotContain(typeof(ReferencesOwnNamespace)); + } + + [Fact] + public async Task ShouldIncludeFilterInDescription() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(types.GetDescription()) + .Contains( + $"which have dependencies outside (types within namespace \"{Layer1Namespace}\" in all loaded assemblies)"); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFilterOutTypesDependingOnEither() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(Layer2Namespace)); + + await That(types).DoesNotContain(typeof(Layer1AndLayer2)); + await That(types).DoesNotContain(typeof(OnlyLayer2)); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_ShouldSelectTypesReferencingOwnSubNamespace() + { + Filtered.Types types = Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(types).Contains(typeof(ReferencesOwnSubNamespace)); + } + + [Fact] + public async Task ExcludingOwnSubNamespaces_ShouldNotAffectOriginalFilter() + { + Filtered.Types.TypeSetDependencyOutsideFilterResult original = + Types.InNamespace(ConsumersNamespace) + .WhichHaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + _ = original.ExcludingOwnSubNamespaces(); + + await That(original).DoesNotContain(typeof(ReferencesOwnSubNamespace)); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs new file mode 100644 index 00000000..57a26657 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs @@ -0,0 +1,405 @@ +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; +using Xunit.Sdk; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatType +{ + public sealed class HasDependenciesOutside + { + private const string Layer1Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer1"; + private const string Layer2Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2"; + + public sealed class Tests + { + [Fact] + public async Task WhenDependingOnNamespaceOutsideAllowedSet_ShouldSucceed() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAllDependenciesAreAllowed_ShouldFail() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenAllowingMultipleNamespaces_ShouldFail() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace, Layer2Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}" or "{Layer2Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFail() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace).OrOn(Layer2Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}" or "{Layer2Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenNoNamespaceIsSpecified_ShouldThrowArgumentException() + { + Type subject = typeof(OnlyLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(); + + await That(Act).Throws() + .WithMessage("At least one namespace must be specified."); + } + + [Fact] + public async Task WhenDependingOnlyOnOwnNamespace_ShouldFail() + { + // The type's own namespace never counts as outside. + Type subject = typeof(ReferencesOwnNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenDependingOnlyOnFramework_ShouldFail() + { + // Framework dependencies never count as outside. + Type subject = typeof(FrameworkConsumer); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenDependingOnSubNamespaceOfAllowedNamespace_ShouldFail() + { + // ViaSubNamespace references only Layer1.Sub, which is covered by Layer1 by default. + Type subject = typeof(ViaSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenExcludingSubNamespaces_SubNamespaceDependencyCountsAsOutside() + { + Type subject = typeof(ViaSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace).ExcludingSubNamespaces(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenExcludingSubNamespaces_OwnSubNamespaceStaysAllowed() + { + // ReferencesOwnSubNamespace (in ...Consumers) only references ...Consumers.OwnSub. By default the + // type's own sub-namespaces never count as outside, even when sub-namespaces are excluded. + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace).ExcludingSubNamespaces(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_OwnSubNamespaceCountsAsOutside() + { + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace).ExcludingOwnSubNamespaces(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenDependingOnGlobalNamespace_ShouldSucceed() + { + Type subject = typeof(ReferencesGlobal); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenGlobalNamespaceIsAllowedWithEmptyString_ShouldFail() + { + // An empty string allows exactly the global namespace. + Type subject = typeof(ReferencesGlobal); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace, ""); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}" or "", + but it only depended on the allowed namespaces + """); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it was + """); + } + } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenDependingOnTypeOutsideTargetCollections_ShouldSucceed() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAllDependenciesAreInTargetCollection_ShouldFail() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it only depended on the allowed types + """); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFail() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies or types within namespace "{Layer2Namespace}" in all loaded assemblies, + but it only depended on the allowed types + """); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_FrameworkDependenciesStayInside() + { + Type subject = typeof(FrameworkConsumer); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).Throws(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_OwnNamespaceStaysInside() + { + Type subject = typeof(ReferencesOwnNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).Throws(); + } + + [Fact] + public async Task WhenTargetCollectionIsEmpty_DisallowedDependencyShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace("Non.Existent.Namespace")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenReferencingOwnSubNamespace_ShouldFailByDefault() + { + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it only depended on the allowed types + """); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_OwnSubNamespaceCountsAsOutside() + { + Type subject = typeof(ReferencesOwnSubNamespace); + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenTypeIsNull_ShouldFail() + { + Type? subject = null; + + async Task Act() + => await That(subject).HasDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it was + """); + } + + [Fact] + public async Task WhenNegated_ShouldSucceedForTypeDependingOnlyOnAllowedTypes() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject) + .DoesNotComplyWith(it => it.HasDependenciesOutside(Types.InNamespace(Layer1Namespace))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenNegated_ShouldFailForTypeWithDependencyOutside() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject) + .DoesNotComplyWith(it => it.HasDependenciesOutside(Types.InNamespace(Layer1Namespace))); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + does not have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it also depended on ["TargetB"] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenAllDependenciesAreAllowed_ShouldSucceed() + { + Type subject = typeof(OnlyLayer1); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.HasDependenciesOutside(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenDependingOnNamespaceOutsideAllowedSet_ShouldFail() + { + Type subject = typeof(Layer1AndLayer2); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.HasDependenciesOutside(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + does not have dependencies outside namespace "{Layer1Namespace}", + but it also depended on ["{Layer2Namespace}"] + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatTypes.HaveDependenciesOutside.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatTypes.HaveDependenciesOutside.Tests.cs new file mode 100644 index 00000000..a429f2d7 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatTypes.HaveDependenciesOutside.Tests.cs @@ -0,0 +1,377 @@ +using System.Collections.Generic; +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; +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 HaveDependenciesOutside + { + private const string Layer1Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer1"; + private const string Layer2Namespace = "aweXpect.Reflection.Tests.TestHelpers.Dependencies.Layer2"; + + public sealed class Tests + { + [Fact] + public async Task WhenAllTypesHaveDependenciesOutside_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer2), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypeDependsOnlyOnAllowedNamespaces_ShouldFail() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside namespace "{Layer1Namespace}", + but it contained types depending only on the allowed namespaces [ + OnlyLayer1 + ] + """); + } + + [Fact] + public async Task WhenCollectionContainsNull_ShouldListNull() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + null, + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside namespace "{Layer1Namespace}", + but it contained types depending only on the allowed namespaces [ + + ] + """); + } + + [Fact] + public async Task WhenAllowingAllNamespaces_ShouldFail() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace, Layer2Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside namespace "{Layer1Namespace}" or "{Layer2Namespace}", + but it contained types depending only on the allowed namespaces [ + Layer1AndLayer2 + ] + """); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task WhenAsyncEnumerableTypesHaveDependenciesOutside_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(Layer1AndLayer2), typeof(OnlyLayer2), + }.ToTestAsyncEnumerable(); + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAsyncEnumerableContainsTypeDependingOnlyOnAllowedNamespaces_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(Layer1AndLayer2), typeof(OnlyLayer1), + }.ToTestAsyncEnumerable(); + + async Task Act() + => await That(subject).HaveDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside namespace "{Layer1Namespace}", + but it contained types depending only on the allowed namespaces [ + OnlyLayer1 + ] + """); + } +#endif + } + + public sealed class FilteredTypesTargetTests + { + [Fact] + public async Task WhenAllTypesHaveDependenciesOutsideTargetCollection_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer2), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSomeTypeDependsOnlyOnTargetCollections_ShouldFail() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types depending only on the allowed types [ + OnlyLayer1 + ] + """); + } + + [Fact] + public async Task WhenCollectionContainsNull_ShouldListNull() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + null, + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types depending only on the allowed types [ + + ] + """); + } + + [Fact] + public async Task WhenWidenedWithOrOn_ShouldFail() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .OrOn(Types.InNamespace(Layer2Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies or types within namespace "{Layer2Namespace}" in all loaded assemblies, + but it contained types depending only on the allowed types [ + Layer1AndLayer2 + ] + """); + } + + [Fact] + public async Task WhenReferencingOwnSubNamespace_ShouldFailByDefault() + { + IEnumerable subject = + [ + typeof(ReferencesOwnSubNamespace), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types depending only on the allowed types [ + ReferencesOwnSubNamespace + ] + """); + } + + [Fact] + public async Task WhenExcludingOwnSubNamespaces_OwnSubNamespaceCountsAsOutside() + { + IEnumerable subject = + [ + typeof(ReferencesOwnSubNamespace), + ]; + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)) + .ExcludingOwnSubNamespaces(); + + await That(Act).DoesNotThrow(); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task WhenAsyncEnumerableTypesHaveDependenciesOutsideTargetCollection_ShouldSucceed() + { + IAsyncEnumerable subject = new[] + { + typeof(Layer1AndLayer2), typeof(OnlyLayer2), + }.ToTestAsyncEnumerable(); + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAsyncEnumerableContainsTypeDependingOnlyOnTargetCollections_ShouldFail() + { + IAsyncEnumerable subject = new[] + { + typeof(Layer1AndLayer2), typeof(OnlyLayer1), + }.ToTestAsyncEnumerable(); + + async Task Act() + => await That(subject).HaveDependenciesOutside(Types.InNamespace(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it contained types depending only on the allowed types [ + OnlyLayer1 + ] + """); + } +#endif + + [Fact] + public async Task WhenNegated_ShouldSucceedWhenSomeTypeDependsOnlyOnTargetCollections() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject) + .DoesNotComplyWith(they => they.HaveDependenciesOutside(Types.InNamespace(Layer1Namespace))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenNegated_ShouldFailWhenAllTypesHaveDependenciesOutside() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer2), + ]; + + async Task Act() + => await That(subject) + .DoesNotComplyWith(they => they.HaveDependenciesOutside(Types.InNamespace(Layer1Namespace))); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + not all have dependencies outside types within namespace "{Layer1Namespace}" in all loaded assemblies, + but it only contained types with dependencies outside the allowed types [ + Layer1AndLayer2 depends on ["TargetB"], + OnlyLayer2 depends on ["TargetB"] + ] + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenSomeTypeDependsOnlyOnAllowedNamespaces_ShouldSucceed() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer1), + ]; + + async Task Act() + => await That(subject) + .DoesNotComplyWith(they => they.HaveDependenciesOutside(Layer1Namespace)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenAllTypesHaveDependenciesOutside_ShouldFail() + { + IEnumerable subject = + [ + typeof(Layer1AndLayer2), + typeof(OnlyLayer2), + ]; + + async Task Act() + => await That(subject) + .DoesNotComplyWith(they => they.HaveDependenciesOutside(Layer1Namespace)); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + not all have dependencies outside namespace "{Layer1Namespace}", + but it only contained types with dependencies outside the allowed namespaces [ + Layer1AndLayer2 depends on ["{Layer2Namespace}"], + OnlyLayer2 depends on ["{Layer2Namespace}"] + ] + """); + } + } + } +} From 9b077d045fd40d7971d3f0c39b1c165d310a38cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 6 Jun 2026 05:30:54 +0200 Subject: [PATCH 2/2] docs: address review feedback on the has-dependencies-outside family - Fix the README example asserting a legacy module: the plural assertion is all-quantified, so demonstrate the single-type assertion instead. - Extend the stale (for depends-only-on) remarks in the shared dependency options to mention the has-dependencies-outside family. - Mirror two only-on edge tests: a subject in the global namespace and the namespace-qualification of same-named violators in the negated message. --- README.md | 6 ++-- .../Options/NamespaceDependencyOptions.cs | 6 ++-- .../Options/TypeSetDependencyOptions.cs | 6 ++-- .../ThatType.HasDependenciesOutside.Tests.cs | 32 +++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7c6022a2..d165ae77 100644 --- a/README.md +++ b/README.md @@ -834,9 +834,9 @@ chainable refinements): // Select the current violators of an architecture rule (e.g. for a baseline) In.AllLoadedAssemblies().Types().WhichHaveDependenciesOutside("MyApp.Application", "MyApp.Domain") -// Assert that a legacy module still has its known external dependencies -await Expect.That(Types.InNamespace("MyApp.Legacy")) - .HaveDependenciesOutside("MyApp.Application", "MyApp.Domain"); +// Assert that a known legacy type still has its external dependency +await Expect.That(typeof(LegacyImportService)) + .HasDependenciesOutside("MyApp.Application", "MyApp.Domain"); ``` `DependsOn` and `DoesNotDependOn` (single types only) also accept a **specific type** via `()` or diff --git a/Source/aweXpect.Reflection/Options/NamespaceDependencyOptions.cs b/Source/aweXpect.Reflection/Options/NamespaceDependencyOptions.cs index 086489f3..30146220 100644 --- a/Source/aweXpect.Reflection/Options/NamespaceDependencyOptions.cs +++ b/Source/aweXpect.Reflection/Options/NamespaceDependencyOptions.cs @@ -47,7 +47,8 @@ public NamespaceDependencyOptions Copy() public bool ExcludeSubNamespaces => _excludeSubNamespaces; /// - /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on). + /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on + /// and has-dependencies-outside). /// /// /// The type's own namespace is always allowed; its sub-namespaces stay allowed unless the caller opted into @@ -123,7 +124,8 @@ public void ExcludingSubNamespaces() => _excludeSubNamespaces = true; /// - /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on). + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on + /// and has-dependencies-outside). /// public void ExcludingOwnSubNamespaces() => _excludeOwnSubNamespaces = true; diff --git a/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs b/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs index 88108149..3c3c4343 100644 --- a/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs +++ b/Source/aweXpect.Reflection/Options/TypeSetDependencyOptions.cs @@ -46,7 +46,8 @@ private TypeSetDependencyOptions(IEnumerable targets, bool exclu } /// - /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on). + /// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on + /// and has-dependencies-outside). /// /// /// The type's own namespace is always allowed; its sub-namespaces stay allowed unless the caller opted into @@ -79,7 +80,8 @@ public void OrOn(Filtered.Types[] targets) } /// - /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on). + /// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on + /// and has-dependencies-outside). /// /// /// Only the exemption rule changes, not the resolved target set, so a previously resolved set stays valid. diff --git a/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs index 57a26657..33995ac3 100644 --- a/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs +++ b/Tests/aweXpect.Reflection.Tests/ThatType.HasDependenciesOutside.Tests.cs @@ -1,4 +1,5 @@ using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Consumers; +using aweXpect.Reflection.Tests.TestHelpers.Dependencies.Synthetic; using Xunit.Sdk; namespace aweXpect.Reflection.Tests; @@ -202,6 +203,22 @@ but it only depended on the allowed namespaces """); } + [Fact] + public async Task WhenSubjectIsInGlobalNamespaceAndDependsOnlyOnFramework_ShouldFail() + { + Type subject = typeof(GlobalNamespaceTarget); + + async Task Act() + => await That(subject).HasDependenciesOutside(Layer1Namespace); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + has dependencies outside namespace "{Layer1Namespace}", + but it only depended on the allowed namespaces + """); + } + [Fact] public async Task WhenTypeIsNull_ShouldFail() { @@ -354,6 +371,21 @@ async Task Act() await That(Act).DoesNotThrow(); } + [Fact] + public async Task WhenNegatedAndDistinctViolatorsShareTheSimpleName_ShouldQualifyThemByNamespace() + { + // Both AmbiguousTarget dependencies are outside the allowed set; they must stay apart in the + // message instead of collapsing into one indistinguishable "AmbiguousTarget" entry. + Type subject = typeof(WithSameNamedDependencies); + + async Task Act() + => await That(subject) + .DoesNotComplyWith(it => it.HasDependenciesOutside(Types.InNamespace(Layer1Namespace))); + + await That(Act).Throws() + .WithMessage("*AmbiguousA.AmbiguousTarget*AmbiguousB.AmbiguousTarget*").AsWildcard(); + } + [Fact] public async Task WhenNegated_ShouldFailForTypeWithDependencyOutside() {