From 8a78006d7428e4f8a7fe6a4cf74f5071312741a2 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 23 Oct 2026 18:23:52 +0200 Subject: [PATCH] feat: nest slices in packages via GenerationOptions, deprecate MatchingWithPackages MatchingWithPackages existed because a slice carried nothing but its name: the exporter worked out the nesting from that string, so it had to be fully qualified, while rules needed short names. The choice between the two had to be made when the slices were created, and the qualified names ended up in failure messages and frozen-rule stores as well. Both methods group types the same way: the namespace before the capture group is literal pattern text, so prefixing every name with it changes no grouping. Diagrams need no data from the pattern: the exporter nests every slice in the namespaces its types share. GenerationOptions.NestSlicesInPackages (default false) makes the exporter draw the namespace tree as packages and put each slice, under its name, in the package of the deepest namespace that contains all of its types. A package with no slice of its own and a single child package is merged with it, e.g. into "Orders.Domain". A slice whose types share no namespace is drawn outside any package. This applies to every slice, also to those of SlicedBy and of custom assignments. Focus mode takes the options too. MatchingWithPackages is obsolete. It creates the same slices as Matching, so its rules report the same names and violations. It only still nests its slices in diagrams by default; they are now boxes inside their namespaces rather than the namespaces themselves, and package arrows are gone. Packages now come from the types' namespaces, so "..DuplicatePrefix.Sub.(**)" no longer gets a package ".DuplicatePrefix.Sub.". Slices whose name is a prefix of another's, such as App.Order and App.Orders, are no longer mistaken for a package and dropped from the diagram. Focus mode focuses the slices whose types all lie in the given namespace, and draws the slices they depend on or that depend on them. LimitDependencies keeps the arrows between slices whose common namespaces are equally deep. Both rules now use the types rather than the diagram names, so they work the same flat and nested. The guide shows Matching with NestSlicesInPackages in its diagram examples and gains a migration section. Breaking changes: - MatchingWithPackages is obsolete, and creates Matching's slices, so its failure messages and frozen violations no longer use names qualified with the namespace before the capture group. - Slice.NameSpace, Slice.ContainsNamespace() and SliceIdentifier.NameSpace are removed, and SliceIdentifier.Of loses its nameSpace parameter. - Nested slice diagrams change: every slice is a box at its types' common namespace; no slice is drawn as a package and there are no package arrows. - Focus mode takes a namespace, also for flat diagrams, and matches it by segments. - Nested LimitDependencies keeps same-depth arrows instead of a sibling overview. - OneToOneCompact renders whatever the dots in its names; the exporter filters instead. Signed-off-by: Alexander Linne --- .../PlantUml/Export/GenerationOptions.cs | 9 + .../PlantUml/Export/PlantUmlDependency.cs | 11 +- .../Domain/PlantUml/Export/PlantUmlDiagram.cs | 17 +- .../PlantUml/Export/PlantUmlFileBuilder.cs | 536 +++++++++--------- .../Domain/PlantUml/Export/PlantUmlSlice.cs | 57 ++ ArchUnitNET/Domain/Slice.cs | 15 +- ArchUnitNET/Domain/SliceIdentifier.cs | 18 +- ...antUmlFluentComponentDiagramInitializer.cs | 15 +- ArchUnitNET/Fluent/Slices/SliceAssignment.cs | 18 +- .../Fluent/Slices/SliceRuleInitializer.cs | 46 +- .../PlantUml/PlantUmlFileBuilderTest.cs | 45 ++ .../PlantUml/PlantUmlSliceDiagramTests.cs | 352 +++++++++--- ...LimitDependenciesSnapshotTest.verified.txt | 33 ++ ...iceNamesThatArePackageIdsTest.verified.txt | 17 + ...Tests.BuildUmlBySlicesFocusOn.verified.txt | 19 +- ...FocusOn_SingleAsteriskPattern.verified.txt | 21 + ...SkipsSlicesWithoutFocusString.verified.txt | 19 - ...sMatchingWithPackages_C4Style.verified.txt | 27 - ...hPackages_CaptureGroupAtStart.verified.txt | 11 - ...ngWithPackages_DoubleAsterisk.verified.txt | 27 - ...ackages_MultipleCaptureGroups.verified.txt | 23 - ...es_NonContiguousCaptureGroups.verified.txt | 23 - ...ithPackages_SameNamedPackages.verified.txt | 23 - ...ges_SameNamedPackages_C4Style.verified.txt | 23 - ...ngWithPackages_SingleAsterisk.verified.txt | 16 - ...uildUmlBySlicesNested_C4Style.verified.txt | 43 ++ ...cesNested_CaptureGroupAtStart.verified.txt | 22 + ...lBySlicesNested_DirectCircle.verified.txt} | 10 +- ...BySlicesNested_DoubleAsterisk.verified.txt | 43 ++ ...Nested_DuplicatePrefixSegment.verified.txt | 11 + ...licesNested_LimitDependencies.verified.txt | 41 ++ ...Nested_MultipleCaptureGroups.verified.txt} | 24 +- ...ed_NonContiguousCaptureGroups.verified.txt | 37 ++ ...licesNested_SameNamedPackages.verified.txt | 28 + ...ted_SameNamedPackages_C4Style.verified.txt | 28 + ...BySlicesNested_SingleAsterisk.verified.txt | 22 + .../PlantUmlFluentComponentDiagramTests.cs | 11 +- .../Slices/MatchingWithPackagesTests.cs | 192 +++++++ ArchUnitNETTests/Fluent/Slices/SlicesTests.cs | 55 +- ...ithPackages_ReportsViolations.verified.txt | 19 - .../PrefixSiblings/Orders/OrdersClass.cs | 4 + .../OrdersArchive/OrdersArchiveClass.cs | 8 + documentation/docs/guide.md | 37 +- 43 files changed, 1351 insertions(+), 705 deletions(-) create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesLimitDependenciesSnapshotTest.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.SliceNamesThatArePackageIdsTest.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt delete mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt} (51%) create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt rename ArchUnitNETTests/Domain/PlantUml/Snapshots/{PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt => PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt} (51%) create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt create mode 100644 ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt create mode 100644 ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs delete mode 100644 ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt create mode 100644 TestAssemblies/SlicesTestAssembly/PrefixSiblings/Orders/OrdersClass.cs create mode 100644 TestAssemblies/SlicesTestAssembly/PrefixSiblings/OrdersArchive/OrdersArchiveClass.cs diff --git a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs index 44348f502..6687cb14f 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs @@ -10,5 +10,14 @@ public class GenerationOptions public bool IncludeNodesWithoutDependencies { get; set; } = true; public bool LimitDependencies { get; set; } = false; public bool C4Style { get; set; } = false; + + /// + /// Whether slice diagrams draw the namespace tree as package blocks and put each + /// slice in the package of the deepest namespace that contains all of its types. A package + /// with no slice of its own and a single child package is merged with it. A slice whose + /// types share no namespace is drawn outside any package. Slices of the obsolete + /// MatchingWithPackages are always nested. + /// + public bool NestSlicesInPackages { get; set; } = false; } } diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs index 431b8cee0..e5cfd02e4 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs @@ -81,16 +81,7 @@ public string GetPlantUmlString(RenderOptions renderOptions = null) return OriginAlias + " -[#green]> " + TargetAlias + Environment.NewLine; case DependencyType.OneToOneCompact: - if (OriginCountOfDots() == TargetCountOfDots()) - { - return "[" - + OriginAlias - + "] --> [" - + TargetAlias - + "]" - + Environment.NewLine; - } - return ""; + return "[" + OriginAlias + "] --> [" + TargetAlias + "]" + Environment.NewLine; case DependencyType.Circle: return "[" diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs index 7d0e01167..1412fd343 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs @@ -64,6 +64,10 @@ public string GetPlantUmlString(RenderOptions renderOptions) /// place either, so the id is made from the name with those characters replaced by "_", /// and suffixed until it clashes with no other name in the diagram. The name itself stays /// the label, and slices whose names are valid ids keep them. + /// + /// A valid name can still be taken by a package: a nested slice named after its own + /// namespace, such as "App.Orders", would share its id with the package "App.Orders" it + /// is drawn in. Such a slice gets a replacement id as well. /// private static void AssignAliases(IReadOnlyCollection elements) { @@ -72,13 +76,22 @@ private static void AssignAliases(IReadOnlyCollection elements // Only slices are declared with an id; a dependency between components that are not // declared names them in brackets, where PlantUML accepts more. var sliceNames = slices.Select(slice => slice.ToString()).Distinct().ToList(); + var packageIds = new HashSet( + slices.Where(slice => slice.IsNested).SelectMany(slice => slice.PackageIds) + ); var taken = new HashSet( - sliceNames.Concat(dependencies.SelectMany(dep => new[] { dep.Origin, dep.Target })) + sliceNames + .Concat(dependencies.SelectMany(dep => new[] { dep.Origin, dep.Target })) + .Concat(packageIds) ); // In the order the slices appear, so the same diagram always gets the same ids. var aliases = new Dictionary(); - foreach (var name in sliceNames.Where(name => CharactersNotAllowedInId.IsMatch(name))) + foreach ( + var name in sliceNames.Where(name => + CharactersNotAllowedInId.IsMatch(name) || packageIds.Contains(name) + ) + ) { var alias = CharactersNotAllowedInId.Replace(name, "_"); var candidate = alias; diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs index 7432a2354..2384d3d3d 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs @@ -15,6 +15,12 @@ public class PlantUmlFileBuilder private List _sliceList = new List(); + /// + /// The deepest namespace that contains all types of each slice; see + /// . + /// + private Dictionary _commonNamespaces = new Dictionary(); + public string AsString(RenderOptions renderOptions = null) { if (renderOptions == null) @@ -122,6 +128,16 @@ public PlantUmlFileBuilder WithDependenciesFrom( return this; } + /// + /// Draws the given slices as components, with an arrow from one slice to another wherever + /// one of its types depends on one of the other's. + /// + /// The slices to draw, e.g. from GivenSlices.GetObjects. + /// Filters and layout options; the defaults if null. + /// With , every slice is drawn in the + /// package of the deepest namespace that contains all of its types. With + /// , only the arrows between slices whose + /// types share an equally deep namespace are drawn. public PlantUmlFileBuilder WithDependenciesFrom( IEnumerable slices, GenerationOptions generationOptions = null @@ -129,8 +145,8 @@ public PlantUmlFileBuilder WithDependenciesFrom( { _sliceList = slices.Distinct().ToList(); RemovePatternInappropriateSlices(); + AssignCommonNamespaces(); - var nodes = new Dictionary(); if (generationOptions == null) { generationOptions = new GenerationOptions(); @@ -138,7 +154,6 @@ public PlantUmlFileBuilder WithDependenciesFrom( foreach (var slice in _sliceList) { - var sliceIsPackage = IsPackage(slice); var dependencyTargets = SelectDependencies( slice, _sliceList, @@ -147,94 +162,52 @@ public PlantUmlFileBuilder WithDependenciesFrom( generationOptions ); - if (!sliceIsPackage) - { - var slc = new PlantUmlSlice(slice.Description, slice.NameSpace); - if (generationOptions.C4Style) - { - slc.UseS4Style(); - } - nodes.Add(slice, slc); - } - if (!generationOptions.LimitDependencies) { - if (sliceIsPackage) - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.PackageToOne - )) - ); - } - else - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.OneToOne - )) - ); - } + _dependencies.AddRange( + dependencyTargets.Select(target => new PlantUmlDependency( + slice.Description, + target.Description, + DependencyType.OneToOne + )) + ); } - else if (slice.ContainsNamespace()) + else { - if (sliceIsPackage) - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.PackageToPackageIfSameParentNamespace - )) - ); - } - else - { - var dt = dependencyTargets + _dependencies.AddRange( + dependencyTargets + .Where(target => + CountOfSegments(_commonNamespaces[target]) + == CountOfSegments(_commonNamespaces[slice]) + ) .Select(target => new PlantUmlDependency( slice.Description, target.Description, - DependencyType.OneToOneIfSameParentNamespace + DependencyType.OneToOneCompact )) - .Where(dependency => - dependency.OriginCountOfDots() < dependency.TargetCountOfDots() - || _sliceList.All(slc => - slc.Description == dependency.Target - || !slc.Description.Contains(dependency.Target) - ) - ); - _dependencies.AddRange(dt); - } - } - else - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.OneToOneCompact - )) ); } } - if (!generationOptions.IncludeNodesWithoutDependencies) - { - RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); - } - if (!generationOptions.LimitDependencies) { - RemoveDuplicateDependenciesWhenShowingPackages(); ReplaceCirclesWithAppropriateDependencyType(); } RemoveDuplicatedArrowsIfExist(); - _diagram.AddElements(nodes.Values); + var drawnSlices = generationOptions.IncludeNodesWithoutDependencies + ? _sliceList + : SlicesContainedInDependencies(); + var packages = AssignPackages(drawnSlices, generationOptions.NestSlicesInPackages); + foreach (var slice in drawnSlices) + { + var node = CreateNode(slice, packages); + if (generationOptions.C4Style) + { + node.UseS4Style(); + } + _diagram.AddElement(node); + } _diagram.AddElements(_dependencies); return this; @@ -275,6 +248,10 @@ public PlantUmlFileBuilder WithDependenciesFrom( target => target.Types, generationOptions ) + .Where(target => + !generationOptions.LimitDependencies + || CountOfSegments(target.FullName) == CountOfSegments(ns.FullName) + ) .Select(target => new PlantUmlDependency( ns.FullName, target.FullName, @@ -328,9 +305,21 @@ var parent in parentNamespaces.Where(parentName => return this; } + /// + /// Draws only the slices whose types all lie in the namespace + /// or below it, colored, and the slices they depend on or that depend on them, with the + /// arrows between them that start or end at one of the former. + /// + /// The slices to draw, e.g. from GivenSlices.GetObjects. + /// The namespace to focus on. + /// Only + /// is read; the defaults if null. + /// The package is empty, a single dot or a namespace + /// that no slice lies in. public PlantUmlFileBuilder WithDependenciesFromFocusOn( IEnumerable slices, - string package + string package, + GenerationOptions generationOptions = null ) { _sliceList = slices.Distinct().ToList(); @@ -348,114 +337,62 @@ string package package = package.Remove(package.Length - 1); } - RemovePatternInappropriateSlices(package); + RemovePatternInappropriateSlices(); + AssignCommonNamespaces(); - var existPackage = _sliceList.Any(slice => - (slice.NameSpace + slice.Description).Contains(package) + var focusedSlices = new HashSet( + _sliceList + .Where(slice => LiesIn(_commonNamespaces[slice], package)) + .Select(slice => slice.Description) ); - if (!existPackage) + if (focusedSlices.Count == 0) { - throw new ArgumentException( - "The package [" + package + "] is not contained in this slice" - ); + throw new ArgumentException("No slice lies in the namespace [" + package + "]"); } - var nodes = new Dictionary(); - var generationOptions = new GenerationOptions(); + var defaultOptions = new GenerationOptions(); foreach (var slice in _sliceList) { - var sliceIsPackage = IsPackage(slice); var dependencyTargets = SelectDependencies( slice, _sliceList, target => target.Description, target => target.Types, - generationOptions + defaultOptions ); - if (sliceIsPackage) - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.PackageToOne - )) - ); - } - else - { - _dependencies.AddRange( - dependencyTargets.Select(target => new PlantUmlDependency( - slice.Description, - target.Description, - DependencyType.OneToOne - )) - ); - } - - if (!sliceIsPackage) - { - nodes.Add( - slice, - slice.Description.Contains(package) - ? new PlantUmlSlice(slice.Description, slice.NameSpace, "99ffd1") - : new PlantUmlSlice(slice.Description, slice.NameSpace) - ); - } - else if (!slice.Description.Contains(package)) - { - nodes.Add(slice, new PlantUmlSlice(slice.Description + ".", slice.NameSpace)); - } - } - - if (!generationOptions.IncludeNodesWithoutDependencies) - { - RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); + _dependencies.AddRange( + dependencyTargets.Select(target => new PlantUmlDependency( + slice.Description, + target.Description, + DependencyType.OneToOne + )) + ); } - RemoveDuplicateDependenciesWhenShowingPackages(); ReplaceCirclesWithAppropriateDependencyType(); RemoveDuplicatedArrowsIfExist(); - for (var j = _dependencies.Count - 1; j >= 0; j--) - { - if ( - !_dependencies[j].Origin.Contains(package) - && !_dependencies[j].Target.Contains(package) - ) - { - _dependencies.RemoveAt(j); - } - } - - for (var i = nodes.Count - 1; i >= 0; i--) - { - var nodeExist = _dependencies.Any(dep => - nodes.ElementAt(i).Key.Description == dep.Origin - || nodes.ElementAt(i).Key.Description == dep.Target - ); - if (!nodeExist) - { - nodes.Remove(nodes.ElementAt(i).Key); - } - } - - _diagram.AddElements(nodes.Values); + _dependencies.RemoveAll(dependency => + !focusedSlices.Contains(dependency.Origin) + && !focusedSlices.Contains(dependency.Target) + ); - var tempList = _sliceList - .Where(slice => IsPackage(slice) && slice.Description.Contains(package)) - .Select(slice => new PlantUmlSlice( - slice.Description + ".", - slice.NameSpace, - "99ffd1" - )) - .ToList(); - if (tempList.Count > 0) - { - _diagram.AddElements(tempList.OrderByDescending(element => element.ToString())); - } + var drawnSlices = SlicesContainedInDependencies(); + var packages = AssignPackages( + drawnSlices, + generationOptions?.NestSlicesInPackages ?? false + ); + _diagram.AddElements( + drawnSlices.Select(slice => + CreateNode( + slice, + packages, + focusedSlices.Contains(slice.Description) ? "99ffd1" : null + ) + ) + ); _diagram.AddElements(_dependencies); return this; @@ -508,36 +445,20 @@ GenerationOptions generationOptions ); } - private void RemovePatternInappropriateSlices(string thatContainsThisString = null) + private void RemovePatternInappropriateSlices() { if (_sliceList.Any(slice => slice.CountOfAsteriskInPattern == null)) { return; } - if ( - thatContainsThisString != null - && !_sliceList.Any(slice => - (slice.NameSpace + slice.Description).Contains(thatContainsThisString) - ) - ) - { - return; - } for (var i = _sliceList.Count - 1; i >= 0; i--) { var dots = 0; - if ( - thatContainsThisString != null - && !(_sliceList[i].Description).Contains(thatContainsThisString) - ) - { - continue; - } - + // The description is never qualified, so its dots are those below the namespace + // the pattern matched before its capture group. dots += _sliceList[i].Description.Count(c => c == '.'); - dots -= (_sliceList[i].NameSpace ?? string.Empty).Count(c => c == '.'); if (dots >= _sliceList[i].CountOfAsteriskInPattern) { @@ -563,148 +484,197 @@ private void RemoveDuplicatedArrowsIfExist() } } - private void RemoveDuplicateDependenciesWhenShowingPackages() + private void ReplaceCirclesWithAppropriateDependencyType() { - if (_sliceList.Any(slice => !slice.ContainsNamespace())) - { - return; - } - - for (var j = _dependencies.Count - 1; j >= 0; j--) + for (var i = _dependencies.Count - 1; i >= 0; i--) { - if ( - _sliceList.Any(slice => - slice.Description.Contains(_dependencies[j].Target) - && slice.Description != _dependencies[j].Target - ) - ) + for (var j = i - 1; j >= 0; j--) { - if (_dependencies[j].Origin.Contains(_dependencies[j].Target)) - { - _dependencies.RemoveAt(j); - } - else if (_dependencies[j].DependencyType == DependencyType.PackageToOne) - { - _dependencies[j] = new PlantUmlDependency( - _dependencies[j].Origin, - _dependencies[j].Target, - DependencyType.PackageToPackage - ); - } - else + if ( + _dependencies[i].Target != _dependencies[j].Origin + || _dependencies[i].Origin != _dependencies[j].Target + ) { - _dependencies[j] = new PlantUmlDependency( - _dependencies[j].Origin, - _dependencies[j].Target, - DependencyType.OneToPackage - ); + continue; } - continue; - } - if (_dependencies[j].DependencyType != DependencyType.PackageToOne) - { - continue; - } - - if (_dependencies[j].Target.Contains(_dependencies[j].Origin)) - { - _dependencies.RemoveAt(j); + _dependencies.RemoveAt(i); + _dependencies[j] = new PlantUmlDependency( + _dependencies[j].Origin, + _dependencies[j].Target, + DependencyType.Circle + ); + break; } } + } - for (var i = _dependencies.Count - 1; i >= 0; i--) + /// + /// The slices that a dependency starts or ends at, in the order of the slice list. + /// + private List SlicesContainedInDependencies() + { + var namesInDependencies = new HashSet( + _dependencies.SelectMany(dep => new[] { dep.Origin, dep.Target }) + ); + return _sliceList + .Where(slice => namesInDependencies.Contains(slice.Description)) + .ToList(); + } + + private void AssignCommonNamespaces() + { + _commonNamespaces = _sliceList.ToDictionary(slice => slice, CommonNamespaceOf); + } + + /// + /// The deepest namespace that contains all types of the slice, i.e. the longest sequence + /// of leading segments their namespaces share, or an empty string if they share none. + /// + private static string CommonNamespaceOf(Slice slice) + { + string[] commonSegments = null; + var length = 0; + foreach (var type in slice.Types) { - if ( - _dependencies[i].DependencyType == DependencyType.PackageToOne - || _dependencies[i].DependencyType == DependencyType.PackageToPackage - ) + var segments = (type.Namespace?.FullName ?? "").Split('.'); + if (commonSegments == null) { - if ( - _dependencies.Any(dependency => - _dependencies[i].Target == dependency.Target - && dependency.Origin.Contains(_dependencies[i].Origin) - && dependency.Origin != _dependencies[i].Origin - ) - ) - { - _dependencies.RemoveAt(i); - continue; - } + commonSegments = segments; + length = segments.Length; + continue; } - if ( - _dependencies[i].DependencyType == DependencyType.OneToPackage - || _dependencies[i].DependencyType == DependencyType.PackageToPackage - ) + length = Math.Min(length, segments.Length); + for (var i = 0; i < length; i++) { - if ( - _dependencies.Any(dependency => - dependency.Target.Contains(_dependencies[i].Target) - && dependency.Target != _dependencies[i].Target - && dependency.Origin.Contains(_dependencies[i].Origin) - ) - ) + if (segments[i] != commonSegments[i]) { - _dependencies.RemoveAt(i); - continue; + length = i; + break; } } + } - if ( - _dependencies[i].Target.Contains(_dependencies[i].Origin) - && _dependencies[i].Target != _dependencies[i].Origin - ) + return commonSegments == null ? "" : string.Join(".", commonSegments.Take(length)); + } + + private static int CountOfSegments(string nameSpace) + { + return nameSpace == "" ? 0 : nameSpace.Count(c => c == '.') + 1; + } + + /// + /// Whether is or lies below it. + /// + private static bool LiesIn(string nameSpace, string package) + { + return nameSpace == package + || nameSpace.StartsWith(package + ".", StringComparison.Ordinal); + } + + /// + /// Decides for every slice the packages it is drawn in, outermost first, if slices are + /// nested at all. + /// + /// + /// The packages are the namespace tree of the slices' common namespaces, and each slice + /// sits in the package of its own common namespace. A package that holds no slice and + /// exactly one package is merged with that package into one labelled with both, e.g. + /// "Orders.Domain", down to the first package that holds a slice or several packages. So + /// the outermost package is a whole namespace such as "App.Orders". A slice whose types + /// share no namespace is in no package and is drawn on its own. + /// + /// The slices that are drawn. + /// + /// Whether the options ask for nesting; slices of MatchingWithPackages are nested + /// regardless. + /// + private Dictionary> AssignPackages( + List slices, + bool nestSlicesInPackages + ) + { + var packages = new Dictionary>(); + if (!nestSlicesInPackages && !slices.Any(slice => slice.NestByDefault)) + { + return packages; + } + + var nestedSlices = slices.Where(slice => _commonNamespaces[slice] != "").ToList(); + var root = new NamespaceNode(); + foreach (var slice in nestedSlices) + { + var node = root; + foreach (var segment in _commonNamespaces[slice].Split('.')) { - _dependencies.RemoveAt(i); + node = node.GetOrAddChild(segment); } + node.HoldsSlice = true; } - } - private void ReplaceCirclesWithAppropriateDependencyType() - { - for (var i = _dependencies.Count - 1; i >= 0; i--) + foreach (var slice in nestedSlices) { - for (var j = i - 1; j >= 0; j--) + var path = new List(); + var node = root; + foreach (var segment in _commonNamespaces[slice].Split('.')) { - if ( - _dependencies[i].Target != _dependencies[j].Origin - || _dependencies[i].Origin != _dependencies[j].Target - ) + var mergesWithChild = + node != root && !node.HoldsSlice && node.Children.Count == 1; + node = node.Children[segment]; + if (mergesWithChild) { - continue; + path[path.Count - 1] += "." + segment; + } + else + { + path.Add(segment); } - - _dependencies.RemoveAt(i); - _dependencies[j] = new PlantUmlDependency( - _dependencies[j].Origin, - _dependencies[j].Target, - DependencyType.Circle - ); - break; } + packages.Add(slice, path); } + + return packages; } - private bool IsPackage(Slice slice) + private static PlantUmlSlice CreateNode( + Slice slice, + Dictionary> packages, + string color = null + ) { - if (!slice.ContainsNamespace()) + if (!packages.TryGetValue(slice, out var path)) { - return false; + return new PlantUmlSlice(slice.Description, null, color); } - for (var i = _sliceList.Count - 1; i >= 0; i--) + return new PlantUmlSlice( + slice.Description, + path[0] + ".", + path.Skip(1).ToList(), + slice.Description, + color + ); + } + + /// A namespace in the tree that builds. + private class NamespaceNode + { + public readonly Dictionary Children = + new Dictionary(); + + public bool HoldsSlice; + + public NamespaceNode GetOrAddChild(string segment) { - if ( - _sliceList[i].Description != slice.Description - & _sliceList[i].Description.StartsWith(slice.Description) - ) + if (!Children.TryGetValue(segment, out var child)) { - return true; + child = new NamespaceNode(); + Children.Add(segment, child); } - } - return false; + return child; + } } } } diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs index 75ab17283..7c02c72b4 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs @@ -13,6 +13,10 @@ public class PlantUmlSlice : IPlantUmlElement private string Color { get; } private bool C4Style { get; set; } + // Set when the caller already knows where the slice sits; see the internal constructor. + private readonly IReadOnlyList _packages; + private readonly string _leaf; + public PlantUmlSlice( string name, string nameSpace = null, @@ -29,6 +33,31 @@ public PlantUmlSlice( Alias = name; } + /// + /// Creates a nested slice whose packages and label are given rather than read off its + /// name. + /// + /// The full slice name, which is also the alias arrows point at. + /// The root package, with a trailing ".". + /// The packages between the root package and the slice. + /// + /// The label inside the innermost package, or an empty string if the slice only stands + /// for that package. + /// + /// The background color, if any. + internal PlantUmlSlice( + string name, + string nameSpace, + IReadOnlyList packages, + string leaf, + string color = null + ) + : this(name, nameSpace, color) + { + _packages = packages; + _leaf = leaf; + } + public override string ToString() { return _name; @@ -66,12 +95,35 @@ internal IReadOnlyList PackagePath get { var path = new List { RootPackage }; + if (_packages != null) + { + path.AddRange(_packages); + return path; + } + var segments = _name.Remove(0, Namespace.Length).Split('.'); path.AddRange(segments.Take(segments.Length - 1)); return path; } } + /// + /// The ids of the packages this slice is nested in, outermost first; see + /// . + /// + internal IEnumerable PackageIds + { + get + { + string id = null; + foreach (var package in PackagePath) + { + id = id == null ? package : id + "." + package; + yield return id; + } + } + } + /// /// The label of the component inside its innermost package, or an empty string if the /// slice only stands for that package. @@ -80,6 +132,11 @@ internal string Leaf { get { + if (_leaf != null) + { + return _leaf; + } + var name = _name.Remove(0, Namespace.Length); return name.Substring(name.LastIndexOf('.') + 1); } diff --git a/ArchUnitNET/Domain/Slice.cs b/ArchUnitNET/Domain/Slice.cs index 658a06596..0c1bb482b 100644 --- a/ArchUnitNET/Domain/Slice.cs +++ b/ArchUnitNET/Domain/Slice.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using ArchUnitNET.Domain.Dependencies; -using JetBrains.Annotations; namespace ArchUnitNET.Domain { @@ -27,10 +26,15 @@ public Slice(SliceIdentifier identifier, IEnumerable types) public string Description => Identifier.Description; - [CanBeNull] - public string NameSpace => Identifier.NameSpace; public int? CountOfAsteriskInPattern => Identifier.CountOfAsteriskInPattern; + /// + /// Whether diagrams nest this slice in packages even if + /// is not set, which + /// is the case for slices of the obsolete MatchingWithPackages. + /// + internal bool NestByDefault { get; set; } + protected bool Equals(Slice other) { return Equals(Identifier, other.Identifier); @@ -65,10 +69,5 @@ public override string ToString() { return Description; } - - public bool ContainsNamespace() - { - return NameSpace != null; - } } } diff --git a/ArchUnitNET/Domain/SliceIdentifier.cs b/ArchUnitNET/Domain/SliceIdentifier.cs index e84a4a539..60af37821 100644 --- a/ArchUnitNET/Domain/SliceIdentifier.cs +++ b/ArchUnitNET/Domain/SliceIdentifier.cs @@ -1,6 +1,4 @@ -using JetBrains.Annotations; - -namespace ArchUnitNET.Domain +namespace ArchUnitNET.Domain { public class SliceIdentifier : StringIdentifier, IHasDescription { @@ -10,29 +8,21 @@ public class SliceIdentifier : StringIdentifier, IHasDescription private SliceIdentifier( string identifier, bool ignored, - int? countOfAsteriskInPattern = null, - string nameSpace = null + int? countOfAsteriskInPattern = null ) : base(identifier) { Ignored = ignored; CountOfAsteriskInPattern = countOfAsteriskInPattern; - NameSpace = nameSpace; } public string Description => Identifier; - [CanBeNull] - public readonly string NameSpace; public readonly int? CountOfAsteriskInPattern; - public static SliceIdentifier Of( - string identifier, - int? countOfAsteriskInPattern = null, - string nameSpace = null - ) + public static SliceIdentifier Of(string identifier, int? countOfAsteriskInPattern = null) { - return new SliceIdentifier(identifier, false, countOfAsteriskInPattern, nameSpace); + return new SliceIdentifier(identifier, false, countOfAsteriskInPattern); } public static SliceIdentifier Ignore() diff --git a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs index 2bc3f92bc..2be5fef63 100644 --- a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs +++ b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs @@ -65,14 +65,25 @@ public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices( return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator); } + /// + /// Draws only the slices whose types all lie in the namespace + /// or below it, and the slices they depend on or that + /// depend on them; see . + /// + /// The slices to draw. + /// The namespace to focus on. + /// Only + /// is read. public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices( IEnumerable slices, - string focusOnPackage + string focusOnPackage, + GenerationOptions generationOptions = null ) { _fluentComponentDiagramCreator.Builder.WithDependenciesFromFocusOn( slices, - focusOnPackage + focusOnPackage, + generationOptions ); _fluentComponentDiagramCreator.AddToDescription("with dependencies from slices"); return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator); diff --git a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs index db5bdf1c4..23a89a9ff 100644 --- a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs +++ b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs @@ -12,19 +12,35 @@ public class SliceAssignment : IHasDescription public SliceAssignment( Func assignIdentifierFunc, string description + ) + : this(assignIdentifierFunc, description, false) { } + + internal SliceAssignment( + Func assignIdentifierFunc, + string description, + bool nestByDefault ) { _assignIdentifierFunc = assignIdentifierFunc; Description = description; + _nestByDefault = nestByDefault; } + /// + /// Whether diagrams nest the slices in packages even if + /// is not set; + /// only for the obsolete MatchingWithPackages. + /// + private readonly bool _nestByDefault; + public string Description { get; } public IEnumerable Apply(IEnumerable types) { return types.GroupBy( _assignIdentifierFunc, - (identifier, enumerable) => new Slice(identifier, enumerable), + (identifier, enumerable) => + new Slice(identifier, enumerable) { NestByDefault = _nestByDefault }, SliceIdentifier.Comparer ); } diff --git a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs index 228de6820..00b67c11f 100644 --- a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs +++ b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs @@ -21,29 +21,36 @@ public SliceRuleInitializer(SliceRuleCreator ruleCreator) /// public GivenSlices Matching(string pattern) { - _ruleCreator.SetSliceAssignment( - new SliceAssignment( - t => - { - var parseResult = Parse(pattern); - return AssignFunc(t, parseResult.pattern, parseResult.asterisk); - }, - "matching \"" + pattern + "\"" - ) - ); - return new GivenSlices(_ruleCreator); + return Matching(pattern, false); } + /// + /// Creates the same slices as , which PlantUML diagrams nest in + /// package blocks even without + /// . + /// + /// The namespace pattern, as for . + [Obsolete( + "Use Matching(pattern) and draw the slices with " + + "new GenerationOptions { NestSlicesInPackages = true }. MatchingWithPackages " + + "creates the same slices as Matching and will be removed in a future release." + )] public GivenSlices MatchingWithPackages(string pattern) + { + return Matching(pattern, true); + } + + private GivenSlices Matching(string pattern, bool nestByDefault) { _ruleCreator.SetSliceAssignment( new SliceAssignment( t => { var parseResult = Parse(pattern); - return AssignFunc(t, parseResult.pattern, parseResult.asterisk, true); + return AssignFunc(t, parseResult.pattern, parseResult.asterisk); }, - "matching \"" + pattern + "\"" + "matching \"" + pattern + "\"", + nestByDefault ) ); return new GivenSlices(_ruleCreator); @@ -87,8 +94,7 @@ private static (string pattern, int? asterisk) Parse(string pattern) private static SliceIdentifier AssignFunc( IType type, string pattern, - int? countOfSingleAsterisk, - bool fullName = false + int? countOfSingleAsterisk ) { var indexOfAsteriskInPattern = pattern.IndexOf("(*", StringComparison.Ordinal); @@ -161,15 +167,7 @@ private static SliceIdentifier AssignFunc( ); } - // An empty prefix means the pattern starts with its capture group; there is no - // namespace to nest the slice under, so report none rather than an empty one. - return fullName - ? SliceIdentifier.Of( - slicePrefix + sliceString, - countOfSingleAsterisk, - slicePrefix == "" ? null : slicePrefix - ) - : SliceIdentifier.Of(sliceString, countOfSingleAsterisk); + return SliceIdentifier.Of(sliceString, countOfSingleAsterisk); } } } diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs index 64302e59f..95cbfbf97 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs @@ -65,6 +65,22 @@ public Task BuildUmlByNamespacesSnapshotTest() return Verifier.Verify(uml).DisableDiff().UseDirectory("Snapshots"); } + [Fact] + public Task BuildUmlByNamespacesLimitDependenciesSnapshotTest() + { + // Only the arrows between namespaces with the same number of segments are drawn. + var namespaces = StaticTestArchitectures.SlicesTestArchitecture.Namespaces.Where(ns => + ns.FullName.StartsWith("SlicesTestAssembly.MultipleSubnamespaces") + ); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFrom( + namespaces, + new GenerationOptions { LimitDependencies = true } + ) + .AsString(); + return Verifier.Verify(uml).DisableDiff().UseDirectory("Snapshots"); + } + [Fact] public void BuildUmlBySlicesTest() { @@ -187,6 +203,20 @@ public Task SliceNamesThatAreNotValidIdsTest() ); } + [Fact] + public Task SliceNamesThatArePackageIdsTest() + { + // Each slice is named like the package it is drawn in, so both get an id of their own. + return VerifyElements( + new IPlantUmlElement[] + { + new PlantUmlSlice("A.X", "A.", new[] { "X" }, "A.X"), + new PlantUmlSlice("A.X.Y", "A.", new[] { "X", "Y" }, "A.X.Y"), + new PlantUmlDependency("A.X.Y", "A.X", DependencyType.OneToOne), + } + ); + } + [Fact] public Task C4StyleSliceNamesThatAreNotValidIdsTest() { @@ -246,6 +276,21 @@ public void PackageArrowsTest(DependencyType dependencyType, string expected) Assert.Equal(expected + Environment.NewLine, dependency.GetPlantUmlString()); } + [Fact] + public void OneToOneCompactArrowIgnoresDotsTest() + { + // The exporter decides which compact arrows to draw, not the arrow itself. + var dependency = new PlantUmlDependency( + "A.Web", + "A.Billing.Domain", + DependencyType.OneToOneCompact + ); + Assert.Equal( + "[A.Web] --> [A.Billing.Domain]" + Environment.NewLine, + dependency.GetPlantUmlString() + ); + } + [Fact] public void PackageToPackageIfSameParentNamespaceArrowTest() { diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs index 824bece2b..974e9a766 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using ArchUnitNET.Domain; using ArchUnitNET.Domain.PlantUml.Export; @@ -22,12 +23,15 @@ public class PlantUmlSliceDiagramTests private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + private static GenerationOptions Nested => + new GenerationOptions { NestSlicesInPackages = true }; + // --- 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 + // a slice once its depth (dots in its Description) reaches // Slice.CountOfAsteriskInPattern: // // (*) count 1 -> 3 of 9 slices rendered @@ -54,7 +58,7 @@ public Task BuildUmlBySlices_DoubleAsterisk() // 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 + // their nested 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. @@ -76,64 +80,60 @@ public Task BuildUmlBySlices_NonContiguousCaptureGroups() } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SingleAsterisk() + public Task BuildUmlBySlicesNested_SingleAsterisk() { - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)")); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk() + public Task BuildUmlBySlicesNested_DoubleAsterisk() { - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups() + public Task BuildUmlBySlicesNested_MultipleCaptureGroups() { - return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*).(*)") - ); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*).(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups() + public Task BuildUmlBySlicesNested_NonContiguousCaptureGroups() { - return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)..(*)") - ); + return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)..(*)"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart() + public Task BuildUmlBySlicesNested_CaptureGroupAtStart() { - // Nothing precedes the capture group, so there is no namespace to nest the slices - // under and they are drawn flat, like those of Matching. - return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages("(**).Service")); + // Nothing precedes the capture group, so every slice is named after a whole + // namespace, but it is nested where its types are: in that namespace's "Service". + return VerifySlices(SliceRuleDefinition.Slices().Matching("(**).Service"), Nested); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_LimitDependencies() + public Task BuildUmlBySlicesNested_LimitDependencies() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), - new GenerationOptions { LimitDependencies = true } + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, LimitDependencies = true } ); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_C4Style() + public Task BuildUmlBySlicesNested_C4Style() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"), - new GenerationOptions { C4Style = true } + SliceRuleDefinition.Slices().Matching(Root + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, 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. + // Slices that are not nested have no namespace prefix, so this exercises + // PlantUmlSlice.BuildStringC4Style's no-namespace branch. return VerifySlices( SliceRuleDefinition.Slices().Matching(Root + "(**)"), new GenerationOptions { C4Style = true } @@ -143,11 +143,9 @@ public Task BuildUmlBySlices_C4Style() [Fact] public Task BuildUmlBySlicesFocusOn() { - var slices = SortedSlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)") - ); + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); var uml = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slices, Root + "Slice1") + .WithDependenciesFromFocusOn(slices, Root + "Slice1", Nested) .AsString(); return VerifyUml(uml); } @@ -155,13 +153,12 @@ public Task BuildUmlBySlicesFocusOn() [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. + // Slices that are not nested have no namespace, so the focused ones take + // PlantUmlSlice.BuildString's namespace-less branch with a colour set, which renders + // "[Slice1] #99ffd1". The focus string is a namespace, flat or nested. var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); var uml = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slices, "Slice1") + .WithDependenciesFromFocusOn(slices, Root + "Slice1") .AsString(); return VerifyUml(uml); } @@ -179,14 +176,12 @@ public Task BuildUmlBySlices_DirectCircle_RendersCircleDependency() } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle() + public Task BuildUmlBySlicesNested_DirectCircle() { var slices = SortedSlices( - SliceRuleDefinition - .Slices() - .MatchingWithPackages("SlicesTestAssembly.DirectCircle.(*)") + SliceRuleDefinition.Slices().Matching("SlicesTestAssembly.DirectCircle.(*)") ); - var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); return VerifyUml(uml); } @@ -199,23 +194,27 @@ public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle() private const string SameNamedRoot = "SlicesTestAssembly.SameNamedSubnamespaces."; [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages() + public Task BuildUmlBySlicesNested_SameNamedPackages() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)") + SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"), + Nested ); } [Fact] - public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style() + public Task BuildUmlBySlicesNested_SameNamedPackages_C4Style() { return VerifySlices( - SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)"), - new GenerationOptions { C4Style = true } + SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"), + new GenerationOptions { NestSlicesInPackages = true, C4Style = true } ); } - // --- LimitDependencies without namespaces takes the OneToOneCompact branch ---- + // --- LimitDependencies only keeps arrows between slices at the same depth ------ + // + // A slice's depth is that of the namespace all its types share, so Slice2.Service -> + // Slice3 and Slice3.Group1 -> Slice3 are dropped, flat and nested alike. [Fact] public Task BuildUmlBySlices_LimitDependencies_Compact() @@ -272,10 +271,10 @@ public void BuildUmlBySlicesFocusOn_EmptyPackage_Throws() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "", Nested) ); Assert.Equal("Package can't be empty", ex.Message); } @@ -285,10 +284,10 @@ public void BuildUmlBySlicesFocusOn_DotOnlyPackage_Throws() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".", Nested) ); Assert.Equal("Package can't contain a single dot only", ex.Message); } @@ -296,17 +295,32 @@ public void BuildUmlBySlicesFocusOn_DotOnlyPackage_Throws() [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 + "(*)") + .Matching(Root + "(*)") .GetObjects(Architecture); var ex = Assert.Throws(() => - new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage") + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage", Nested) + ); + Assert.Equal("No slice lies in the namespace [NotAPackage]", ex.Message); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_NamespaceIsMatchedBySegments() + { + // The substring "Slice" is contained in every slice name, but no slice lies in a + // namespace called "Slice". + var slices = SliceRuleDefinition + .Slices() + .Matching(Root + "(**)") + .GetObjects(Architecture); + var ex = Assert.Throws(() => + new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, Root + "Slice") + ); + Assert.Equal( + "No slice lies in the namespace [SlicesTestAssembly.MultipleSubnamespaces.Slice]", + ex.Message ); - Assert.Contains("is not contained in this slice", ex.Message); } [Fact] @@ -314,39 +328,186 @@ public void BuildUmlBySlicesFocusOn_TrailingDotPackage_IsTrimmed() { var slicesWithTrailingDot = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var umlWithTrailingDot = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.") + .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.", Nested) .AsString(); var slicesWithoutTrailingDot = SliceRuleDefinition .Slices() - .MatchingWithPackages(Root + "(**)") + .Matching(Root + "(**)") .GetObjects(Architecture); var umlWithoutTrailingDot = new PlantUmlFileBuilder() - .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1") + .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1", Nested) .AsString(); Assert.Equal(umlWithoutTrailingDot, umlWithTrailingDot); } - // --- RemovePatternInappropriateSlices' per-slice "continue" for FocusOn ------ + // --- Which slices FocusOn focuses ------------------------------------------------ + // + // A slice is focused when all of its types lie in the namespace focused on or below + // it, which is a question about the types, so it gets the same answer flat and nested. [Fact] - public Task BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString() + public Task BuildUmlBySlicesFocusOn_SingleAsteriskPattern() { - // 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. + // RemovePatternInappropriateSlices leaves the three slices "(*)" stands for, of + // which "Slice3" lies in the namespace focused on. + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(*)")); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, Root + "Slice3", Nested) + .AsString(); + return VerifyUml(uml); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_SiblingWithSamePrefix_IsNotFocused() + { + // OrdersArchive starts with "Orders", but does not lie in the namespace Orders. It + // is drawn as a neighbour, because it depends on Orders. var slices = SortedSlices( - SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)") + SliceRuleDefinition.Slices().Matching("SlicesTestAssembly.PrefixSiblings.(*)") ); var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, "SlicesTestAssembly.PrefixSiblings.Orders") + .AsString(); + + Assert.Contains("[Orders] #99ffd1", uml); + Assert.Contains("[OrdersArchive]" + Environment.NewLine, uml); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_SliceSpanningTheNamespace_IsANeighbour() + { + // "Services" has types in Slice1 and Slice2, so focusing on Slice1 does not focus it, + // and neither does focusing on Slice3, which it depends on. + var slices = SlicesWithServices(); + var uml = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, Root + "Slice3", Nested) + .AsString(); + + Assert.Contains("[Slice3] as Slice3 #99ffd1", uml); + Assert.Contains("[Services] as Services" + Environment.NewLine, uml); + Assert.Contains("[Services] --|> [Slice3]", uml); + } + + [Fact] + public void BuildUmlBySlicesFocusOn_FocusesTheSameSlicesFlatAndNested() + { + var slices = SlicesWithServices(); + + var flat = new PlantUmlFileBuilder() .WithDependenciesFromFocusOn(slices, Root + "Slice3") .AsString(); - return VerifyUml(uml); + var nested = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn(slices, Root + "Slice3", Nested) + .AsString(); + + Assert.Equal(DrawnSlices(flat), DrawnSlices(nested)); + Assert.Contains("Slice3.Group1 (focused)", DrawnSlices(flat)); + } + + // --- Where nested slices are drawn ----------------------------------------------- + // + // A nested slice sits in the package of the deepest namespace that contains all of its + // types. The packages come from those namespaces, not from the pattern. + + [Fact] + public Task BuildUmlBySlicesNested_DuplicatePrefixSegment() + { + // The leading ".." matches "SlicesTestAssembly.DuplicatePrefix.Sub.", but that is + // not what the package is made of: the slice's type lies in + // "SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub", and there is no + // package ".DuplicatePrefix.Sub". + return VerifySlices( + SliceRuleDefinition.Slices().Matching("..DuplicatePrefix.Sub.(**)"), + Nested + ); + } + + [Fact] + public void BuildUmlBySlicesNested_SliceSitsInItsOwnNamespace() + { + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.Contains( + Lines( + " package \"Slice3\" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 {", + " [Slice3] as Slice3", + " package \"Group1\" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 {", + " [Slice3.Group1] as Slice3.Group1" + ), + uml + ); + } + + [Fact] + public void BuildUmlBySlicesNested_PackageWithASinglePackage_IsMerged() + { + // Orders holds no slice of its own and only the package Domain. + var slices = SortedSlices( + SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)") + ); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.Contains( + "package \"Orders.Domain\" as SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain {", + uml + ); + Assert.DoesNotContain("package \"Orders\"", uml); + } + + [Fact] + public void BuildUmlBySlicesNested_CustomSliceAssignment() + { + var slices = SlicesWithServices(); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.Contains( + Lines( + "package \"SlicesTestAssembly.MultipleSubnamespaces\" as SlicesTestAssembly.MultipleSubnamespaces {", + " [Services] as Services" + ), + uml + ); + } + + [Fact] + public void BuildUmlBySlicesNested_SliceWithoutCommonNamespace_IsDrawnOnItsOwn() + { + var slice1 = Architecture.Types.Single(type => + type.Name == "Slice1Class" && type.Namespace.FullName == Root + "Slice1" + ); + var objectType = Architecture.ReferencedTypes.Single(type => + type.FullName == "System.Object" + ); + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")) + .Concat( + new[] { new Slice(SliceIdentifier.Of("Mixed"), new[] { slice1, objectType }) } + ) + .ToList(); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.Contains(Environment.NewLine + "[Mixed]" + Environment.NewLine, uml); + } + + // --- One set of slices, drawn flat and nested ------------------------------------ + + [Fact] + public void BuildUmlBySlices_SameSlicesDrawnFlatAndNested() + { + var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")); + + var flat = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString(); + var nested = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString(); + + Assert.DoesNotContain("package", flat); + Assert.Contains("[Slice1.Service]", flat); + Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", nested); + Assert.Contains("[Slice1.Service] as Slice1.Service", nested); } /// @@ -363,6 +524,59 @@ private static Slice[] SortedSlices(GivenSlices slices) .ToArray(); } + /// + /// The slices of a custom assignment that gives every namespace below + /// its own slice, except Slice1.Service and Slice2.Service, which form the single slice + /// "Services". + /// + private static Slice[] SlicesWithServices() + { + var assignment = new SliceAssignment( + type => + { + var nameSpace = type.Namespace?.FullName ?? ""; + if (!nameSpace.StartsWith(Root, StringComparison.Ordinal)) + { + return SliceIdentifier.Ignore(); + } + + return nameSpace.EndsWith(".Service", StringComparison.Ordinal) + ? SliceIdentifier.Of("Services") + : SliceIdentifier.Of(nameSpace.Substring(Root.Length)); + }, + "custom" + ); + return assignment + .Apply(Architecture.Types) + .Where(slice => !slice.Identifier.Ignored) + .OrderBy(slice => slice.Description, StringComparer.Ordinal) + .ToArray(); + } + + /// + /// The slices a diagram declares, and whether each one is colored as focused. + /// + private static string[] DrawnSlices(string uml) + { + return uml.Split(new[] { Environment.NewLine }, StringSplitOptions.None) + .Select(line => SliceDeclaration.Match(line)) + .Where(match => match.Success) + .Select(match => + match.Groups["name"].Value + (match.Groups["color"].Success ? " (focused)" : "") + ) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + } + + private static readonly Regex SliceDeclaration = new Regex( + @"^\s*\[(?[^\]]+)\](?: as \S+)?(? #99ffd1)?$" + ); + + private static string Lines(params string[] lines) + { + return string.Join(Environment.NewLine, lines); + } + private static Task VerifyUml(string uml) { return Verifier diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesLimitDependenciesSnapshotTest.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesLimitDependenciesSnapshotTest.verified.txt new file mode 100644 index 000000000..8d034160f --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesLimitDependenciesSnapshotTest.verified.txt @@ -0,0 +1,33 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +namespace SlicesTestAssembly { +} +namespace SlicesTestAssembly.MultipleSubnamespaces { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner { +} +[SlicesTestAssembly.MultipleSubnamespaces.Slice3] --> [SlicesTestAssembly.MultipleSubnamespaces.Slice1] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2] --> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --> [SlicesTestAssembly.MultipleSubnamespaces.Slice2] +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.SliceNamesThatArePackageIdsTest.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.SliceNamesThatArePackageIdsTest.verified.txt new file mode 100644 index 000000000..041c19d94 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.SliceNamesThatArePackageIdsTest.verified.txt @@ -0,0 +1,17 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "A" as A { + package "X" as A.X { + [A.X] as A.X_2 + package "Y" as A.X.Y { + [A.X.Y] as A.X.Y_2 + } + } +} + +[A.X.Y_2] --|> [A.X_2] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt index 28668d82b..86cc1d774 100644 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn.verified.txt @@ -5,17 +5,24 @@ HIDE_STEREOTYPE() package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 #99ffd1 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service #99ffd1 + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 #99ffd1 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [Slice1.Service] as Slice1.Service #99ffd1 + } } package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service + [Slice2] as Slice2 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [Slice2.Service] as Slice2.Service + } } package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { - label " " as SlicesTestAssembly.MultipleSubnamespaces.Slice3.__empty__ + [Slice3] as Slice3 } } -[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -SlicesTestAssembly.MultipleSubnamespaces.Slice3 -[#green]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 +[Slice1] --|> [Slice2] +[Slice1.Service] --|> [Slice2.Service] +[Slice3] --|> [Slice1] @enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern.verified.txt new file mode 100644 index 000000000..4f22ba744 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern.verified.txt @@ -0,0 +1,21 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 + } + package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { + [Slice2] as Slice2 + } + package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 #99ffd1 + } +} + +[Slice2] --|> [Slice3] +[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 deleted file mode 100644 index 37d98cbf3..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocusString.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { - label " " as SlicesTestAssembly.MultipleSubnamespaces.Slice1.__empty__ - } - package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service - } - [Slice3] as SlicesTestAssembly.MultipleSubnamespaces.Slice3 #99ffd1 -} - -[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] -[SlicesTestAssembly.MultipleSubnamespaces.Slice3] -[#red]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt deleted file mode 100644 index b042a3dba..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt +++ /dev/null @@ -1,27 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { - Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice1, Slice1) { - Container(SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service, Service) - } - Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice2, Slice2) { - Container(SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service, Service) - } - Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3, Slice3) { - Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1, Group1) { - Container(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner, Inner) - } - Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2, Group2) { - Container(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner, Inner) - } - } -} - -[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> SlicesTestAssembly.MultipleSubnamespaces.Slice3 -SlicesTestAssembly.MultipleSubnamespaces.Slice3 -[#green]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt deleted file mode 100644 index 3e4dd4a96..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt +++ /dev/null @@ -1,11 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -[SlicesTestAssembly.DotDotSemantics.Alpha] -[SlicesTestAssembly.MultipleSubnamespaces.Slice1] -[SlicesTestAssembly.MultipleSubnamespaces.Slice2] -[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2] -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt deleted file mode 100644 index 882f21891..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt +++ /dev/null @@ -1,27 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service - } - package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service - } - package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { - package "Group1" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { - [Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner - } - package "Group2" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { - [Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner - } - } -} - -[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> SlicesTestAssembly.MultipleSubnamespaces.Slice3 -SlicesTestAssembly.MultipleSubnamespaces.Slice3 -[#green]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt deleted file mode 100644 index 58bf4a94f..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service - } - package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service - } - package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { - [Group1] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 - [Group2] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 - } -} - -[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> SlicesTestAssembly.MultipleSubnamespaces.Slice3 -SlicesTestAssembly.MultipleSubnamespaces.Slice3 -[#green]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt deleted file mode 100644 index 58bf4a94f..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service - } - package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service - } - package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { - [Group1] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 - [Group2] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 - } -} - -[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] -[#red]> SlicesTestAssembly.MultipleSubnamespaces.Slice3 -SlicesTestAssembly.MultipleSubnamespaces.Slice3 -[#green]> SlicesTestAssembly.MultipleSubnamespaces.Slice1 -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt deleted file mode 100644 index 5338f9f99..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.SameNamedSubnamespaces" as SlicesTestAssembly.SameNamedSubnamespaces { - package "Billing" as SlicesTestAssembly.SameNamedSubnamespaces.Billing { - package "Domain" as SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain { - [Model] as SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model - } - } - package "Orders" as SlicesTestAssembly.SameNamedSubnamespaces.Orders { - package "Domain" as SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain { - [Model] as SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model - } - } - [Web] as SlicesTestAssembly.SameNamedSubnamespaces.Web -} - -[SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model] --|> [SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model] -[SlicesTestAssembly.SameNamedSubnamespaces.Web] -[#red]> SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt deleted file mode 100644 index 4d3dc2e23..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -Boundary(SlicesTestAssembly.SameNamedSubnamespaces, SlicesTestAssembly.SameNamedSubnamespaces) { - Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Billing, Billing) { - Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain, Domain) { - Container(SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model, Model) - } - } - Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Orders, Orders) { - Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain, Domain) { - Container(SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model, Model) - } - } - Container(SlicesTestAssembly.SameNamedSubnamespaces.Web, Web) -} - -[SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model] --|> [SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model] -[SlicesTestAssembly.SameNamedSubnamespaces.Web] -[#red]> SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain -@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt deleted file mode 100644 index 63fde3d69..000000000 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt +++ /dev/null @@ -1,16 +0,0 @@ -@startuml - -!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml - -HIDE_STEREOTYPE() - -package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { - [Slice1] as SlicesTestAssembly.MultipleSubnamespaces.Slice1 - [Slice2] as SlicesTestAssembly.MultipleSubnamespaces.Slice2 - [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.BuildUmlBySlicesNested_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt new file mode 100644 index 000000000..7acc06996 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt @@ -0,0 +1,43 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +Boundary(SlicesTestAssembly.MultipleSubnamespaces, SlicesTestAssembly.MultipleSubnamespaces) { + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice1, Slice1) { + Container(Slice1, Slice1) + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service, Service) { + Container(Slice1.Service, Slice1.Service) + } + } + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice2, Slice2) { + Container(Slice2, Slice2) + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service, Service) { + Container(Slice2.Service, Slice2.Service) + } + } + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3, Slice3) { + Container(Slice3, Slice3) + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1, Group1) { + Container(Slice3.Group1, Slice3.Group1) + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner, Inner) { + Container(Slice3.Group1.Inner, Slice3.Group1.Inner) + } + } + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2, Group2) { + Container(Slice3.Group2, Slice3.Group2) + Boundary(SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner, Inner) { + 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.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt new file mode 100644 index 000000000..eeb8ee985 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt @@ -0,0 +1,22 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly" as SlicesTestAssembly { + package "DotDotSemantics.Alpha.Service" as SlicesTestAssembly.DotDotSemantics.Alpha.Service { + [SlicesTestAssembly.DotDotSemantics.Alpha] as SlicesTestAssembly.DotDotSemantics.Alpha + } + package "MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1.Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [SlicesTestAssembly.MultipleSubnamespaces.Slice1] as SlicesTestAssembly.MultipleSubnamespaces.Slice1 + } + package "Slice2.Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [SlicesTestAssembly.MultipleSubnamespaces.Slice2] as SlicesTestAssembly.MultipleSubnamespaces.Slice2 + } + } +} + +[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt similarity index 51% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt index 1202286f3..08df7321b 100644 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt @@ -5,9 +5,13 @@ HIDE_STEREOTYPE() package "SlicesTestAssembly.DirectCircle" as SlicesTestAssembly.DirectCircle { - [Slice1] as SlicesTestAssembly.DirectCircle.Slice1 - [Slice2] as SlicesTestAssembly.DirectCircle.Slice2 + package "Slice1" as SlicesTestAssembly.DirectCircle.Slice1 { + [Slice1] as Slice1 + } + package "Slice2" as SlicesTestAssembly.DirectCircle.Slice2 { + [Slice2] as Slice2 + } } -[SlicesTestAssembly.DirectCircle.Slice1] <-[#red]> [SlicesTestAssembly.DirectCircle.Slice2] +[Slice1] <-[#red]> [Slice2] @enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt new file mode 100644 index 000000000..da111c5a5 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt @@ -0,0 +1,43 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [Slice1.Service] as Slice1.Service + } + } + package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { + [Slice2] as Slice2 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [Slice2.Service] as Slice2.Service + } + } + package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 + package "Group1" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { + [Slice3.Group1] as Slice3.Group1 + package "Inner" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner { + [Slice3.Group1.Inner] as Slice3.Group1.Inner + } + } + package "Group2" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { + [Slice3.Group2] as Slice3.Group2 + package "Inner" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner { + [Slice3.Group2.Inner] as 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.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt new file mode 100644 index 000000000..4da60c5df --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt @@ -0,0 +1,11 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub" as SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub { + [DuplicatePrefix.Sub] as DuplicatePrefix.Sub +} + +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt new file mode 100644 index 000000000..7f821f149 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt @@ -0,0 +1,41 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [Slice1.Service] as Slice1.Service + } + } + package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { + [Slice2] as Slice2 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [Slice2.Service] as Slice2.Service + } + } + package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 + package "Group1" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { + [Slice3.Group1] as Slice3.Group1 + package "Inner" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner { + [Slice3.Group1.Inner] as Slice3.Group1.Inner + } + } + package "Group2" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { + [Slice3.Group2] as Slice3.Group2 + package "Inner" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner { + [Slice3.Group2.Inner] as Slice3.Group2.Inner + } + } + } +} + +[Slice1] --> [Slice2] +[Slice1.Service] --> [Slice2.Service] +[Slice2] --> [Slice3] +[Slice3] --> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt similarity index 51% rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt index a050a9c2d..0b106110b 100644 --- a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt @@ -6,22 +6,32 @@ HIDE_STEREOTYPE() package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service + [Slice1] as Slice1 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [Slice1.Service] as Slice1.Service + } } package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { - [Service] as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service + [Slice2] as Slice2 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [Slice2.Service] as Slice2.Service + } } package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 package "Group1" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { - [Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner + [Slice3.Group1] as Slice3.Group1 } package "Group2" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { - [Inner] as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner + [Slice3.Group2] as Slice3.Group2 } } } -SlicesTestAssembly.MultipleSubnamespaces.Slice1 ..> SlicesTestAssembly.MultipleSubnamespaces.Slice2 -SlicesTestAssembly.MultipleSubnamespaces.Slice2 ..> SlicesTestAssembly.MultipleSubnamespaces.Slice3 -SlicesTestAssembly.MultipleSubnamespaces.Slice3 ..> SlicesTestAssembly.MultipleSubnamespaces.Slice1 +[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.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt new file mode 100644 index 000000000..0b106110b --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt @@ -0,0 +1,37 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.MultipleSubnamespaces" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { + [Slice1.Service] as Slice1.Service + } + } + package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { + [Slice2] as Slice2 + package "Service" as SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { + [Slice2.Service] as Slice2.Service + } + } + package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 + package "Group1" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { + [Slice3.Group1] as Slice3.Group1 + } + package "Group2" as SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { + [Slice3.Group2] as 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.BuildUmlBySlicesNested_SameNamedPackages.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt new file mode 100644 index 000000000..b60093fe6 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt @@ -0,0 +1,28 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +package "SlicesTestAssembly.SameNamedSubnamespaces" as SlicesTestAssembly.SameNamedSubnamespaces { + package "Billing.Domain" as SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain { + [Billing.Domain] as Billing.Domain + package "Model" as SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model { + [Billing.Domain.Model] as Billing.Domain.Model + } + } + package "Orders.Domain" as SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain { + [Orders.Domain] as Orders.Domain + package "Model" as SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model { + [Orders.Domain.Model] as Orders.Domain.Model + } + } + package "Web" as SlicesTestAssembly.SameNamedSubnamespaces.Web { + [Web] as Web + } +} + +[Orders.Domain] --|> [Billing.Domain] +[Orders.Domain.Model] --|> [Billing.Domain.Model] +[Web] --|> [Billing.Domain] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt new file mode 100644 index 000000000..69f5c7906 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt @@ -0,0 +1,28 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +Boundary(SlicesTestAssembly.SameNamedSubnamespaces, SlicesTestAssembly.SameNamedSubnamespaces) { + Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain, Billing.Domain) { + Container(Billing.Domain, Billing.Domain) + Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Billing.Domain.Model, Model) { + Container(Billing.Domain.Model, Billing.Domain.Model) + } + } + Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain, Orders.Domain) { + Container(Orders.Domain, Orders.Domain) + Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Orders.Domain.Model, Model) { + Container(Orders.Domain.Model, Orders.Domain.Model) + } + } + Boundary(SlicesTestAssembly.SameNamedSubnamespaces.Web, Web) { + Container(Web, Web) + } +} + +[Orders.Domain] --|> [Billing.Domain] +[Orders.Domain.Model] --|> [Billing.Domain.Model] +[Web] --|> [Billing.Domain] +@enduml diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt new file mode 100644 index 000000000..393c2fb54 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_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" as SlicesTestAssembly.MultipleSubnamespaces { + package "Slice1" as SlicesTestAssembly.MultipleSubnamespaces.Slice1 { + [Slice1] as Slice1 + } + package "Slice2" as SlicesTestAssembly.MultipleSubnamespaces.Slice2 { + [Slice2] as Slice2 + } + package "Slice3" as SlicesTestAssembly.MultipleSubnamespaces.Slice3 { + [Slice3] as Slice3 + } +} + +[Slice1] --|> [Slice2] +[Slice2] --|> [Slice3] +[Slice3] --|> [Slice1] +@enduml diff --git a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs index 033fd5683..a18ebfeb7 100644 --- a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs +++ b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs @@ -45,11 +45,14 @@ public void ComponentDiagramFromSlicesTest() Assert.NotEmpty(uml2); var arch1 = new ArchLoader().LoadAssembly(typeof(Architecture).Assembly).Build(); - var sliceRule1 = SliceRuleDefinition - .Slices() - .MatchingWithPackages("ArchUnitNET.(*).(*).(*)"); + var sliceRule1 = SliceRuleDefinition.Slices().Matching("ArchUnitNET.(*).(*).(*)"); const string path = "../../../Fluent/PlantUml/Test.puml"; - var g = new GenerationOptions() { C4Style = true, LimitDependencies = true }; + var g = new GenerationOptions() + { + C4Style = true, + LimitDependencies = true, + NestSlicesInPackages = true, + }; ComponentDiagram() .WithDependenciesFromSlices(sliceRule1.GetObjects(arch1), g) .WriteToFile(path); diff --git a/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs new file mode 100644 index 000000000..7230472fc --- /dev/null +++ b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs @@ -0,0 +1,192 @@ +using System; +using System.Linq; +using ArchUnitNET.Domain; +using ArchUnitNET.Domain.PlantUml.Export; +using ArchUnitNET.Fluent; +using ArchUnitNET.Fluent.Slices; +using Xunit; + +// These tests cover the obsolete method itself. +#pragma warning disable CS0618 + +namespace ArchUnitNETTests.Fluent.Slices +{ + /// + /// is obsolete. It creates exactly the + /// slices creates, so its rules behave the same, + /// and the only thing it still does differently is nest its slices in diagrams by default. + /// + public class MatchingWithPackagesTests + { + private static Architecture Architecture => StaticTestArchitectures.SlicesTestArchitecture; + + private const string Root = "SlicesTestAssembly.MultipleSubnamespaces."; + + public static TheoryData Patterns => + new TheoryData + { + Root + "(*)", + Root + "(**)", + Root + "(**)..", + Root + "(*).(*)", + Root + "(*)..(*)", + "..DuplicatePrefix.Sub.(**)", + "(**).Service", + "SlicesTestAssembly.SubnamespaceCircle.(**)", + }; + + [Theory] + [MemberData(nameof(Patterns))] + public void Slices_AreThoseOfMatching(string pattern) + { + var withPackages = SliceRuleDefinition + .Slices() + .MatchingWithPackages(pattern) + .GetObjects(Architecture) + .ToList(); + var matching = SliceRuleDefinition.Slices().Matching(pattern).GetObjects(Architecture); + + Assert.Equal(matching.Select(Describe), withPackages.Select(Describe)); + } + + [Theory] + [MemberData(nameof(Patterns))] + public void Rules_EvaluateLikeThoseOfMatching(string pattern) + { + AssertSameResults( + SliceRuleDefinition.Slices().Matching(pattern).Should().BeFreeOfCycles(), + SliceRuleDefinition.Slices().MatchingWithPackages(pattern).Should().BeFreeOfCycles() + ); + AssertSameResults( + SliceRuleDefinition.Slices().Matching(pattern).Should().NotDependOnEachOther(), + SliceRuleDefinition + .Slices() + .MatchingWithPackages(pattern) + .Should() + .NotDependOnEachOther() + ); + } + + [Fact] + public void BeFreeOfCycles_DetectsCycle() + { + Assert.False( + SliceRuleDefinition + .Slices() + .MatchingWithPackages(Root + "(**)") + .Should() + .BeFreeOfCycles() + .HasNoViolations(Architecture) + ); + } + + // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no + // cycle, so an empty graph is not what makes this pass. + [Fact] + public void BeFreeOfCycles_WhenAcyclic_Passes() + { + Assert.True( + SliceRuleDefinition + .Slices() + .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)") + .Should() + .BeFreeOfCycles() + .HasNoViolations(Architecture) + ); + } + + [Theory] + [MemberData(nameof(Patterns))] + public void Diagram_IsThatOfMatchingNestedInPackages(string pattern) + { + foreach ( + var options in new[] + { + new GenerationOptions(), + new GenerationOptions { C4Style = true }, + new GenerationOptions { LimitDependencies = true }, + } + ) + { + var expected = new PlantUmlFileBuilder() + .WithDependenciesFrom( + SortedSlices(SliceRuleDefinition.Slices().Matching(pattern)), + new GenerationOptions + { + NestSlicesInPackages = true, + C4Style = options.C4Style, + LimitDependencies = options.LimitDependencies, + } + ) + .AsString(); + var actual = new PlantUmlFileBuilder() + .WithDependenciesFrom( + SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(pattern)), + options + ) + .AsString(); + + Assert.Equal(expected, actual); + } + } + + [Fact] + public void Diagram_FocusOn_IsThatOfMatchingNestedInPackages() + { + var expected = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn( + SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")), + Root + "Slice1", + new GenerationOptions { NestSlicesInPackages = true } + ) + .AsString(); + var actual = new PlantUmlFileBuilder() + .WithDependenciesFromFocusOn( + SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")), + Root + "Slice1" + ) + .AsString(); + + Assert.Equal(expected, actual); + Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", actual); + } + + private static string Describe(Slice slice) + { + return slice.Description + + ": " + + string.Join( + ", ", + slice + .Types.Select(type => type.FullName) + .OrderBy(n => n, StringComparer.Ordinal) + ); + } + + private static void AssertSameResults(IArchRule matching, IArchRule withPackages) + { + Assert.Equal(matching.Description, withPackages.Description); + Assert.Equal( + matching.Evaluate(Architecture).Select(Summarize), + withPackages.Evaluate(Architecture).Select(Summarize) + ); + } + + private static string Summarize(EvaluationResult result) + { + return result.Passed + + " " + + result.EvaluatedObjectIdentifier.Identifier + + " " + + result.Description; + } + + private static Slice[] SortedSlices(GivenSlices slices) + { + return slices + .GetObjects(Architecture) + .OrderBy(slice => slice.Description, StringComparer.Ordinal) + .ToArray(); + } + } +} diff --git a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs index 143d03af5..232cb4a2b 100644 --- a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs +++ b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs @@ -27,48 +27,6 @@ public void CycleDetectionTest() .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() { @@ -305,21 +263,20 @@ public void Matching_LeadingDotDot_CapturesEveryDepth() // (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. + // "DuplicatePrefix.Sub" 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() + public void DuplicatePrefixSegment_CapturesAfterTheFirstOccurrenceOfThePrefix() { var slices = SliceRuleDefinition .Slices() - .MatchingWithPackages("..DuplicatePrefix.Sub.(*)") + .Matching("..DuplicatePrefix.Sub.(**)") .GetObjects(StaticTestArchitectures.SlicesTestArchitecture) .ToList(); Assert.Single(slices); - Assert.Equal(".DuplicatePrefix.Sub.DuplicatePrefix.Sub", slices[0].Description); + Assert.Equal("DuplicatePrefix.Sub", slices[0].Description); } // --- ".." semantics ------------------------------------------------------------- diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt deleted file mode 100644 index ae856ef18..000000000 --- a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -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/TestAssemblies/SlicesTestAssembly/PrefixSiblings/Orders/OrdersClass.cs b/TestAssemblies/SlicesTestAssembly/PrefixSiblings/Orders/OrdersClass.cs new file mode 100644 index 000000000..bee6eea29 --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/PrefixSiblings/Orders/OrdersClass.cs @@ -0,0 +1,4 @@ +namespace SlicesTestAssembly.PrefixSiblings.Orders; + +// "Orders" is the start of its sibling "OrdersArchive", but not one of its namespace segments. +public class OrdersClass { } diff --git a/TestAssemblies/SlicesTestAssembly/PrefixSiblings/OrdersArchive/OrdersArchiveClass.cs b/TestAssemblies/SlicesTestAssembly/PrefixSiblings/OrdersArchive/OrdersArchiveClass.cs new file mode 100644 index 000000000..500d6043c --- /dev/null +++ b/TestAssemblies/SlicesTestAssembly/PrefixSiblings/OrdersArchive/OrdersArchiveClass.cs @@ -0,0 +1,8 @@ +using SlicesTestAssembly.PrefixSiblings.Orders; + +namespace SlicesTestAssembly.PrefixSiblings.OrdersArchive; + +public class OrdersArchiveClass +{ + public OrdersClass Reference = null!; +} diff --git a/documentation/docs/guide.md b/documentation/docs/guide.md index 022bf60d9..ed193526d 100644 --- a/documentation/docs/guide.md +++ b/documentation/docs/guide.md @@ -239,28 +239,29 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb ``` ### 5.2 Structured diagram dependencies -On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only with the modified creation of SliceRule. +On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only in the `NestSlicesInPackages` option, which draws the namespaces as `package` blocks and each slice in the package of the deepest namespace that contains all of its types. A package that holds no slice and only one other package is merged with it, e.g. into `Orders.Domain`. A slice whose types share no namespace is drawn outside any package. This applies to every slice, also to those of `SlicedBy` and of custom assignments. ![Diagram](diagrams/archUnitNet_all_withPackages.svg) ```cs string pattern = "ArchUnitNET.(**)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true}; string path = "diagram.puml"; -PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch)).WriteToFile(path); +PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), g).WriteToFile(path); ``` ### 5.3 LimitDependencies-mod -The previous case still shows a large number of connections. For maximum simplification, as well as demonstrating the overall picture, it makes sense to reformat it into a limited-dependencies version. This kind of display shows dependencies between packages at the same slice level. +The previous case still shows a large number of connections. For maximum simplification, as well as demonstrating the overall picture, it makes sense to reformat it into a limited-dependencies version. This kind of display shows only dependencies between slices at the same depth, i.e. whose types share an equally deep namespace, nested or not. ![Diagram](diagrams/archUnitNet_all_compact.svg) ```cs string pattern = "ArchUnitNET.(**)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); -GenerationOptions g = new GenerationOptions(){LimitDependencies = true}; +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, LimitDependencies = true}; string path = "diagram.puml"; @@ -298,9 +299,9 @@ To enable an C4plantUML-style view of diagrams, set in the GenerationOptions fla ![Diagram](diagrams/archUnitNet_three_alternative.svg) ```cs string pattern = "ArchUnitNET.(*).(*).(*)"; -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); -GenerationOptions g = new GenerationOptions(){C4Style = true}; +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, C4Style = true}; string path = "diagram.puml"; @@ -308,20 +309,32 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb ``` ### 5.6 Focus-mod -Focus mod allows you to show all dependencies on the selected package or out of the package. +Focus mod allows you to show all dependencies on the selected package or out of the package. The package to focus on is a namespace. A slice is focused when all of its types lie in it, flat or nested; slices that depend on it or that it depends on are drawn as well. ![Diagram](diagrams/archUnitNet_focusOn.svg) ```cs string pattern = "ArchUnitNET.(**)"; -string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements" -GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern); +string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements"; +GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern); Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build(); +GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true}; string path = "diagram.puml"; -PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage).WriteToFile(path); +PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage, g).WriteToFile(path); ``` +### 5.7 Migrating from MatchingWithPackages +`MatchingWithPackages` is obsolete and will be removed in a future release. It creates the same slices +as `Matching`, with the same names and violations, and only nests them in diagrams. Replace +`MatchingWithPackages(pattern)` with `Matching(pattern)` and draw the slices with +`new GenerationOptions { NestSlicesInPackages = true }`. Diagrams change too: a slice is a box +inside its namespace instead of being drawn as that namespace. + +Failure messages of former `MatchingWithPackages` rules no longer start a slice's name with the +namespace before the first capture group, and their frozen violations, which were stored under those +longer names, have to be frozen again once. + ## 6. Further Reading and Examples A complete overview of all available methods can be found [here](additional.md).