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/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/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/AssemblyTestHelper.cs b/ArchUnitNETTests/AssemblyTestHelper/AssemblyTestHelper.cs index 639b51be4..9fdfa57e8 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,45 @@ 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); + } + + /// + /// 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) @@ -85,7 +130,7 @@ public void AssertAnyViolations(IArchRule rule) { Assert.Fail(output); } - _snapshot.Append(output); + Record(output); } public void AssertOnlyViolations(IArchRule rule) @@ -109,7 +154,7 @@ public void AssertOnlyViolations(IArchRule rule, bool allowNoResults) + output ); } - _snapshot.Append(output); + Record(output); } public void AssertException(IArchRule rule) @@ -123,6 +168,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/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/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/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/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs new file mode 100644 index 000000000..f069b07b5 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs @@ -0,0 +1,353 @@ +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 Task BuildUmlBySlices_C4Style() + { + // Slices produced by Matching (as opposed to MatchingWithPackages) have no namespace + // prefix, so this exercises PlantUmlSlice.BuildStringC4Style's no-namespace branch. + return VerifySlices( + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { C4Style = true } + ); + } + + [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_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 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/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/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/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/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs index 8fa7c0dc2..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,80 +12,111 @@ public class SlicesTests [Fact] public void CycleDetectionTest() { - Assert.Throws(() => - SliceRuleDefinition - .Slices() - .Matching("TestAssembly.Slices.(**)") - .Should() - .BeFreeOfCycles() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) - ); - Assert.False( - SliceRuleDefinition - .Slices() - .Matching("TestAssembly.Slices.(**)") - .Should() - .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) - ); - Assert.True( - SliceRuleDefinition - .Slices() - .Matching("TestAssembly.Slices.(**)..") - .Should() - .BeFreeOfCycles() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) - ); + 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] 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() ); } @@ -91,58 +124,342 @@ public void MatchingTest() [Fact] public void NotDependOnEachOtherTest() { + var helper = new SlicesAssemblyTestHelper().WithoutSnapshot(); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice3.(*)") + .Should() + .NotDependOnEachOther() + .AssertNoViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.Slice1.(*)") + .Should() + .NotDependOnEachOther() + .AssertNoViolations(helper); + SliceRuleDefinition + .Slices() + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)") + .Should() + .NotDependOnEachOther() + .AssertAnyViolations(helper); SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice3.(*)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(*)..") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); + .AssertAnyViolations(helper); SliceRuleDefinition .Slices() - .Matching("TestAssembly.Slices.Slice1.(*)") + .Matching("SlicesTestAssembly.MultipleSubnamespaces.(**)..") .Should() .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture); - Assert.True( + .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("TestAssembly.Slices.Slice1.(*)") + .Matching("SlicesTestAssembly.DirectCircle." + pattern) .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) - ); - 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("TestAssembly.Slices.(**)") + .Matching("SlicesTestAssembly.SubnamespaceCircle." + pattern) .Should() - .NotDependOnEachOther() - .Check(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + .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("TestAssembly.Slices.(**)") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + } + + [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("TestAssembly.Slices.(*)..") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + } + + [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("TestAssembly.Slices.(**)..") - .Should() - .NotDependOnEachOther() - .HasNoViolations(StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture) + } + + [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/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/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/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..feedab11d --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/DotDotSemantics/AlphaService/AlphaServiceClass.cs @@ -0,0 +1,7 @@ +namespace SlicesTestAssembly.DotDotSemantics.AlphaService; + +// 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/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/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/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 { } 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; - } -}