diff --git a/ArchUnitNET/Fluent/Syntax/Elements/Types/TypeConditionsDefinition.cs b/ArchUnitNET/Fluent/Syntax/Elements/Types/TypeConditionsDefinition.cs index d75ca4f76..ae0018484 100644 --- a/ArchUnitNET/Fluent/Syntax/Elements/Types/TypeConditionsDefinition.cs +++ b/ArchUnitNET/Fluent/Syntax/Elements/Types/TypeConditionsDefinition.cs @@ -348,13 +348,19 @@ ConditionResult Condition(TRuleType ruleType) .Concat(classDiagramAssociation.GetTargetNamespaceIdentifiers(ruleType)) .ToList()); + var pass = true; var dynamicFailDescription = "does depend on"; - foreach (var dependency in ruleType.GetTypeDependencies()) + + //Prevent failDescriptions like "does depend on X and does depend on X and does depend on Y and does depend on Y + var ruleTypeDependencies = ruleType.GetTypeDependencies().GroupBy(p => p.FullName).Select(g => g.First()); + foreach (var dependency in ruleTypeDependencies) { - if (classDiagramAssociation.Contains(dependency) && !allAllowedTargets.Any(pattern => dependency.FullNameMatches(pattern, true))) + if (classDiagramAssociation.Contains(dependency) + && !allAllowedTargets.Any(pattern => dependency.FullNameMatches(pattern, true))) { dynamicFailDescription += pass ? " " + dependency.FullName : " and " + dependency.FullName; + pass = false; } } diff --git a/ArchUnitNETTests/ArchUnitNETTests.csproj b/ArchUnitNETTests/ArchUnitNETTests.csproj index 4ac977511..fce6557f1 100644 --- a/ArchUnitNETTests/ArchUnitNETTests.csproj +++ b/ArchUnitNETTests/ArchUnitNETTests.csproj @@ -8,14 +8,20 @@ - - + + - - - + + + + + + + + PreserveNewest + diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlErrorMessagesCheck.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlErrorMessagesCheck.cs new file mode 100644 index 000000000..787d4c372 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlErrorMessagesCheck.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ArchUnitNET.Domain; +using ArchUnitNET.Fluent; +using ArchUnitNET.xUnit; +using Xunit; +using static ArchUnitNET.Fluent.ArchRuleDefinition; + +namespace ArchUnitNETTests.Domain.PlantUml +{ + public class PlantUmlErrorMessagesCheck + { + private static readonly Architecture Architecture = StaticTestArchitectures.ArchUnitNETTestAssemblyArchitecture; + private readonly string _umlFile; + + public PlantUmlErrorMessagesCheck() + { + _umlFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Domain", "PlantUml", + "zzz_test_version_with_errors.puml"); + } + + [Fact] + public void NoDuplicatesInErrorMessageTest() + { + var testPassed = CheckByPuml(out var rawErrormessage); + Assert.False(testPassed); + + //CheckForDuplicates returns false when errormessage contains duplicates or is empty + var containsNoDuplicates = ContainsNoDuplicates(rawErrormessage, out var explainErrormessage); + + var errormessage = "\nOriginal (ArchUnitNet) Exception:\n" + rawErrormessage + + "\n\nAssert Error:\n" + explainErrormessage + "\n"; + + Assert.True(containsNoDuplicates, errormessage); + } + + private bool CheckByPuml(out string errormessage) + { + errormessage = null; + + try + { + IArchRule adhereToPlantUmlDiagram = Types().Should().AdhereToPlantUmlDiagram(_umlFile); + adhereToPlantUmlDiagram.Check(Architecture); + } + //xUnit + catch (FailedArchRuleException exception) + { + errormessage = exception.Message; + + return false; + } + + return true; + } + + private static bool ContainsNoDuplicates(string uncutMessage, out string errormessage) + { + if (string.IsNullOrWhiteSpace(uncutMessage)) + { + errormessage = "Error message is empty."; + return false; + } + + var sources = new List(); + + var errors = uncutMessage.Split('\n').Skip(1); + + foreach (var error in errors.Where(e => !string.IsNullOrWhiteSpace(e))) + { + var splitError = error.Split(" does depend on "); + var source = splitError[0].Trim(); + var targets = splitError[1].Trim().Split(" and "); + if (sources.Contains(source)) + { + errormessage = $"Two errors with {source} as source found."; + return false; + } + + sources.Add(source); + if (targets.Distinct().Count() < targets.Length) + { + errormessage = $"The error \"{error}\" contains duplicate targets."; + return false; + } + } + + errormessage = "No duplicates found."; + return true; + } + } +} \ No newline at end of file diff --git a/ArchUnitNETTests/Domain/PlantUml/zzz_test_version_with_errors.puml b/ArchUnitNETTests/Domain/PlantUml/zzz_test_version_with_errors.puml new file mode 100644 index 000000000..f585911e7 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/zzz_test_version_with_errors.puml @@ -0,0 +1,50 @@ +@startuml + + +' !!! Test Version !!! +' This PlantUML Diagram will cause errors when you run the ArchUnitNet PLantUML Test +' It is made to check the errormessages that you will get when the codebase architecture does not match the architecture of the PUML +' Unlogical or wrong dependencies are made on purpose + +skinparam componentStyle uml2 +skinparam component { + BorderColor #grey + BackgroundColor #white +} + +[Addresses] <> +[Customers] <> +[Orders] <> +[Products] <> +[Product Catalog] <> as catalog +[Product Import] <> as import + +' Could be some random comment +[XML] <> <> as xml + +'Causes Error +[Addresses] <-[#blue]- catalog + +'Causes Error +[Orders] <-[#blue]- [Customers] : is placed by +[Orders] --> [Products] +[Orders] --> [Addresses] + +'Causes Error +[Customers] <-[#blue]- [Addresses] +[Customers] --> [Products] + + +[Products] <--[#green]- catalog + +'Causes Error +catalog <-[#blue]- [Orders] + +import -left-> catalog : parse products +import --> xml +'Causes Error +import <-[#blue]- [Customers] + +note top on link #lightgreen: is responsible for translating XML to csharp classes + +@enduml \ No newline at end of file diff --git a/TestAssembly/PlantUml/Addresses/Address.cs b/TestAssembly/PlantUml/Addresses/Address.cs new file mode 100644 index 000000000..4db7a327b --- /dev/null +++ b/TestAssembly/PlantUml/Addresses/Address.cs @@ -0,0 +1,11 @@ +using TestAssembly.PlantUml.Catalog; + +namespace TestAssembly.PlantUml.Addresses +{ + public class Address + { +#pragma warning disable CS0169 + private ProductCatalog productCatalog; +#pragma warning restore CS0169 + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Catalog/ProductCatalog.cs b/TestAssembly/PlantUml/Catalog/ProductCatalog.cs new file mode 100644 index 000000000..56642c9de --- /dev/null +++ b/TestAssembly/PlantUml/Catalog/ProductCatalog.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using TestAssembly.PlantUml.Orders; +using TestAssembly.PlantUml.Products; + +namespace TestAssembly.PlantUml.Catalog +{ + public class ProductCatalog + { + private readonly List _allProducts = new List(); + + internal void GonnaDoSomethingIllegalWithOrder() + { + var order = new Order(); + foreach (var product in _allProducts) + { + product.Register(); + } + + order.AddProducts(_allProducts); + } + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Customers/Customer.cs b/TestAssembly/PlantUml/Customers/Customer.cs new file mode 100644 index 000000000..efd00e015 --- /dev/null +++ b/TestAssembly/PlantUml/Customers/Customer.cs @@ -0,0 +1,15 @@ +using TestAssembly.PlantUml.Addresses; +using TestAssembly.PlantUml.Orders; + +namespace TestAssembly.PlantUml.Customers +{ + public class Customer + { + public Address Address { get; set; } + + internal void AddOrder(Order order) + { + // simply having such a parameter violates the specified UML diagram + } + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Importer/ProductImport.cs b/TestAssembly/PlantUml/Importer/ProductImport.cs new file mode 100644 index 000000000..16f19d413 --- /dev/null +++ b/TestAssembly/PlantUml/Importer/ProductImport.cs @@ -0,0 +1,21 @@ +using TestAssembly.PlantUml.Catalog; +using TestAssembly.PlantUml.Customers; +using TestAssembly.PlantUml.Xml.Processor; +using TestAssembly.PlantUml.Xml.Types; + +namespace TestAssembly.PlantUml.Importer +{ + public class ProductImport + { + public ProductCatalog productCatalog; + public XmlTypes xmlType; + public XmlProcessor xmlProcessor; + + public Customer Customer => new Customer(); // violates diagram -> product import may not directly know Customer + + private ProductCatalog Parse(byte[] xml) + { + return new ProductCatalog(); + } + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Orders/Order.cs b/TestAssembly/PlantUml/Orders/Order.cs new file mode 100644 index 000000000..f13a0bf16 --- /dev/null +++ b/TestAssembly/PlantUml/Orders/Order.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using TestAssembly.PlantUml.Addresses; +using TestAssembly.PlantUml.Customers; +using TestAssembly.PlantUml.Products; + +namespace TestAssembly.PlantUml.Orders +{ + public class Order + { + public Customer _customer; + private readonly List _products = new List(); + + public void AddProducts(IList products) + { + _products.AddRange(products); + } + + internal void Report() + { + Report(_customer.Address); + foreach (var product in _products) + { + product.Report(); + } + } + + private void Report(Address address) + { + } + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Products/Product.cs b/TestAssembly/PlantUml/Products/Product.cs new file mode 100644 index 000000000..1a8074e1a --- /dev/null +++ b/TestAssembly/PlantUml/Products/Product.cs @@ -0,0 +1,21 @@ +using TestAssembly.PlantUml.Customers; +using TestAssembly.PlantUml.Orders; + +namespace TestAssembly.PlantUml.Products +{ + public class Product + { + public Customer _customer; + + internal Order Order => null; // the return type violates the specified UML diagram + + + public void Register() + { + } + + public void Report() + { + } + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Xml/Processor/XmlProcessor.cs b/TestAssembly/PlantUml/Xml/Processor/XmlProcessor.cs new file mode 100644 index 000000000..d3a3648dc --- /dev/null +++ b/TestAssembly/PlantUml/Xml/Processor/XmlProcessor.cs @@ -0,0 +1,6 @@ +namespace TestAssembly.PlantUml.Xml.Processor +{ + public class XmlProcessor + { + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Xml/Types/XmlTypes.cs b/TestAssembly/PlantUml/Xml/Types/XmlTypes.cs new file mode 100644 index 000000000..8575a8ce5 --- /dev/null +++ b/TestAssembly/PlantUml/Xml/Types/XmlTypes.cs @@ -0,0 +1,6 @@ +namespace TestAssembly.PlantUml.Xml.Types +{ + public class XmlTypes + { + } +} \ No newline at end of file diff --git a/TestAssembly/PlantUml/Xml/Utils/XmlUtils.cs b/TestAssembly/PlantUml/Xml/Utils/XmlUtils.cs new file mode 100644 index 000000000..6ce6f4988 --- /dev/null +++ b/TestAssembly/PlantUml/Xml/Utils/XmlUtils.cs @@ -0,0 +1,6 @@ +namespace TestAssembly.PlantUml.Xml.Utils +{ + public class XmlUtils + { + } +} \ No newline at end of file