From 97b5b2da6bf2ebdafe0e0c7067c9e64e42f2d8c5 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 14 Aug 2026 15:05:34 +0200 Subject: [PATCH 1/4] test: move slice fixtures into a dedicated SlicesTestAssembly The slice tests shared TestAssembly with the rest of the suite, which meant their namespace layout could not be changed without disturbing unrelated tests, and it offered no way to express the namespace shapes slice patterns need to be tested against. Add TestAssemblies/SlicesTestAssembly, mirroring the other purpose-built test assemblies, with one folder per scenario: DirectCircle two slices depending on each other SubnamespaceCircle a cycle that only appears when sub-namespaces are folded into their parent slice (see #208) MultipleSubnamespaces slices nested up to three levels deep DuplicatePrefix a namespace whose last segment repeats its parent DotDotSemantics namespace shapes that distinguish a ".." matching whole segments from one matching within a segment No production code changes. The expectations in SlicesTests are the current implementation's, adjusted only for the new fixture: MultipleSubnamespaces has nine namespaces where TestAssembly.Slices had seven, and Slice3 now has four sub-namespaces rather than two. Signed-off-by: Alexander Linne --- .csharpierignore | 3 + ArchUnit.sln | 15 ++++ .../Storage/CustomPathFrozenRules.json | 2 +- .../ArchUnitNET/Storage/FrozenRules.json | 4 +- .../ArchUnitNET/Storage/FrozenRules.xml | 6 +- ArchUnitNETTests/ArchUnitNETTests.csproj | 1 + .../SlicesAssemblyTestHelper.cs | 9 +++ ArchUnitNETTests/Fluent/FreezeTests.cs | 15 ++-- ArchUnitNETTests/Fluent/Slices/SlicesTests.cs | 72 +++++++++---------- ArchUnitNETTests/StaticTestArchitectures.cs | 6 ++ .../DirectCircle/Slice1/Slice1Class.cs | 8 +++ .../DirectCircle/Slice2/Slice2Class.cs | 8 +++ .../Alpha/Service/AlphaServiceSegmentClass.cs | 4 ++ .../AlphaService/AlphaServiceClass.cs | 6 ++ .../DotDotSemantics/Outer/Inner/InnerClass.cs | 4 ++ .../Outer/Mid/Inner/MidInnerClass.cs | 4 ++ .../DotDotSemantics/Single/SingleClass.cs | 5 ++ .../DuplicatePrefix/Sub/OuterClass.cs | 3 + .../DuplicatePrefix/Sub/Sub/InnerClass.cs | 3 + .../Slice1/Service/Service1Class.cs | 8 +++ .../Slice1/Slice1Class.cs | 8 +++ .../Slice2/Service/Service2Class.cs | 8 +++ .../Slice2/Slice2Class.cs | 8 +++ .../Slice3/Group1/Group1Class.cs | 6 ++ .../Slice3/Group1/Inner/Inner1Class.cs | 3 + .../Slice3/Group2/Group2Class.cs | 3 + .../Slice3/Group2/Inner/Inner2Class.cs | 3 + .../Slice3/Slice3Class.cs | 8 +++ .../SlicesTestAssembly.csproj | 12 ++++ .../SubnamespaceCircle/Slice1/Slice1Class.cs | 8 +++ .../Slice2/Inner/Slice2InnerClass.cs | 6 ++ .../SubnamespaceCircle/Slice2/Slice2Class.cs | 3 + .../Slices/Slice1/Service/Service1Class.cs | 9 --- TestAssembly/Slices/Slice1/Slice1Class.cs | 9 --- .../Slices/Slice2/Service/Service2Class.cs | 9 --- TestAssembly/Slices/Slice2/Slice2Class.cs | 9 --- .../Slices/Slice3/Group1/Group1Class.cs | 7 -- .../Slices/Slice3/Group2/Group2Class.cs | 4 -- TestAssembly/Slices/Slice3/Slice3Class.cs | 9 --- 39 files changed, 212 insertions(+), 106 deletions(-) create mode 100644 .csharpierignore create mode 100644 ArchUnitNETTests/AssemblyTestHelper/SlicesAssemblyTestHelper.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DirectCircle/Slice1/Slice1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DirectCircle/Slice2/Slice2Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DotDotSemantics/Alpha/Service/AlphaServiceSegmentClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Inner/InnerClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Mid/Inner/MidInnerClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DotDotSemantics/Single/SingleClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Service/Service1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Slice1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Service/Service2Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Slice2Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Group1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Inner/Inner1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Group2Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Inner/Inner2Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Slice3Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/SlicesTestAssembly.csproj create mode 100644 TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice1/Slice1Class.cs create mode 100644 TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Inner/Slice2InnerClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Slice2Class.cs delete mode 100644 TestAssembly/Slices/Slice1/Service/Service1Class.cs delete mode 100644 TestAssembly/Slices/Slice1/Slice1Class.cs delete mode 100644 TestAssembly/Slices/Slice2/Service/Service2Class.cs delete mode 100644 TestAssembly/Slices/Slice2/Slice2Class.cs delete mode 100644 TestAssembly/Slices/Slice3/Group1/Group1Class.cs delete mode 100644 TestAssembly/Slices/Slice3/Group2/Group2Class.cs delete mode 100644 TestAssembly/Slices/Slice3/Slice3Class.cs diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 000000000..801717d9f --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,3 @@ +# XmlViolationStore rewrites this file without a trailing newline on every test run; CSharpier's +# XML formatter requires one. Excluded so `csharpier check` doesn't flag the store's own output. +ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml diff --git a/ArchUnit.sln b/ArchUnit.sln index 43206a38a..5f7c40a35 100644 --- a/ArchUnit.sln +++ b/ArchUnit.sln @@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PropertyMemberAssembly", "T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassAssembly", "TestAssemblies\ClassAssembly\ClassAssembly.csproj", "{2C04EB93-5AE0-474C-B328-145A734A5E2B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlicesTestAssembly", "TestAssemblies\SlicesTestAssembly\SlicesTestAssembly.csproj", "{E421FA12-BBB9-4A8B-BD29-8757836079A5}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A780B3D6-C525-4270-ABF1-63BA65EA1593}" ProjectSection(SolutionItems) = preProject README.md = README.md @@ -427,6 +429,18 @@ Global {2C04EB93-5AE0-474C-B328-145A734A5E2B}.Release|x64.Build.0 = Release|Any CPU {2C04EB93-5AE0-474C-B328-145A734A5E2B}.Release|x86.ActiveCfg = Release|Any CPU {2C04EB93-5AE0-474C-B328-145A734A5E2B}.Release|x86.Build.0 = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|x64.Build.0 = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Debug|x86.Build.0 = Debug|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|Any CPU.Build.0 = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x64.ActiveCfg = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x64.Build.0 = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x86.ActiveCfg = Release|Any CPU + {E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -444,6 +458,7 @@ Global {05F40E4D-94EC-4DF7-B0FA-1BCBFDF56088} = {B1191F18-91CB-4387-B775-A5EB64D3AC30} {1D0187EB-9D04-4CF3-AE63-0C0E97FCB49C} = {B1191F18-91CB-4387-B775-A5EB64D3AC30} {2C04EB93-5AE0-474C-B328-145A734A5E2B} = {B1191F18-91CB-4387-B775-A5EB64D3AC30} + {E421FA12-BBB9-4A8B-BD29-8757836079A5} = {B1191F18-91CB-4387-B775-A5EB64D3AC30} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D1A8274D-32D6-44DB-9BB3-1A5B273709AF} diff --git a/ArchUnitNETTests/ArchUnitNET/Storage/CustomPathFrozenRules.json b/ArchUnitNETTests/ArchUnitNET/Storage/CustomPathFrozenRules.json index f42d52c8b..9216791d3 100644 --- a/ArchUnitNETTests/ArchUnitNET/Storage/CustomPathFrozenRules.json +++ b/ArchUnitNETTests/ArchUnitNET/Storage/CustomPathFrozenRules.json @@ -13,7 +13,7 @@ ] }, { - "ArchRuleDescription": "Slices matching \"TestAssembly.Slices.(**)\" should not depend on each other", + "ArchRuleDescription": "Slices matching \"SlicesTestAssembly.MultipleSubnamespaces.(**)\" should not depend on each other", "Violations": [ "Slice3", "Slice3.Group1", diff --git a/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.json b/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.json index 06bbca207..5e4531c65 100644 --- a/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.json +++ b/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.json @@ -19,7 +19,7 @@ ] }, { - "ArchRuleDescription": "Slices matching \"TestAssembly.Slices.(**)\" should not depend on each other", + "ArchRuleDescription": "Slices matching \"SlicesTestAssembly.MultipleSubnamespaces.(**)\" should not depend on each other", "Violations": [ "Slice3", "Slice3.Group1", @@ -30,7 +30,7 @@ ] }, { - "ArchRuleDescription": "Slices matching \"TestAssembly.Slices.(*)..\" should not depend on each other", + "ArchRuleDescription": "Slices matching \"SlicesTestAssembly.MultipleSubnamespaces.(*)..\" should not depend on each other", "Violations": [] } ] \ No newline at end of file diff --git a/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml b/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml index a174b04db..c0905ff78 100644 --- a/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml +++ b/ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml @@ -3,7 +3,7 @@ ArchUnitNETTests.Fluent.FreezeTests+Violation - + ArchUnitNETTests.Fluent.FreezeTests+Violation ArchUnitNETTests.Fluent.FreezeTests+Violation2 @@ -11,7 +11,7 @@ ArchUnitNETTests.Fluent.FreezeTests+Violation - + Slice3 Slice3.Group1 Slice2 @@ -19,4 +19,4 @@ Slice1 Slice1.Service - + \ No newline at end of file diff --git a/ArchUnitNETTests/ArchUnitNETTests.csproj b/ArchUnitNETTests/ArchUnitNETTests.csproj index 8fb6edaf1..c56d20a5a 100644 --- a/ArchUnitNETTests/ArchUnitNETTests.csproj +++ b/ArchUnitNETTests/ArchUnitNETTests.csproj @@ -35,6 +35,7 @@ + diff --git a/ArchUnitNETTests/AssemblyTestHelper/SlicesAssemblyTestHelper.cs b/ArchUnitNETTests/AssemblyTestHelper/SlicesAssemblyTestHelper.cs new file mode 100644 index 000000000..1ea4b2208 --- /dev/null +++ b/ArchUnitNETTests/AssemblyTestHelper/SlicesAssemblyTestHelper.cs @@ -0,0 +1,9 @@ +using ArchUnitNET.Domain; + +namespace ArchUnitNETTests.AssemblyTestHelper; + +public class SlicesAssemblyTestHelper : AssemblyTestHelper +{ + public sealed override Architecture Architecture => + StaticTestArchitectures.SlicesTestArchitecture; +} diff --git a/ArchUnitNETTests/Fluent/FreezeTests.cs b/ArchUnitNETTests/Fluent/FreezeTests.cs index 968856cb3..ff8e36c84 100644 --- a/ArchUnitNETTests/Fluent/FreezeTests.cs +++ b/ArchUnitNETTests/Fluent/FreezeTests.cs @@ -36,13 +36,13 @@ public class FreezeTests private readonly IArchRule _frozenSliceRule = SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") .Should() .NotDependOnEachOther(); private readonly IArchRule _failingFrozenSliceRule = SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(*)..") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") .Should() .NotDependOnEachOther(); @@ -51,8 +51,7 @@ public void PassFrozenRules() { Freeze(_frozenRule).Check(Architecture); Freeze(_frozenRule2).Check(Architecture); - Freeze(_frozenSliceRule) - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + Freeze(_frozenSliceRule).Check(StaticTestArchitectures.SlicesTestArchitecture); } [Fact] @@ -61,7 +60,7 @@ public void PassFrozenRulesUsingXmlViolationStore() Freeze(_frozenRule, new XmlViolationStore()).Check(Architecture); Freeze(_frozenRule2, new XmlViolationStore()).Check(Architecture); Freeze(_frozenSliceRule, new XmlViolationStore()) - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + .Check(StaticTestArchitectures.SlicesTestArchitecture); } [Fact] @@ -72,7 +71,7 @@ public void FailFrozenRule() ); Assert.Throws(() => Freeze(_failingFrozenSliceRule) - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Check(StaticTestArchitectures.SlicesTestArchitecture) ); } @@ -84,7 +83,7 @@ public void FailFrozenRuleUsingXmlViolationStore() ); Assert.Throws(() => Freeze(_failingFrozenSliceRule, new XmlViolationStore()) - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Check(StaticTestArchitectures.SlicesTestArchitecture) ); } @@ -96,7 +95,7 @@ public void PassFrozenRulesWithCustomViolationStorePath() Freeze(_frozenRule2, "../../../ArchUnitNET/Storage/CustomPathFrozenRules.json") .Check(Architecture); Freeze(_frozenSliceRule, "../../../ArchUnitNET/Storage/CustomPathFrozenRules.json") - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + .Check(StaticTestArchitectures.SlicesTestArchitecture); } private class Violation { } diff --git a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs index 8fa7c0dc2..507016d81 100644 --- a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs +++ b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs @@ -13,26 +13,26 @@ public void CycleDetectionTest() Assert.Throws(() => SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") .Should() .BeFreeOfCycles() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Check(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.False( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") .Should() .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.True( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)..") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") .Should() .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); } @@ -40,50 +40,50 @@ public void CycleDetectionTest() public void MatchingTest() { Assert.Equal( - 7, + 9, SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(*)") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Count() ); Assert.Equal( - 7, + 9, SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Count() ); Assert.Equal( - 7, + 9, SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(*)..") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Count() ); Assert.Equal( 3, SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)..") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Count() ); Assert.Equal( - 2, + 4, SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice3.(*)") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice3.(*)") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Count() ); Assert.False( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Service.(*)") - .GetObjects(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Service.(*)") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .Any() ); } @@ -93,55 +93,55 @@ public void NotDependOnEachOtherTest() { SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice3.(*)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice3.(*)") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + .Check(StaticTestArchitectures.SlicesTestArchitecture); SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice1.(*)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice1.(*)") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + .Check(StaticTestArchitectures.SlicesTestArchitecture); Assert.True( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice1.(*)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice1.(*)") .Should() .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.Throws(() => SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .Check(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.False( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") .Should() .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.False( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(*)..") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") .Should() .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); Assert.False( SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.(**)..") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") .Should() .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) ); } } diff --git a/ArchUnitNETTests/StaticTestArchitectures.cs b/ArchUnitNETTests/StaticTestArchitectures.cs index 39246cccc..b9dcef78b 100644 --- a/ArchUnitNETTests/StaticTestArchitectures.cs +++ b/ArchUnitNETTests/StaticTestArchitectures.cs @@ -96,6 +96,12 @@ public static class StaticTestArchitectures .LoadAssemblies(typeof(ClassNamespace.RegularClass).Assembly) .Build(); + public static readonly Architecture SlicesTestArchitecture = new ArchLoader() + .LoadAssemblies( + typeof(SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class).Assembly + ) + .Build(); + public static readonly Architecture ArchUnitNETTestAssemblyArchitecture = new ArchLoader() .LoadAssemblies(typeof(Class1).Assembly) .Build(); diff --git a/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice1/Slice1Class.cs b/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice1/Slice1Class.cs new file mode 100644 index 000000000..7088f2e7a --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice1/Slice1Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.DirectCircle.Slice2; + +namespace SlicesTestAssembly.DirectCircle.Slice1; + +public class Slice1Class +{ + public Slice2Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice2/Slice2Class.cs b/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice2/Slice2Class.cs new file mode 100644 index 000000000..c35904236 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DirectCircle/Slice2/Slice2Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.DirectCircle.Slice1; + +namespace SlicesTestAssembly.DirectCircle.Slice2; + +public class Slice2Class +{ + public Slice1Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Alpha/Service/AlphaServiceSegmentClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Alpha/Service/AlphaServiceSegmentClass.cs new file mode 100644 index 000000000..1144250df --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Alpha/Service/AlphaServiceSegmentClass.cs @@ -0,0 +1,4 @@ +namespace SlicesTestAssembly.DotDotSemantics.Alpha.Service; + +// "Service" as a segment of its own: the case "DotDotSemantics.(*)..Service" is meant to match. +public class AlphaServiceSegmentClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs new file mode 100644 index 000000000..cc43345ab --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs @@ -0,0 +1,6 @@ +namespace SlicesTestAssembly.DotDotSemantics.AlphaService; + +// A single segment that ends with the literal "Service": a pattern such as +// "DotDotSemantics.(*)..Service" must not match it by splitting the segment into +// "Alpha" + "Service". +public class AlphaServiceClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Inner/InnerClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Inner/InnerClass.cs new file mode 100644 index 000000000..d9e18519b --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Inner/InnerClass.cs @@ -0,0 +1,4 @@ +namespace SlicesTestAssembly.DotDotSemantics.Outer.Inner; + +// Two segments below DotDotSemantics, with nothing skipped in between. +public class InnerClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Mid/Inner/MidInnerClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Mid/Inner/MidInnerClass.cs new file mode 100644 index 000000000..cc23a0591 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Outer/Mid/Inner/MidInnerClass.cs @@ -0,0 +1,4 @@ +namespace SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner; + +// Three segments below DotDotSemantics: "Mid" is the segment a ".." is expected to skip. +public class MidInnerClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Single/SingleClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Single/SingleClass.cs new file mode 100644 index 000000000..c59cbd8e4 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/Single/SingleClass.cs @@ -0,0 +1,5 @@ +namespace SlicesTestAssembly.DotDotSemantics.Single; + +// One segment below DotDotSemantics: a pattern capturing two segments must not match this +// namespace by splitting "Single" into two parts. +public class SingleClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs new file mode 100644 index 000000000..cc0b61161 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.DuplicatePrefix.Sub; + +public class OuterClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs new file mode 100644 index 000000000..c6c14d7e2 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.DuplicatePrefix.Sub.Sub; + +public class InnerClass { } diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Service/Service1Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Service/Service1Class.cs new file mode 100644 index 000000000..31076cef8 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Service/Service1Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service; + +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service; + +public class Service1Class +{ + public Service2Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Slice1Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Slice1Class.cs new file mode 100644 index 000000000..72880102a --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice1/Slice1Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.MultipleSubnamespaces.Slice2; + +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1; + +public class Slice1Class +{ + public Slice2Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Service/Service2Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Service/Service2Class.cs new file mode 100644 index 000000000..1e6d1f501 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Service/Service2Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.MultipleSubnamespaces.Slice3; + +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service; + +public class Service2Class +{ + public Slice3Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Slice2Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Slice2Class.cs new file mode 100644 index 000000000..3dd170cb8 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice2/Slice2Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.MultipleSubnamespaces.Slice3; + +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2; + +public class Slice2Class +{ + public Slice3Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Group1Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Group1Class.cs new file mode 100644 index 000000000..2a5730b74 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Group1Class.cs @@ -0,0 +1,6 @@ +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1; + +public class Group1Class +{ + public Slice3Class Dependency = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Inner/Inner1Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Inner/Inner1Class.cs new file mode 100644 index 000000000..d49ea72dd --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group1/Inner/Inner1Class.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner; + +public class Inner1Class { } diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Group2Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Group2Class.cs new file mode 100644 index 000000000..b93159d41 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Group2Class.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2; + +public class Group2Class { } diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Inner/Inner2Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Inner/Inner2Class.cs new file mode 100644 index 000000000..80bfb05a2 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Group2/Inner/Inner2Class.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner; + +public class Inner2Class { } diff --git a/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Slice3Class.cs b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Slice3Class.cs new file mode 100644 index 000000000..04de2226e --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MultipleSubnamespaces/Slice3/Slice3Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.MultipleSubnamespaces.Slice1; + +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3; + +public class Slice3Class +{ + public Slice1Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/SlicesTestAssembly.csproj b/TestAssemblies/SlicesTestAssembly/SlicesTestAssembly.csproj new file mode 100644 index 000000000..51fae4aaa --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/SlicesTestAssembly.csproj @@ -0,0 +1,12 @@ + + + net10.0 + enable + enable + true + false + True + ..\..\strongNameKey.snk + 1.0.0.0 + + diff --git a/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice1/Slice1Class.cs b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice1/Slice1Class.cs new file mode 100644 index 000000000..dc9aeb136 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice1/Slice1Class.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.SubnamespaceCircle.Slice2; + +namespace SlicesTestAssembly.SubnamespaceCircle.Slice1; + +public class Slice1Class +{ + public Slice2Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Inner/Slice2InnerClass.cs b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Inner/Slice2InnerClass.cs new file mode 100644 index 000000000..f3db26f37 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Inner/Slice2InnerClass.cs @@ -0,0 +1,6 @@ +namespace SlicesTestAssembly.SubnamespaceCircle.Slice2.Inner; + +public class Slice2InnerClass +{ + public Slice1.Slice1Class Reference = null!; +} diff --git a/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Slice2Class.cs b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Slice2Class.cs new file mode 100644 index 000000000..83071b022 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/SubnamespaceCircle/Slice2/Slice2Class.cs @@ -0,0 +1,3 @@ +namespace SlicesTestAssembly.SubnamespaceCircle.Slice2; + +public class Slice2Class { } diff --git a/TestAssembly/Slices/Slice1/Service/Service1Class.cs b/TestAssembly/Slices/Slice1/Service/Service1Class.cs deleted file mode 100644 index 553582c9b..000000000 --- a/TestAssembly/Slices/Slice1/Service/Service1Class.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TestAssembly.Slices.Slice2.Service; - -namespace TestAssembly.Slices.Slice1.Service -{ - public class Service1Class - { - public Service2Class Reference; - } -} diff --git a/TestAssembly/Slices/Slice1/Slice1Class.cs b/TestAssembly/Slices/Slice1/Slice1Class.cs deleted file mode 100644 index 901d6140e..000000000 --- a/TestAssembly/Slices/Slice1/Slice1Class.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TestAssembly.Slices.Slice2; - -namespace TestAssembly.Slices.Slice1 -{ - public class Slice1Class - { - public Slice2Class Reference; - } -} diff --git a/TestAssembly/Slices/Slice2/Service/Service2Class.cs b/TestAssembly/Slices/Slice2/Service/Service2Class.cs deleted file mode 100644 index 7f088c16c..000000000 --- a/TestAssembly/Slices/Slice2/Service/Service2Class.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TestAssembly.Slices.Slice3; - -namespace TestAssembly.Slices.Slice2.Service -{ - public class Service2Class - { - public Slice3Class Reference; - } -} diff --git a/TestAssembly/Slices/Slice2/Slice2Class.cs b/TestAssembly/Slices/Slice2/Slice2Class.cs deleted file mode 100644 index aac88867b..000000000 --- a/TestAssembly/Slices/Slice2/Slice2Class.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TestAssembly.Slices.Slice3; - -namespace TestAssembly.Slices.Slice2 -{ - public class Slice2Class - { - public Slice3Class Reference; - } -} diff --git a/TestAssembly/Slices/Slice3/Group1/Group1Class.cs b/TestAssembly/Slices/Slice3/Group1/Group1Class.cs deleted file mode 100644 index f6dc0664d..000000000 --- a/TestAssembly/Slices/Slice3/Group1/Group1Class.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TestAssembly.Slices.Slice3.Group1 -{ - public class Group1Class - { - public Slice3Class Dependency; - } -} diff --git a/TestAssembly/Slices/Slice3/Group2/Group2Class.cs b/TestAssembly/Slices/Slice3/Group2/Group2Class.cs deleted file mode 100644 index cffad2c95..000000000 --- a/TestAssembly/Slices/Slice3/Group2/Group2Class.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace TestAssembly.Slices.Slice3.Group2 -{ - public class Group2Class { } -} diff --git a/TestAssembly/Slices/Slice3/Slice3Class.cs b/TestAssembly/Slices/Slice3/Slice3Class.cs deleted file mode 100644 index fd1b7f971..000000000 --- a/TestAssembly/Slices/Slice3/Slice3Class.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TestAssembly.Slices.Slice1; - -namespace TestAssembly.Slices.Slice3 -{ - public class Slice3Class - { - public Slice1Class Reference; - } -} From 29417a84ed3b8c3b4d3450c491d690e07ef0dc17 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 28 Aug 2026 09:56:52 +0200 Subject: [PATCH 2/4] test: pin current slice rule and PlantUML diagram behaviour The slice pattern matcher is about to be reimplemented. Before touching it, capture what it does today so that each following commit shows its own effect as a test diff rather than leaving the reader to infer it. No production code changes; the expectations below are the current behaviour, warts included: - Everything after the first "(*" is discarded, so "(*)", "(*)..", "(*).(*)" and "(*)..(*)" all produce the same nine slices, a trailing literal such as "(**).Service.." matches nothing, and alternation is not supported at all. - A leading ".." is likewise ignored: the prefix is matched with Contains. - ".." may match within a namespace segment, so "(*)..Service" matches the single segment "AlphaService" as well as "Alpha.Service". - Patterns are validated per type while the slices are enumerated, so an invalid pattern raises nothing until the result is consumed. Two patterns reach the "not clearly assignable" guard: prefix and postfix are matched independently against the raw namespace string, so crafting them to overlap makes the postfix check pass against the full namespace while the slice string left after stripping the prefix no longer contains it. - Matching (unlike MatchingWithPackages) leaves a slice without a namespace prefix, which PlantUmlSlice.BuildStringC4Style dereferences unconditionally -- pinned as the NullReferenceException it currently throws. PlantUmlSliceDiagramTests covers every combination of pattern and rendering mode: plain and with packages, single and double asterisk, multiple and non-contiguous capture groups, LimitDependencies, C4Style and focus-on, plus the paths a coverage pass showed were only reached incidentally rather than by a dedicated test: FocusOn's argument validation, focus-on over namespace-less slices -- the only path on which the exporter colours a node rather than wrapping it in a package -- a cyclic dependency folded into a single Circle arrow, the compact OneToOneCompact rendering, IncludeNodesWithoutDependencies = false, and a DependencyFilter actually excluding a dependency. SlicesTests gains the same MatchingWithPackages coverage for BeFreeOfCycles and NotDependOnEachOther that Matching already had. Signed-off-by: Alexander Linne --- .../AssemblyTestHelper/AssemblyTestHelper.cs | 43 +- .../PlantUml/PlantUmlSliceDiagramTests.cs | 359 +++++++++++++++ ...Tests.BuildUmlBySlicesFocusOn.verified.txt | 31 ++ ..._Matching_ColorsFocusedSlices.verified.txt | 15 + ...SkipsSlicesWithoutFocusString.verified.txt | 24 + ...sMatchingWithPackages_C4Style.verified.txt | 38 ++ ...hingWithPackages_DirectCircle.verified.txt | 16 + ...ngWithPackages_DoubleAsterisk.verified.txt | 38 ++ ...ithPackages_LimitDependencies.verified.txt | 38 ++ ...ackages_MultipleCaptureGroups.verified.txt | 34 ++ ...es_NonContiguousCaptureGroups.verified.txt | 34 ++ ...ngWithPackages_SingleAsterisk.verified.txt | 22 + ...ircle_RendersCircleDependency.verified.txt | 10 + ...ildUmlBySlices_DoubleAsterisk.verified.txt | 22 + ...cludeNodesWithoutDependencies.verified.txt | 19 + ...ces_LimitDependencies_Compact.verified.txt | 20 + ...ySlices_MultipleCaptureGroups.verified.txt | 20 + ...es_NonContiguousCaptureGroups.verified.txt | 20 + ...ildUmlBySlices_SingleAsterisk.verified.txt | 13 + ...BySlices_WithDependencyFilter.verified.txt | 21 + .../Fluent/Slices/PatternValidationTests.cs | 103 +++++ ArchUnitNETTests/Fluent/Slices/SlicesTests.cs | 433 +++++++++++++++--- ...bnamespaces_ReportsEveryCycle.verified.txt | 23 + ..._ReportsAllSlicesFreeOfCycles.verified.txt | 6 + ...eturnsDescriptiveCycleMessage.verified.txt | 19 + ...ithPackages_ReportsViolations.verified.txt | 19 + ...spaces_ReportsEveryDependency.verified.txt | 66 +++ ...sDescriptiveDependencyMessage.verified.txt | 19 + .../AlphaService/AlphaServiceClass.cs | 7 +- .../Sub/DuplicatePrefix/Sub/LeafClass.cs | 5 + .../DuplicatePrefix/Sub/OuterClass.cs | 3 - .../DuplicatePrefix/Sub/Sub/InnerClass.cs | 3 - 32 files changed, 1472 insertions(+), 71 deletions(-) create mode 100644 ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DirectCircle_RendersCircleDependency.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DoubleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_ExcludeNodesWithoutDependencies.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_LimitDependencies_Compact.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_MultipleCaptureGroups.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_NonContiguousCaptureGroups.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_SingleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_WithDependencyFilter.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/PatternValidationTests.cs create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_MultipleSubnamespaces_ReportsEveryCycle.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WhenNoCycles_ReportsAllSlicesFreeOfCycles.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MultipleSubnamespaces_ReportsEveryDependency.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_ReturnsDescriptiveDependencyMessage.verified.txt create mode 100644 TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/DuplicatePrefix/Sub/LeafClass.cs delete mode 100644 TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs delete mode 100644 TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs diff --git a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs index 639b51be4..f763a86ea 100644 --- a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs +++ b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs @@ -15,11 +15,26 @@ namespace ArchUnitNETTests.AssemblyTestHelper; public abstract class AssemblyTestHelper { private readonly StringBuilder _snapshot = new StringBuilder(); + private bool _recordSnapshot = true; public readonly string NonExistentObjectName = "NotTheNameOfAnyObject"; public abstract Architecture Architecture { get; } + public AssemblyTestHelper WithoutSnapshot() + { + _recordSnapshot = false; + return this; + } + + private void Record(string output) + { + if (_recordSnapshot) + { + _snapshot.Append(output); + } + } + public void AddSnapshotHeader(string header) { _snapshot.AppendLine("===== " + header + " =====\n"); @@ -62,15 +77,26 @@ private static bool HasNoRealResults(IEnumerable results) return results.Any(result => result.EvaluatedObject is ICanBeEvaluated); } + /// + /// Asserts the rule's verdict -- -- rather than the + /// shape of its results. The distinction matters for + /// : its verdict applies the conjunction, while its + /// merely concatenates both operands' results and + /// never applies it. A passing Or whose first operand fails therefore has failing + /// results, which is why this assertion cannot be expressed as + /// results.All(r => r.Passed), and why and + /// -- which do read the results -- say nothing about a + /// combinator. + /// public void AssertNoViolations(IArchRule rule) { var results = rule.Evaluate(Architecture).ToList(); var output = FormatSnapshot(rule, results); - if (!results.All(result => result.Passed)) + if (!rule.HasNoViolations(Architecture)) { Assert.Fail(output); } - _snapshot.Append(output); + Record(output); } public void AssertAnyViolations(IArchRule rule) @@ -85,7 +111,7 @@ public void AssertAnyViolations(IArchRule rule) { Assert.Fail(output); } - _snapshot.Append(output); + Record(output); } public void AssertOnlyViolations(IArchRule rule) @@ -109,7 +135,7 @@ public void AssertOnlyViolations(IArchRule rule, bool allowNoResults) + output ); } - _snapshot.Append(output); + Record(output); } public void AssertException(IArchRule rule) @@ -123,6 +149,15 @@ public void AssertException(IArchRule rule) public Task AssertSnapshotMatches([CallerFilePath] string sourceFile = "") { + if (!_recordSnapshot) + { + Assert.Fail( + "This helper was created with WithoutSnapshot(), so nothing was recorded and the " + + "snapshot would verify as empty. Drop the WithoutSnapshot() call or the " + + "AssertSnapshotMatches() call." + ); + } + return Verifier .Verify(_snapshot.ToString(), null, sourceFile) .DisableDiff() // Don't open diff tool during the test diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs new file mode 100644 index 000000000..0737b966a --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs @@ -0,0 +1,359 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using ArchUnitNET.Domain; +using ArchUnitNET.Domain.PlantUml.Export; +using ArchUnitNET.Fluent.Slices; +using VerifyXunit; +using Xunit; + +namespace ArchUnitNETTests.Domain.PlantUml +{ + /// + /// Pins the PlantUML output for every combination of slice pattern and rendering mode. + /// These are characterization tests: a snapshot records what the exporter currently + /// produces, not necessarily what it ideally should, so a diff here is a prompt to look + /// rather than proof of a regression. + /// + public class PlantUmlSliceDiagramTests + { + private static readonly ArchUnitNET.Domain.Architecture Architecture = + StaticTestArchitectures.SlicesTestArchitecture; + + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + + // --- The asterisk count decides which slices the exporter renders --------------- + // + // Every pattern below matches the same nine slices -- SlicesTests.MatchingTest and + // Matching_SingleAsterisk_CapturesEveryDepth assert that. What differs is how many + // of them survive PlantUmlFileBuilder.RemovePatternInappropriateSlices, which drops + // a slice once its depth (dots in Description, less dots in NameSpace) reaches + // Slice.CountOfAsteriskInPattern: + // + // (*) count 1 -> 3 of 9 slices rendered + // (*).(*) count 2 -> 7 of 9 slices rendered + // (**) count null -> 9 of 9 slices rendered + // + // CountOfAsteriskInPattern has no other consumer in the library: the slice domain + // carries it solely so the exporter can perform that deletion. So these snapshots + // are the only record that asking for "(*)" hands the diagram two-thirds fewer + // nodes than GetObjects returned, and a reimplementation is free to drop the field + // from SliceIdentifier only if it also changes what these files say. + + [Fact] + public Task BuildUmlBySlices_SingleAsterisk() + { + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)")); + } + + [Fact] + public Task BuildUmlBySlices_DoubleAsterisk() + { + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + } + + // The matcher discards everything after the first "(*", so "(*).(*)" and "(*)..(*)" + // are indistinguishable to it and the next two snapshots are byte-identical -- as are + // their MatchingWithPackages counterparts below. That identity is the point: it is + // what a reimplementation giving the second capture group any meaning would have to + // change. SlicesTests.DotDot_* cover the shapes that would tell the two patterns + // apart. + // + // Identical snapshots do not make these tests redundant with "(*)", though. The + // second group is dead to the matcher but still raises CountOfAsteriskInPattern to + // 2, which moves the exporter's cutoff -- hence seven nodes here against three for + // "(*)". + [Fact] + public Task BuildUmlBySlices_MultipleCaptureGroups() + { + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*).(*)")); + } + + [Fact] + public Task BuildUmlBySlices_NonContiguousCaptureGroups() + { + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)..(*)")); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_SingleAsterisk() + { + return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)")); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk() + { + return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups() + { + return VerifySlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*).(*)") + ); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups() + { + return VerifySlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)..(*)") + ); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_LimitDependencies() + { + return VerifySlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), + new GenerationOptions { LimitDependencies = true } + ); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_C4Style() + { + return VerifySlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), + new GenerationOptions { C4Style = true } + ); + } + + [Fact] + public void BuildUmlBySlices_C4Style_Throws() + { + // Slices produced by Matching (as opposed to MatchingWithPackages) have no namespace + // prefix, and BuildStringC4Style's no-namespace branch appends its Container line but + // then falls through into Namespace.Remove instead of returning, so C4 style cannot + // render a namespace-less slice at all. + Assert.Throws(() => + new PlantUmlFileBuilder() + .WithDependenciesFrom( + SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")), + new GenerationOptions { C4Style = true } + ) + .AsString() + ); + } + + [Fact] + public Task BuildUmlBySlicesFocusOn() + { + var slices = SortedSlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)") + ); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, Root + "Slice1") + .AsString(); + return VerifyUml(uml); + } + + [Fact] + public Task BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices() + { + // Slices produced by Matching have no namespace, so the focused ones take + // PlantUmlSlice.BuildString's namespace-less branch with a colour set -- the only + // path on which the exporter renders "[Slice1] #99ffd1" rather than a package. + // Their Description carries no Root prefix either, hence the bare focus string. + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, "Slice1") + .AsString(); + return VerifyUml(uml); + } + + // --- Cyclic slice dependencies are folded into a single Circle dependency ---- + + [Fact] + public Task BuildUmlBySlices_DirectCircle_RendersCircleDependency() + { + var slices = SortedSlices( + SliceRuleDefinition.Slices().Matching("SlicesTestAssembly.DirectCircle.(*)") + ); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + return VerifyUml(uml); + } + + [Fact] + public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle() + { + var slices = SortedSlices( + SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.DirectCircle.(*)") + ); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + return VerifyUml(uml); + } + + // --- LimitDependencies without namespaces takes the OneToOneCompact branch ---- + + [Fact] + public Task BuildUmlBySlices_LimitDependencies_Compact() + { + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFrom(slices, new GenerationOptions { LimitDependencies = true }) + .AsString(); + return VerifyUml(uml); + } + + // --- IncludeNodesWithoutDependencies = false removes dependency-less slices ---- + + [Fact] + public Task BuildUmlBySlices_ExcludeNodesWithoutDependencies() + { + // A single-asterisk pattern would already collapse away the dependency-less slices + // via RemovePatternInappropriateSlices, so use "(**)" to keep them in the slice list + // and let IncludeNodesWithoutDependencies do the removal instead. + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFrom( + slices, + new GenerationOptions { IncludeNodesWithoutDependencies = false } + ) + .AsString(); + return VerifyUml(uml); + } + + // --- A DependencyFilter is honoured when building slice diagrams -------------- + + [Fact] + public Task BuildUmlBySlices_WithDependencyFilter() + { + // "(*)" would already collapse Slice3.Group1 away via RemovePatternInappropriateSlices, + // leaving no dependency for the filter to remove, so use "(**)" instead. + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFrom( + slices, + new GenerationOptions + { + DependencyFilter = dep => !dep.Origin.FullName.Contains("Slice3.Group1"), + } + ) + .AsString(); + return VerifyUml(uml); + } + + // --- WithDependenciesFromFocusOn argument validation --------------------------- + + [Fact] + public void BuildUmlBySlicesFocusOn_EmptyPackage_Throws() + { + var slices = SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .GetObjects(Architecture); + var ex = Assert.Throws(() => + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "") + ); + Assert.Equal("Package can't be empty", ex.Message); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_DotOnlyPackage_Throws() + { + var slices = SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .GetObjects(Architecture); + var ex = Assert.Throws(() => + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".") + ); + Assert.Equal("Package can't contain a single dot only", ex.Message); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_UnknownPackage_Throws() + { + // A single-asterisk pattern gives every slice a non-null CountOfAsteriskInPattern, + // which is required to reach RemovePatternInappropriateSlices' early return for a + // focus string that no slice contains, on the way to this exception. + var slices = SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(*)") + .GetObjects(Architecture); + var ex = Assert.Throws(() => + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage") + ); + Assert.Contains("is not contained in this slice", ex.Message); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_TrailingDotPackage_IsTrimmed() + { + var slicesWithTrailingDot = SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .GetObjects(Architecture); + var umlWithTrailingDot = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.") + .AsString(); + + var slicesWithoutTrailingDot = SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .GetObjects(Architecture); + var umlWithoutTrailingDot = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1") + .AsString(); + + Assert.Equal(umlWithoutTrailingDot, umlWithTrailingDot); + } + + // --- RemovePatternInappropriateSlices' per-slice "continue" for FocusOn ------ + + [Fact] + public Task BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString() + { + // Focusing on the single-segment slice "Slice3" means slices whose Description + // doesn't contain it (e.g. "Slice1") skip the pattern-appropriateness check via + // RemovePatternInappropriateSlices' per-slice "continue", while "Slice3" itself is + // shallow enough to survive that check and keep the package reachable. + var slices = SortedSlices( + SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)") + ); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, Root + "Slice3") + .AsString(); + return VerifyUml(uml); + } + + /// + /// GetObjects returns slices in Mono.Cecil's type-table order, which is deterministic + /// per build but not contractually stable across Cecil or runtime upgrades. Sorting by + /// description keeps the node and dependency order in the snapshots below stable + /// regardless of that traversal order. + /// + private static Slice[] SortedSlices(GivenSlices slices) + { + return slices + .GetObjects(Architecture) + .OrderBy(slice => slice.Description, StringComparer.Ordinal) + .ToArray(); + } + + private static Task VerifyUml(string uml) + { + return Verifier + .Verify(uml) + .DisableDiff() // Don't open diff tool during the test + .UseDirectory("Snapshots"); + } + + private static Task VerifySlices( + GivenSlices slices, + GenerationOptions generationOptions = null + ) + { + return VerifyUml( + new PlantUmlFileBuilder() + .WithDependenciesFrom(SortedSlices(slices), generationOptions) + .AsString() + ); + } + } +} diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt new file mode 100644 index 000000000..bf61e1915 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt @@ -0,0 +1,31 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service #99ffd1 +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 #99ffd1 { +} +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +Slice3 -[#green]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices.verified.txt new file mode 100644 index 000000000..b96ed6505 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices.verified.txt @@ -0,0 +1,15 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] #99ffd1 +[Slice1.Service] #99ffd1 +[Slice2] +[Slice2.Service] +[Slice3] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice3] --|> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt new file mode 100644 index 000000000..01d54e434 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt @@ -0,0 +1,24 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +[Slice3] as SlicesTestAssembly.MultipleSubnamespaces.Slice3 #99ffd1 +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice3] -[#red]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt new file mode 100644 index 000000000..cec8d4002 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt @@ -0,0 +1,38 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { +Boundary(Slice1, Slice1) { +Container(SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service, Service) +} +} + +Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { +Boundary(Slice2, Slice2) { +Container(SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service, Service) +} +} + +Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { +Boundary(Slice3, Slice3) { +Boundary(Group1, Group1) { +Container(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner, Inner) +} +} +} + +Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { +Boundary(Slice3, Slice3) { +Boundary(Group2, Group2) { +Container(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner, Inner) +} +} +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> Slice3 +Slice3 -[#green]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt new file mode 100644 index 000000000..d1981eff7 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt @@ -0,0 +1,16 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.DirectCircle { +[Slice1] as SlicesTestAssembly.DirectCircle.Slice1 +} + +package SlicesTestAssembly.DirectCircle { +[Slice2] as SlicesTestAssembly.DirectCircle.Slice2 +} + +[SlicesTestAssembly.DirectCircle.Slice1] <-[#red]> [SlicesTestAssembly.DirectCircle.Slice2] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt new file mode 100644 index 000000000..2e17772dc --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt @@ -0,0 +1,38 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +package Group1 { +[Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner +} +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +package Group2 { +[Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner +} +} +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> Slice3 +Slice3 -[#green]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt new file mode 100644 index 000000000..44b61fc01 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt @@ -0,0 +1,38 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +package Group1 { +[Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner +} +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +package Group2 { +[Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner +} +} +} + +Slice1 ..> Slice2 +Slice2 ..> Slice3 +Slice3 ..> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt new file mode 100644 index 000000000..9416d0726 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt @@ -0,0 +1,34 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +[Group1] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +[Group2] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 +} +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> Slice3 +Slice3 -[#green]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt new file mode 100644 index 000000000..9416d0726 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt @@ -0,0 +1,34 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice1 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice2 { +[Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +[Group1] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 +} +} + +package SlicesTestAssembly.MultipleSubnamespaces { +package Slice3 { +[Group2] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 +} +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> Slice3 +Slice3 -[#green]> Slice1 +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt new file mode 100644 index 000000000..6e0cbf47d --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt @@ -0,0 +1,22 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package SlicesTestAssembly.MultipleSubnamespaces { +[Slice1] as SlicesTestAssembly.MultipleSubnamespaces.Slice1 +} + +package SlicesTestAssembly.MultipleSubnamespaces { +[Slice2] as SlicesTestAssembly.MultipleSubnamespaces.Slice2 +} + +package SlicesTestAssembly.MultipleSubnamespaces { +[Slice3] as SlicesTestAssembly.MultipleSubnamespaces.Slice3 +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice3] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice1] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DirectCircle_RendersCircleDependency.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DirectCircle_RendersCircleDependency.verified.txt new file mode 100644 index 000000000..ffc596323 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DirectCircle_RendersCircleDependency.verified.txt @@ -0,0 +1,10 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice2] +[Slice1] <-[#red]> [Slice2] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DoubleAsterisk.verified.txt new file mode 100644 index 000000000..b33b9dcab --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_DoubleAsterisk.verified.txt @@ -0,0 +1,22 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice3.Group1.Inner] +[Slice3.Group2] +[Slice3.Group2.Inner] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +[Slice3.Group1] --|> [Slice3] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_ExcludeNodesWithoutDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_ExcludeNodesWithoutDependencies.verified.txt new file mode 100644 index 000000000..c5667533f --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_ExcludeNodesWithoutDependencies.verified.txt @@ -0,0 +1,19 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +[Slice3.Group1] --|> [Slice3] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_LimitDependencies_Compact.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_LimitDependencies_Compact.verified.txt new file mode 100644 index 000000000..991ec5378 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_LimitDependencies_Compact.verified.txt @@ -0,0 +1,20 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice3.Group1.Inner] +[Slice3.Group2] +[Slice3.Group2.Inner] +[Slice1] --> [Slice2] +[Slice1.Service] --> [Slice2.Service] +[Slice2] --> [Slice3] +[Slice3] --> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_MultipleCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_MultipleCaptureGroups.verified.txt new file mode 100644 index 000000000..041ebd608 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_MultipleCaptureGroups.verified.txt @@ -0,0 +1,20 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice3.Group2] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +[Slice3.Group1] --|> [Slice3] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_NonContiguousCaptureGroups.verified.txt new file mode 100644 index 000000000..041ebd608 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_NonContiguousCaptureGroups.verified.txt @@ -0,0 +1,20 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice3.Group2] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +[Slice3.Group1] --|> [Slice3] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_SingleAsterisk.verified.txt new file mode 100644 index 000000000..c37caedd7 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_SingleAsterisk.verified.txt @@ -0,0 +1,13 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice2] +[Slice3] +[Slice1] --|> [Slice2] +[Slice2] --|> [Slice3] +[Slice3] --|> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_WithDependencyFilter.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_WithDependencyFilter.verified.txt new file mode 100644 index 000000000..969b504e8 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_WithDependencyFilter.verified.txt @@ -0,0 +1,21 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +[Slice1] +[Slice1.Service] +[Slice2] +[Slice2.Service] +[Slice3] +[Slice3.Group1] +[Slice3.Group1.Inner] +[Slice3.Group2] +[Slice3.Group2.Inner] +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Fluent/Slices/PatternValidationTests.cs b/ArchUnitNETTests/Fluent/Slices/PatternValidationTests.cs new file mode 100644 index 000000000..bcc81a835 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/PatternValidationTests.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using ArchUnitNET.Fluent.Slices; +using Xunit; + +namespace ArchUnitNETTests.Fluent.Slices +{ + /// + /// Pins which slice patterns are rejected and with what message. The patterns are + /// validated while the slices are enumerated rather than when the rule is defined, so + /// every case here has to consume the result before the exception surfaces. + /// + public class PatternValidationTests + { + [Theory] + [InlineData("Foo.Bar")] + [InlineData("Foo.*")] + [InlineData("Foo..Bar")] + public void PatternWithoutCaptureGroupThrows(string pattern) + { + var ex = Assert.Throws(() => + SliceRuleDefinition.Slices().Matching(pattern).GetObjects(Architecture).ToList() + ); + Assert.Contains( + "have to contain (*) or (**)", + ex.Message, + StringComparison.OrdinalIgnoreCase + ); + } + + [Fact] + public void PatternMixingSingleAndDoubleAsteriskThrows() + { + var ex = Assert.Throws(() => + SliceRuleDefinition + .Slices() + .Matching("Foo.(*).(**)") + .GetObjects(Architecture) + .ToList() + ); + Assert.Contains( + "can't contain both (*) and (**)", + ex.Message, + StringComparison.OrdinalIgnoreCase + ); + } + + [Fact] + public void PatternWithRepeatedDoubleAsteriskThrows() + { + var ex = Assert.Throws(() => + SliceRuleDefinition + .Slices() + .Matching("Foo.(**).(**)") + .GetObjects(Architecture) + .ToList() + ); + Assert.Contains( + "can contain (**) only once", + ex.Message, + StringComparison.OrdinalIgnoreCase + ); + } + + // Prefix and postfix are matched independently against the full namespace + // string, without regard for segment boundaries. Crafting them so their required + // regions overlap makes the postfix check pass against the full namespace while the + // slice string computed after stripping the prefix no longer contains it -- this is + // the one real way to reach AssignFunc's "not clearly assignable" guard. + [Fact] + public void PatternWithOverlappingPrefixAndPostfix_NotClearlyAssignableThrows() + { + const string prefix = "SlicesTestAssembly.DotDotSeman"; + const string postfix = "Assembly.DotDotSemantics.Single"; + var ex = Assert.Throws(() => + SliceRuleDefinition + .Slices() + .Matching(prefix + "(**)" + postfix) + .GetObjects(Architecture) + .ToList() + ); + Assert.Contains( + "is not clearly assignable", + ex.Message, + StringComparison.OrdinalIgnoreCase + ); + } + + [Theory] + [InlineData("Foo.(*).Bar")] + [InlineData("Foo.(**).Bar")] + [InlineData("Foo.(**)..")] + [InlineData("Foo.(*)..")] + [InlineData("Foo.(*).(*)")] + public void ValidPatternDoesNotThrow(string pattern) + { + SliceRuleDefinition.Slices().Matching(pattern).GetObjects(Architecture).ToList(); + } + + private static ArchUnitNET.Domain.Architecture Architecture => + StaticTestArchitectures.SlicesTestArchitecture; + } +} diff --git a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs index 507016d81..143d03af5 100644 --- a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs +++ b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs @@ -1,6 +1,8 @@ -using System.Linq; +using System; +using System.Linq; +using System.Threading.Tasks; using ArchUnitNET.Fluent.Slices; -using ArchUnitNET.xUnit; +using ArchUnitNETTests.AssemblyTestHelper; using Xunit; namespace ArchUnitNETTests.Fluent.Slices @@ -10,30 +12,61 @@ public class SlicesTests [Fact] public void CycleDetectionTest() { - Assert.Throws(() => - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") - .Should() - .BeFreeOfCycles() - .Check(StaticTestArchitectures.SlicesTestArchitecture) - ); - Assert.False( - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") - .Should() - .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) - ); - Assert.True( - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") - .Should() - .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) - ); + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .Should() + .BeFreeOfCycles() + .AssertOnlyViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") + .Should() + .BeFreeOfCycles() + .AssertNoViolations(helper); + } + + // --- Rule evaluation driven by MatchingWithPackages (not just Matching) ------- + + [Fact] + public void BeFreeOfCycles_MatchingWithPackages_DetectsCycle() + { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .Should() + .BeFreeOfCycles() + .AssertOnlyViolations(helper); + } + + // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no + // cycle, unlike DotDotSemantics whose fixtures have no members and thus no edges at all -- + // an empty graph would pass here even if BeFreeOfCycles always returned "no cycles". + [Fact] + public void BeFreeOfCycles_MatchingWithPackages_WhenAcyclic_Passes() + { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)") + .Should() + .BeFreeOfCycles() + .AssertNoViolations(helper); + } + + [Fact] + public Task NotDependOnEachOther_MatchingWithPackages_ReportsViolations() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)..") + .Should() + .NotDependOnEachOther(); + rule.AssertAnyViolations(helper); + return helper.AssertSnapshotMatches(); } [Fact] @@ -91,58 +124,342 @@ public void MatchingTest() [Fact] public void NotDependOnEachOtherTest() { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); SliceRuleDefinition .Slices() .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice3.(*)") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.SlicesTestArchitecture); + .AssertNoViolations(helper); SliceRuleDefinition .Slices() .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice1.(*)") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.SlicesTestArchitecture); - Assert.True( + .AssertNoViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .Should() + .NotDependOnEachOther() + .AssertAnyViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") + .Should() + .NotDependOnEachOther() + .AssertAnyViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") + .Should() + .NotDependOnEachOther() + .AssertAnyViolations(helper); + } + + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + private const string DotDot = "SlicesTestAssembly.DotDotSemantics."; + + [Fact] + public void DirectCycleDetectionTest() + { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + foreach (var pattern in new[] { "(*)", "(*)..", "(**)" }) + { SliceRuleDefinition .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice1.(*)") + .Matching("SlicesTestAssembly.DirectCircle." + pattern) .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) - ); - Assert.Throws(() => + .BeFreeOfCycles() + .AssertOnlyViolations(helper); + } + } + + // None of these patterns folds Slice2.Inner into Slice2, so Slice1 -> Slice2 and + // Slice2.Inner -> Slice1 stay dependencies between three distinct slices and there is + // genuinely no cycle to find. + [Fact] + public void SubnamespaceCycleDetectionTest() + { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + foreach (var pattern in new[] { "(*)", "(*)..", "(**)" }) + { SliceRuleDefinition .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .Matching("SlicesTestAssembly.SubnamespaceCircle." + pattern) .Should() - .NotDependOnEachOther() - .Check(StaticTestArchitectures.SlicesTestArchitecture) + .BeFreeOfCycles() + .AssertNoViolations(helper); + } + } + + // See: https://github.com/TNG/ArchUnitNET/issues/208 -- "(**).." is the one pattern that + // folds Slice2.Inner into Slice2, which should surface the cycle + // Slice1 -> Slice2 -> (Slice2.Inner) -> Slice1. It currently does not: the fold drops the + // types sitting directly in the captured namespace (see + // Matching_DoubleAsteriskDotDot_DropsTypesDirectlyInCapturedNamespace), so Slice1Class + // never makes it into a slice and the cycle has no starting edge. Pinned as-is; this + // assertion is expected to flip once the matcher is reimplemented. + [Fact] + public void SubnamespaceCycleDetection_FoldedIntoParent_MissesCycle() + { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.SubnamespaceCircle.(**)..") + .Should() + .BeFreeOfCycles() + .AssertNoViolations(helper); + } + + [Fact] + public void Matching_SingleAsterisk_CapturesEveryDepth() + { + Assert.Equal( + new[] + { + "Slice1", + "Slice1.Service", + "Slice2", + "Slice2.Service", + "Slice3", + "Slice3.Group1", + "Slice3.Group1.Inner", + "Slice3.Group2", + "Slice3.Group2.Inner", + }, + Descriptions(Root + "(*)") ); - Assert.False( - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) + } + + [Fact] + public void Matching_SingleAsteriskDotDot_CapturesEveryDepth() + { + Assert.Equal(Descriptions(Root + "(*)"), Descriptions(Root + "(*)..")); + } + + [Fact] + public void Matching_DoubleAsteriskDotDot_FoldsSubnamespacesIntoParent() + { + Assert.Equal(new[] { "Slice1", "Slice2", "Slice3" }, Descriptions(Root + "(**)..")); + } + + /// + /// The descriptions above only say which slices exist; this pins what ends up inside + /// them, which is where "(**).." is at its most surprising. Folding keeps the types + /// from the sub-namespaces but drops the ones sitting directly in the captured + /// namespace, so Slice1Class/Slice2Class/Slice3Class are in no slice at all. That is + /// what makes the NotDependOnEachOther snapshots report Slice2 and Slice3 as + /// depending on nothing, and what hides the cycle in + /// . + /// + [Fact] + public void Matching_DoubleAsteriskDotDot_DropsTypesDirectlyInCapturedNamespace() + { + var types = SliceRuleDefinition + .Slices() + .Matching(Root + "(**)..") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) + .ToDictionary( + slice => slice.Description, + slice => + slice + .Types.Select(type => type.FullName) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray() + ); + + Assert.Equal(new[] { Root + "Slice1.Service.Service1Class" }, types["Slice1"]); + Assert.Equal(new[] { Root + "Slice2.Service.Service2Class" }, types["Slice2"]); + Assert.Equal( + new[] + { + Root + "Slice3.Group1.Group1Class", + Root + "Slice3.Group1.Inner.Inner1Class", + Root + "Slice3.Group2.Group2Class", + Root + "Slice3.Group2.Inner.Inner2Class", + }, + types["Slice3"] ); - Assert.False( - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) + } + + [Fact] + public void Matching_TrailingLiteralAfterCapture_MatchesNothing() + { + Assert.Empty(Descriptions(Root + "(**).Service..")); + } + + [Fact] + public void Matching_Alternation_MatchesNothing() + { + Assert.Empty(Descriptions(Root + "(**).[Service|Inner]")); + } + + [Fact] + public void Matching_LeadingDotDot_CapturesEveryDepth() + { + Assert.Equal(Descriptions(Root + "(*)"), Descriptions("..MultipleSubnamespaces.(*)")); + } + + // The fixture's namespace contains "DuplicatePrefix.Sub" twice + // (SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub), so a leading ".." pattern + // for that prefix has two candidate starting positions to strip from. This pins that + // AssignFunc's IndexOf (leftmost match) picks the first occurrence, leaving the second + // "DuplicatePrefix.Sub" -- and the one leading dot "..DuplicatePrefix.Sub." keeps after + // stripping only one of its two literal dots -- in the description. With an absolute + // (non-"..") prefix, the prefix can only ever be found at index 0, so that variant of this + // test would pass even if IndexOf were replaced by a hardcoded 0. + [Fact] + public void MatchingWithPackages_DuplicatePrefixSegment_UsesFirstOccurrenceAsPrefix() + { + var slices = SliceRuleDefinition + .Slices() + .MatchingWithPackages("..DuplicatePrefix.Sub.(*)") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) + .ToList(); + + Assert.Single(slices); + Assert.Equal(".DuplicatePrefix.Sub.DuplicatePrefix.Sub", slices[0].Description); + } + + // --- ".." semantics ------------------------------------------------------------- + + [Fact] + public void DotDot_BetweenCaptureGroups_CapturesEveryDepth() + { + Assert.Equal( + new[] + { + "Alpha.Service", + "AlphaService", + "Outer.Inner", + "Outer.Mid.Inner", + "Single", + }, + Descriptions(DotDot + "(*)..(*)") ); - Assert.False( - SliceRuleDefinition - .Slices() - .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.SlicesTestArchitecture) + } + + [Fact] + public void DotDot_BeforeLiteral_CapturesEveryDepth() + { + Assert.Equal( + new[] + { + "Alpha.Service", + "AlphaService", + "Outer.Inner", + "Outer.Mid.Inner", + "Single", + }, + Descriptions(DotDot + "(*)..Service") ); } + + [Fact] + public void DotDot_BeforeLiteral_GivesAlphaServiceAndAlphaDotServiceDistinctSlices() + { + var slices = SliceRuleDefinition + .Slices() + .Matching(DotDot + "(*)..Service") + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) + .ToList(); + + // "AlphaService" is one segment and "Alpha.Service" is two, but that distinction is + // never actually evaluated: the "..Service" postfix is discarded (see + // AlphaServiceClass.cs), so both namespaces just get their own unfolded slice, each + // holding exactly the one type it's made of. This is not evidence the matcher tells + // the two shapes apart -- it would look identical if it ignored segment boundaries + // entirely. + Assert.Single(slices.Single(slice => slice.Description == "AlphaService").Types); + Assert.Single(slices.Single(slice => slice.Description == "Alpha.Service").Types); + } + + // --- failure messages ----------------------------------------------------------- + + [Fact] + public Task BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.DirectCircle.(*)") + .Should() + .BeFreeOfCycles(); + rule.AssertOnlyViolations(helper); + return helper.AssertSnapshotMatches(); + } + + // MultipleSubnamespaces is the only fixture whose failing results are asserted purely via + // HasNoViolations, which short-circuits on the first violation and therefore never forces + // the rest of the lazily evaluated sequence. These two pin the complete result set. + // DirectCircle needs no equivalent: its three patterns collapse to the same cycle, whose + // wording BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage already pins. + [Fact] + public Task BeFreeOfCycles_MultipleSubnamespaces_ReportsEveryCycle() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .Matching(Root + "(**)") + .Should() + .BeFreeOfCycles(); + rule.AssertOnlyViolations(helper); + return helper.AssertSnapshotMatches(); + } + + [Fact] + public Task NotDependOnEachOther_MultipleSubnamespaces_ReportsEveryDependency() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .Matching(Root + "(**)") + .Should() + .NotDependOnEachOther(); + rule.AssertAnyViolations(helper); + return helper.AssertSnapshotMatches(); + } + + // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no + // cycle, unlike DotDotSemantics whose fixtures have no members and thus no edges at all -- + // an empty graph would pass here even if BeFreeOfCycles always returned "no cycles". + [Fact] + public Task BeFreeOfCycles_WhenNoCycles_ReportsAllSlicesFreeOfCycles() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.SubnamespaceCircle.(**)") + .Should() + .BeFreeOfCycles(); + rule.AssertNoViolations(helper); + return helper.AssertSnapshotMatches(); + } + + [Fact] + public Task NotDependOnEachOther_ReturnsDescriptiveDependencyMessage() + { + var helper = new SlicesAssemblyTestHelper(); + var rule = SliceRuleDefinition + .Slices() + .Matching(Root + "(**)..") + .Should() + .NotDependOnEachOther(); + // Mixed on purpose: the snapshot then shows both the passing and the failing wording. + rule.AssertAnyViolations(helper); + return helper.AssertSnapshotMatches(); + } + + private static string[] Descriptions(string pattern) + { + return SliceRuleDefinition + .Slices() + .Matching(pattern) + .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) + .Select(slice => slice.Description) + .OrderBy(description => description, StringComparer.Ordinal) + .ToArray(); + } } } diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_MultipleSubnamespaces_ReportsEveryCycle.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_MultipleSubnamespaces_ReportsEveryCycle.verified.txt new file mode 100644 index 000000000..b15623362 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_MultipleSubnamespaces_ReportsEveryCycle.verified.txt @@ -0,0 +1,23 @@ +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WhenNoCycles_ReportsAllSlicesFreeOfCycles.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WhenNoCycles_ReportsAllSlicesFreeOfCycles.verified.txt new file mode 100644 index 000000000..ef163ef79 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WhenNoCycles_ReportsAllSlicesFreeOfCycles.verified.txt @@ -0,0 +1,6 @@ +Query: Slices matching "SlicesTestAssembly.SubnamespaceCircle.(**)" should be free of cycles +Result: True +Description: All Slices are free of cycles. +Message: +All Evaluations passed + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage.verified.txt new file mode 100644 index 000000000..03a2b704c --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.BeFreeOfCycles_WithCycle_ReturnsDescriptiveCycleMessage.verified.txt @@ -0,0 +1,19 @@ +Query: Slices matching "SlicesTestAssembly.DirectCircle.(*)" should be free of cycles +Result: False +Description: Cycle found: +Slice1 -> Slice2 + SlicesTestAssembly.DirectCircle.Slice1.Slice1Class -> SlicesTestAssembly.DirectCircle.Slice2.Slice2Class +Slice2 -> Slice1 + SlicesTestAssembly.DirectCircle.Slice2.Slice2Class -> SlicesTestAssembly.DirectCircle.Slice1.Slice1Class + +Message: +"Slices matching "SlicesTestAssembly.DirectCircle.(*)" should be free of cycles" failed: + Cycle found: +Slice1 -> Slice2 + SlicesTestAssembly.DirectCircle.Slice1.Slice1Class -> SlicesTestAssembly.DirectCircle.Slice2.Slice2Class +Slice2 -> Slice1 + SlicesTestAssembly.DirectCircle.Slice2.Slice2Class -> SlicesTestAssembly.DirectCircle.Slice1.Slice1Class + + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt new file mode 100644 index 000000000..ae856ef18 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt @@ -0,0 +1,19 @@ +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other +Result: False +Description: SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices: +SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + +Result: True +Description: SlicesTestAssembly.MultipleSubnamespaces.Slice2 does not depend on another slice. +Result: True +Description: SlicesTestAssembly.MultipleSubnamespaces.Slice3 does not depend on another slice. +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other" failed: + SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices: +SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MultipleSubnamespaces_ReportsEveryDependency.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MultipleSubnamespaces_ReportsEveryDependency.verified.txt new file mode 100644 index 000000000..0a801d01b --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MultipleSubnamespaces_ReportsEveryDependency.verified.txt @@ -0,0 +1,66 @@ +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should not depend on each other +Result: False +Description: Slice1 does depend on other slices: +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class + +Result: False +Description: Slice1.Service does depend on other slices: +Slice1.Service -> Slice2.Service + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + +Result: False +Description: Slice2 does depend on other slices: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + +Result: False +Description: Slice2.Service does depend on other slices: +Slice2.Service -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + +Result: False +Description: Slice3 does depend on other slices: +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: False +Description: Slice3.Group1 does depend on other slices: +Slice3.Group1 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Group1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + +Result: True +Description: Slice3.Group1.Inner does not depend on another slice. +Result: True +Description: Slice3.Group2 does not depend on another slice. +Result: True +Description: Slice3.Group2.Inner does not depend on another slice. +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should not depend on each other" failed: + Slice1 does depend on other slices: +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class + + Slice1.Service does depend on other slices: +Slice1.Service -> Slice2.Service + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + + Slice2 does depend on other slices: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + + Slice2.Service does depend on other slices: +Slice2.Service -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + + Slice3 does depend on other slices: +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + Slice3.Group1 does depend on other slices: +Slice3.Group1 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Group1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class + + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_ReturnsDescriptiveDependencyMessage.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_ReturnsDescriptiveDependencyMessage.verified.txt new file mode 100644 index 000000000..448e70e4b --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_ReturnsDescriptiveDependencyMessage.verified.txt @@ -0,0 +1,19 @@ +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other +Result: False +Description: Slice1 does depend on other slices: +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + +Result: True +Description: Slice2 does not depend on another slice. +Result: True +Description: Slice3 does not depend on another slice. +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other" failed: + Slice1 does depend on other slices: +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class + + + + diff --git a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs index cc43345ab..feedab11d 100644 --- a/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs @@ -1,6 +1,7 @@ namespace SlicesTestAssembly.DotDotSemantics.AlphaService; -// A single segment that ends with the literal "Service": a pattern such as -// "DotDotSemantics.(*)..Service" must not match it by splitting the segment into -// "Alpha" + "Service". +// A single segment that ends with the literal "Service". Parse() discards everything after the +// first "(*" for single-asterisk patterns, so "DotDotSemantics.(*)..Service" never actually +// applies the "..Service" postfix: this namespace matches it just the same as Alpha/Service below, +// even though "AlphaService" is one segment and not "Alpha" + "Service". public class AlphaServiceClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/DuplicatePrefix/Sub/LeafClass.cs b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/DuplicatePrefix/Sub/LeafClass.cs new file mode 100644 index 000000000..f93a29898 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/DuplicatePrefix/Sub/LeafClass.cs @@ -0,0 +1,5 @@ +namespace SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub; + +// The full slice prefix "DuplicatePrefix.Sub" occurs twice in this namespace: matching it with a +// leading ".." (Contains-based) pattern is ambiguous about which occurrence is "the" prefix. +public class LeafClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs deleted file mode 100644 index cc0b61161..000000000 --- a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/OuterClass.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SlicesTestAssembly.DuplicatePrefix.Sub; - -public class OuterClass { } diff --git a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs b/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs deleted file mode 100644 index c6c14d7e2..000000000 --- a/TestAssemblies/SlicesTestAssembly/DuplicatePrefix/Sub/Sub/InnerClass.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SlicesTestAssembly.DuplicatePrefix.Sub.Sub; - -public class InnerClass { } From 71e4b28c1d367f30c58f32771b886e1ece004f44 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 28 Aug 2026 09:57:29 +0200 Subject: [PATCH 3/4] test: cover slice rule combinators and plain domain-type members A coverage pass over the slice/PlantUML tests turned up a few classes that were exercised only incidentally, or not at all, by the existing fixtures and rule-evaluation tests: - SliceRule.And()/Or() and And(IArchRule)/Or(IArchRule) were entirely uncovered -- the whole of SliceRule's line coverage gap. Cover both the evaluation outcome and the composed description for each. - SliceRuleCreator.GetSlices() guards against being called before a slice assignment is set; nothing reached that guard. - Slice's Classes/Interfaces/BackwardsDependencies/ToString/Equals, SliceIdentifier's CompareTo/Equals, and SliceIdentifierComparer.Equals have no rule-evaluation fixture that would exercise their branches (a null comparison, a reference match, a type mismatch) on its own. - GivenSlices.Description/FormatDescription were reached only as a side effect of building rule descriptions in other tests. - PlantUmlSlice's hyperlink rendering is unreachable through any diagram at all: PlantUmlFileBuilder never passes a hyperlink, so only a caller constructing the element itself can produce one. No production code changes. Signed-off-by: Alexander Linne --- .../AssemblyTestHelper/AssemblyTestHelper.cs | 19 +++ .../AssemblyTestHelperExtensions.cs | 5 + .../PlantUml/PlantUmlFileBuilderTest.cs | 8 + .../Fluent/Slices/SliceDomainTests.cs | 148 ++++++++++++++++ .../Slices/SliceRuleCombinationTests.cs | 158 ++++++++++++++++++ ...binesEvaluationAndDescription.verified.txt | 45 +++++ ...binesEvaluationAndDescription.verified.txt | 53 ++++++ ...binesEvaluationAndDescription.verified.txt | 82 +++++++++ ...binesEvaluationAndDescription.verified.txt | 57 +++++++ .../MixedTypes/IMixedTypesInterface.cs | 6 + .../MixedTypes/MixedTypesClass.cs | 4 + 11 files changed, 585 insertions(+) create mode 100644 ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs create mode 100644 ArchUnitNETTests/Fluent/Slices/SliceRuleCombinationTests.cs create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_FluentDefinition_CombinesEvaluationAndDescription.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_WithArchRule_CombinesEvaluationAndDescription.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_FluentDefinition_CombinesEvaluationAndDescription.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_WithArchRule_CombinesEvaluationAndDescription.verified.txt create mode 100644 TestAssemblies/SlicesTestAssembly/MixedTypes/IMixedTypesInterface.cs create mode 100644 TestAssemblies/SlicesTestAssembly/MixedTypes/MixedTypesClass.cs diff --git a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs index f763a86ea..9fdfa57e8 100644 --- a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs +++ b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs @@ -99,6 +99,25 @@ public void AssertNoViolations(IArchRule rule) Record(output); } + /// + /// The exact negation of , and like it a statement about the + /// verdict, not the results -- see there for why the two differ. Use it wherever the point of + /// the test is that a rule fails as a whole; prefer when + /// the point is that every evaluated object was reported, since this assertion is also + /// satisfied by the failing placeholder injects + /// for a rule whose input set is empty. + /// + public void AssertHasViolations(IArchRule rule) + { + var results = rule.Evaluate(Architecture).ToList(); + var output = FormatSnapshot(rule, results); + if (rule.HasNoViolations(Architecture)) + { + Assert.Fail(output); + } + Record(output); + } + public void AssertAnyViolations(IArchRule rule) { var results = rule.Evaluate(Architecture).ToList(); diff --git a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelperExtensions.cs b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelperExtensions.cs index c391a150a..5c03a0346 100644 --- a/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelperExtensions.cs +++ b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelperExtensions.cs @@ -12,6 +12,11 @@ public static void AssertNoViolations(this IArchRule archRule, AssemblyTestHelpe testHelper.AssertNoViolations(archRule); } + public static void AssertHasViolations(this IArchRule archRule, AssemblyTestHelper testHelper) + { + testHelper.AssertHasViolations(archRule); + } + public static void AssertAnyViolations(this IArchRule archRule, AssemblyTestHelper testHelper) { testHelper.AssertAnyViolations(archRule); diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs index 74d9a1045..feae85002 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs @@ -128,6 +128,14 @@ public void BuildUmlByDependenciesWithObjectsWithNoDependenciesTest() Assert.Equal(expectedUml, uml); } + [Fact] + public void SliceWithHyperlinkAppendsHyperlinkTest() + { + var slice = new PlantUmlSlice("Slice1", hyperlink: "https://example.com"); + var uml = slice.GetPlantUmlString(new RenderOptions()); + Assert.Equal("[Slice1] [[https://example.com]] " + Environment.NewLine, uml); + } + [Fact] public void HandleIllegalComponentNamesTest() { diff --git a/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs new file mode 100644 index 000000000..0827aacf7 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq; +using ArchUnitNET.Domain; +using ArchUnitNET.Domain.Extensions; +using ArchUnitNET.Fluent.Slices; +using SlicesTestAssembly.MixedTypes; +using Xunit; + +namespace ArchUnitNETTests.Fluent.Slices +{ + /// + /// Covers the fluent-API guard in and the plain + /// domain-type members of , and + /// , and that + /// no slice-diagram or rule-evaluation fixture happens to exercise. + /// + public class SliceDomainTests + { + private static Architecture Architecture => StaticTestArchitectures.SlicesTestArchitecture; + + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + + // --- SliceRuleCreator.GetSlices() without a slice assignment ------------------ + + [Fact] + public void GetSlices_WithoutSliceAssignment_Throws() + { + var creator = new SliceRuleCreator(); + var ex = Assert.Throws(() => + creator.GetSlices(Architecture) + ); + Assert.Equal( + "The Slice Assignment has to be set before GetSlices() can be called.", + ex.Message + ); + } + + // --- Domain.Slice members ------------------------------------------------------ + + [Fact] + public void Slice_ClassesAndInterfaces_FilterByType() + { + var classType = Architecture.GetClassOfType(typeof(MixedTypesClass)); + var interfaceType = Architecture.GetInterfaceOfType(typeof(IMixedTypesInterface)); + var slice = new Slice( + SliceIdentifier.Of("Mixed"), + new IType[] { classType, interfaceType } + ); + + Assert.Equal(new[] { classType }, slice.Classes); + Assert.Equal(new[] { interfaceType }, slice.Interfaces); + } + + [Fact] + public void Slice_BackwardsDependencies_AggregatesFromTypes() + { + var type = Architecture.Classes.First(c => c.BackwardsDependencies.Any()); + var slice = new Slice(SliceIdentifier.Of("Backwards"), new IType[] { type }); + + Assert.Equal(type.BackwardsDependencies, slice.BackwardsDependencies); + } + + [Fact] + public void Slice_ToString_ReturnsDescription() + { + var slice = new Slice(SliceIdentifier.Of("SomeSlice"), Array.Empty()); + + Assert.Equal("SomeSlice", slice.ToString()); + Assert.Equal(slice.Description, slice.ToString()); + } + + /// + /// Slice.Equals compares its Types collection by reference, not by content, so + /// differentTypesInstance below is unequal despite also being empty. That + /// relies on the two new IType[0] expressions yielding distinct references -- + /// true today, but it would silently stop testing anything if empty arrays were ever + /// interned, so the two instances are kept deliberately separate and named for it. + /// + [Fact] + public void Slice_Equals_Branches() + { + var types = new IType[0]; + var equalButDistinctTypes = new IType[0]; + Assert.NotSame(types, equalButDistinctTypes); + + var slice = new Slice(SliceIdentifier.Of("A"), types); + var sameIdentifierAndTypes = new Slice(SliceIdentifier.Of("A"), types); + var differentTypesInstance = new Slice(SliceIdentifier.Of("A"), equalButDistinctTypes); + var differentIdentifier = new Slice(SliceIdentifier.Of("B"), types); + + Assert.False(slice.Equals(null)); + Assert.True(slice.Equals(slice)); + Assert.False(slice.Equals("not a slice")); + Assert.True(slice.Equals(sameIdentifierAndTypes)); + Assert.False(slice.Equals(differentTypesInstance)); + Assert.False(slice.Equals(differentIdentifier)); + } + + // --- SliceIdentifier / SliceIdentifierComparer equality ----------------------- + + [Fact] + public void SliceIdentifier_CompareTo_Null_ReturnsFalse() + { + Assert.False(SliceIdentifier.Of("A").CompareTo(null)); + } + + [Fact] + public void SliceIdentifier_Equals_Branches() + { + var identifier = SliceIdentifier.Of("A"); + + Assert.False(identifier.Equals(null)); + Assert.True(identifier.Equals(identifier)); + Assert.False(identifier.Equals("not an identifier")); + Assert.True(identifier.Equals(SliceIdentifier.Of("A"))); + Assert.False(identifier.Equals(SliceIdentifier.Of("B"))); + } + + [Fact] + public void SliceIdentifierComparer_Equals_NullFirstArgument_ReturnsFalse() + { + Assert.False(SliceIdentifier.Comparer.Equals(null, SliceIdentifier.Of("A"))); + Assert.True( + SliceIdentifier.Comparer.Equals(SliceIdentifier.Of("A"), SliceIdentifier.Of("A")) + ); + } + + // --- GivenSlices description plumbing ----------------------------------------- + + [Fact] + public void GivenSlices_Description_ReflectsRuleCreatorDescription() + { + var slices = SliceRuleDefinition.Slices().Matching(Root + "(*)"); + + Assert.Equal("Slices matching \"" + Root + "(*)\"", slices.Description); + } + + [Fact] + public void GivenSlices_FormatDescription_PrependsMultipleDescription() + { + var slices = SliceRuleDefinition.Slices().Matching(Root + "(*)"); + + var formatted = slices.FormatDescription("empty", "single", "multiple"); + + Assert.Equal("multiple " + slices.Description, formatted); + } + } +} diff --git a/ArchUnitNETTests/Fluent/Slices/SliceRuleCombinationTests.cs b/ArchUnitNETTests/Fluent/Slices/SliceRuleCombinationTests.cs new file mode 100644 index 000000000..b62f9bdd4 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/SliceRuleCombinationTests.cs @@ -0,0 +1,158 @@ +using System.Threading.Tasks; +using ArchUnitNET.Fluent; +using ArchUnitNET.Fluent.Slices; +using ArchUnitNETTests.AssemblyTestHelper; +using Xunit; +using static ArchUnitNET.Fluent.ArchRuleDefinition; + +namespace ArchUnitNETTests.Fluent.Slices +{ + /// + /// Covers 's four combinators. Each is exercised for both evaluation + /// outcome and the composed Description, which + /// builds by concatenating the two rules' + /// descriptions with the combinator's conjunction; the snapshot's Query: line pins that + /// description verbatim, so no separate assertion on it is needed. + /// + /// + /// Outcomes are asserted with + /// and , the two helper + /// assertions that read a rule's verdict. The result-shaped AssertAnyViolations and + /// AssertOnlyViolations would be wrong here: a combined rule's results are a plain + /// concatenation of its operands', so "the results contain a failure" holds for a passing + /// Or just as it does for a failing And, and would say nothing about the + /// combinator. + /// + public class SliceRuleCombinationTests + { + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + + /// + /// Deliberately narrow: the bare Types().Should().Exist() that reads most naturally + /// here yields one passing result per type in the assembly, which would bury the point of + /// each snapshot and churn whenever a fixture type is added. + /// + private const string SinglePassingTypeName = + "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass"; + + private static SliceRule CyclicSliceRule => + SliceRuleDefinition.Slices().Matching(Root + "(**)").Should().BeFreeOfCycles(); + + // "(**).." on MultipleSubnamespaces is acyclic too, but only because it folds away the + // types that would form the cycle (see SlicesTests.SubnamespaceCycleDetection_ + // FoldedIntoParent_MissesCycle) -- a fixture that is genuinely acyclic keeps this test + // stable once that fold is reimplemented. + private static SliceRule AcyclicSliceRule => + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.SubnamespaceCircle.(**)") + .Should() + .BeFreeOfCycles(); + + private static IArchRule SinglePassingTypeRule => + Types().That().HaveFullName(SinglePassingTypeName).Should().Exist(); + + private static IArchRule FailingTypesRule(SlicesAssemblyTestHelper helper) => + Types().That().HaveName(helper.NonExistentObjectName).Should().Exist(); + + [Fact] + public async Task And_FluentDefinition_CombinesEvaluationAndDescription() + { + var helper = new SlicesAssemblyTestHelper(); + + helper.AddSnapshotHeader("Slice operand on its own"); + AcyclicSliceRule.AssertNoViolations(helper); + + helper.AddSnapshotHeader("Both operands pass"); + AcyclicSliceRule + .And() + .Types() + .That() + .HaveFullName(SinglePassingTypeName) + .Should() + .Exist() + .AssertNoViolations(helper); + + helper.AddSnapshotHeader("Slice operand fails"); + CyclicSliceRule + .And() + .Types() + .That() + .HaveFullName(SinglePassingTypeName) + .Should() + .Exist() + .AssertHasViolations(helper); + + await helper.AssertSnapshotMatches(); + } + + [Fact] + public async Task Or_FluentDefinition_CombinesEvaluationAndDescription() + { + var helper = new SlicesAssemblyTestHelper(); + + helper.AddSnapshotHeader("Slice operand on its own"); + CyclicSliceRule.AssertHasViolations(helper); + + helper.AddSnapshotHeader("Second operand rescues the failing slice operand"); + CyclicSliceRule + .Or() + .Types() + .That() + .HaveFullName(SinglePassingTypeName) + .Should() + .Exist() + .AssertNoViolations(helper); + + helper.AddSnapshotHeader("Both operands fail"); + CyclicSliceRule + .Or() + .Types() + .That() + .HaveName(helper.NonExistentObjectName) + .Should() + .Exist() + .AssertHasViolations(helper); + + await helper.AssertSnapshotMatches(); + } + + [Fact] + public async Task And_WithArchRule_CombinesEvaluationAndDescription() + { + var helper = new SlicesAssemblyTestHelper(); + + helper.AddSnapshotHeader("Both operands pass"); + AcyclicSliceRule.And(SinglePassingTypeRule).AssertNoViolations(helper); + + helper.AddSnapshotHeader("Second operand fails"); + AcyclicSliceRule.And(FailingTypesRule(helper)).AssertHasViolations(helper); + + helper.AddSnapshotHeader("Both operands fail"); + CyclicSliceRule.And(FailingTypesRule(helper)).AssertHasViolations(helper); + + await helper.AssertSnapshotMatches(); + } + + [Fact] + public async Task Or_WithArchRule_CombinesEvaluationAndDescription() + { + var helper = new SlicesAssemblyTestHelper(); + + // The verdict passes while the recorded results still report violations, and the + // rendered message announces a failure for a rule that passed: CombinedArchRule + // applies the conjunction in HasNoViolations but never in Evaluate, so the cyclic + // operand's failures survive into the message. Pinned as current behaviour, not + // endorsed -- a user calling Evaluate directly sees violations for a rule that Check + // accepts. See AssemblyTestHelper.AssertNoViolations for what this forced on the + // helper. + helper.AddSnapshotHeader("One operand passes, and the other's violations still show"); + CyclicSliceRule.Or(SinglePassingTypeRule).AssertNoViolations(helper); + + helper.AddSnapshotHeader("Both operands fail"); + CyclicSliceRule.Or(FailingTypesRule(helper)).AssertHasViolations(helper); + + await helper.AssertSnapshotMatches(); + } + } +} diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_FluentDefinition_CombinesEvaluationAndDescription.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_FluentDefinition_CombinesEvaluationAndDescription.verified.txt new file mode 100644 index 000000000..15d5cc49f --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_FluentDefinition_CombinesEvaluationAndDescription.verified.txt @@ -0,0 +1,45 @@ +===== Slice operand on its own ===== + +Query: Slices matching "SlicesTestAssembly.SubnamespaceCircle.(**)" should be free of cycles +Result: True +Description: All Slices are free of cycles. +Message: +All Evaluations passed + +===== Both operands pass ===== + +Query: Slices matching "SlicesTestAssembly.SubnamespaceCircle.(**)" should be free of cycles and Types that have full name "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass" should exist +Result: True +Description: All Slices are free of cycles. +Result: True +Description: SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass passed +Message: +All Evaluations passed + +===== Slice operand fails ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles and Types that have full name "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: True +Description: SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass passed +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_WithArchRule_CombinesEvaluationAndDescription.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_WithArchRule_CombinesEvaluationAndDescription.verified.txt new file mode 100644 index 000000000..c22e38343 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.And_WithArchRule_CombinesEvaluationAndDescription.verified.txt @@ -0,0 +1,53 @@ +===== Both operands pass ===== + +Query: Slices matching "SlicesTestAssembly.SubnamespaceCircle.(**)" should be free of cycles and Types that have full name "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass" should exist +Result: True +Description: All Slices are free of cycles. +Result: True +Description: SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass passed +Message: +All Evaluations passed + +===== Second operand fails ===== + +Query: Slices matching "SlicesTestAssembly.SubnamespaceCircle.(**)" should be free of cycles and Types that have name "NotTheNameOfAnyObject" should exist +Result: True +Description: All Slices are free of cycles. +Result: False +Description: There are no objects matching the criteria +Message: +"Types that have name "NotTheNameOfAnyObject" should exist" failed: + There are no objects matching the criteria + + + +===== Both operands fail ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles and Types that have name "NotTheNameOfAnyObject" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: False +Description: There are no objects matching the criteria +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + +"Types that have name "NotTheNameOfAnyObject" should exist" failed: + There are no objects matching the criteria + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_FluentDefinition_CombinesEvaluationAndDescription.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_FluentDefinition_CombinesEvaluationAndDescription.verified.txt new file mode 100644 index 000000000..14faa3188 --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_FluentDefinition_CombinesEvaluationAndDescription.verified.txt @@ -0,0 +1,82 @@ +===== Slice operand on its own ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + + + +===== Second operand rescues the failing slice operand ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles or Types that have full name "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: True +Description: SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass passed +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + + + +===== Both operands fail ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles or Types that have name "NotTheNameOfAnyObject" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: False +Description: There are no objects matching the criteria +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + +"Types that have name "NotTheNameOfAnyObject" should exist" failed: + There are no objects matching the criteria + + + diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_WithArchRule_CombinesEvaluationAndDescription.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_WithArchRule_CombinesEvaluationAndDescription.verified.txt new file mode 100644 index 000000000..42340d70c --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/Snapshots/SliceRuleCombinationTests.Or_WithArchRule_CombinesEvaluationAndDescription.verified.txt @@ -0,0 +1,57 @@ +===== One operand passes, and the other's violations still show ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles or Types that have full name "SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: True +Description: SlicesTestAssembly.DotDotSemantics.Outer.Mid.Inner.MidInnerClass passed +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + + + +===== Both operands fail ===== + +Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles or Types that have name "NotTheNameOfAnyObject" should exist +Result: False +Description: Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + +Result: False +Description: There are no objects matching the criteria +Message: +"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**)" should be free of cycles" failed: + Cycle found: +Slice2 -> Slice3 + SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class +Slice1 -> Slice2 + SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Slice2Class +Slice3 -> Slice1 + SlicesTestAssembly.MultipleSubnamespaces.Slice3.Slice3Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice1.Slice1Class + + +"Types that have name "NotTheNameOfAnyObject" should exist" failed: + There are no objects matching the criteria + + + diff --git a/TestAssemblies/SlicesTestAssembly/MixedTypes/IMixedTypesInterface.cs b/TestAssemblies/SlicesTestAssembly/MixedTypes/IMixedTypesInterface.cs new file mode 100644 index 000000000..0215eca05 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MixedTypes/IMixedTypesInterface.cs @@ -0,0 +1,6 @@ +namespace SlicesTestAssembly.MixedTypes; + +// The only interface in this assembly. It lives in a namespace that no slice pattern in the +// tests matches, so it stays out of every slice-assignment and diagram fixture and exists +// purely so Slice's Classes/Interfaces filters can be exercised on a mixed type list. +public interface IMixedTypesInterface { } diff --git a/TestAssemblies/SlicesTestAssembly/MixedTypes/MixedTypesClass.cs b/TestAssemblies/SlicesTestAssembly/MixedTypes/MixedTypesClass.cs new file mode 100644 index 000000000..ba25a327b --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/MixedTypes/MixedTypesClass.cs @@ -0,0 +1,4 @@ +namespace SlicesTestAssembly.MixedTypes; + +// See IMixedTypesInterface: the class half of the mixed type list. +public class MixedTypesClass : IMixedTypesInterface { } From d1ce0eacca8bfc2b6333e572f3b55be80914b5d7 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Thu, 10 Sep 2026 18:00:01 +0200 Subject: [PATCH 4/4] fix: return from the no-namespace branch of C4-style slice rendering PlantUmlSlice.BuildStringC4Style handles a null Namespace by appending a bare "Container(name, name)" line, but then fell through to Namespace.Remove(Namespace.Length - 1) instead of returning, dereferencing the field it had just found to be null. The branch therefore threw a NullReferenceException on precisely the input it was written for, and no namespace-less slice could be rendered in C4 style at all. Slices carry a namespace only when they come from MatchingWithPackages, so this made Slices().Matching(...) unusable with GenerationOptions.C4Style -- the combination the previous commit had to pin as an exception rather than as output. Add the missing return. BuildUmlBySlices_C4Style_Throws accordingly becomes BuildUmlBySlices_C4Style and snapshots the Container lines the branch was always meant to produce. Signed-off-by: Alexander Linne --- .../Domain/PlantUml/Export/PlantUmlSlice.cs | 1 + .../PlantUml/PlantUmlSliceDiagramTests.cs | 16 +++++--------- ...ests.BuildUmlBySlices_C4Style.verified.txt | 22 +++++++++++++++++++ 3 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_C4Style.verified.txt diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs index 342ccbba6..ad4aa0405 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs @@ -112,6 +112,7 @@ private StringBuilder BuildStringC4Style() if (Namespace == null) { result.Append("Container(" + _name + ", " + _name + ")"); + return result; } var namespc = Namespace.Remove(Namespace.Length - 1); diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs index 0737b966a..f069b07b5 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs @@ -122,19 +122,13 @@ public Task BuildUmlBySlicesMatchingWithPackages_C4Style() } [Fact] - public void BuildUmlBySlices_C4Style_Throws() + public Task BuildUmlBySlices_C4Style() { // Slices produced by Matching (as opposed to MatchingWithPackages) have no namespace - // prefix, and BuildStringC4Style's no-namespace branch appends its Container line but - // then falls through into Namespace.Remove instead of returning, so C4 style cannot - // render a namespace-less slice at all. - Assert.Throws(() => - new PlantUmlFileBuilder() - .WithDependenciesFrom( - SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")), - new GenerationOptions { C4Style = true } - ) - .AsString() + // prefix, so this exercises PlantUmlSlice.BuildStringC4Style's no-namespace branch. + return VerifySlices( + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { C4Style = true } ); } diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_C4Style.verified.txt new file mode 100644 index 000000000..851e24568 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlices_C4Style.verified.txt @@ -0,0 +1,22 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +Container(Slice1, Slice1) +Container(Slice1.Service, Slice1.Service) +Container(Slice2, Slice2) +Container(Slice2.Service, Slice2.Service) +Container(Slice3, Slice3) +Container(Slice3.Group1, Slice3.Group1) +Container(Slice3.Group1.Inner, Slice3.Group1.Inner) +Container(Slice3.Group2, Slice3.Group2) +Container(Slice3.Group2.Inner, Slice3.Group2.Inner) +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice2] --|> [Slice3] +[Slice2.Service] --|> [Slice3] +[Slice3] --|> [Slice1] +[Slice3.Group1] --|> [Slice3] +@enduml