diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index f3ba24f65..a62cf04a1 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -116,7 +116,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Resources.Management. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tests", "src\Tests\Moryx.Runtime.Endpoints.Tests\Moryx.Runtime.Endpoints.Tests.csproj", "{7792C4E0-6D07-42C9-AC29-BAB76836FC11}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -296,6 +300,14 @@ Global {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.Build.0 = Release|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Release|Any CPU.Build.0 = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,10 +352,12 @@ Global {FEB3BA44-2CD9-445A-ABF2-C92378C443F7} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {7792C4E0-6D07-42C9-AC29-BAB76836FC11} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6} + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} + {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} - RESX_TaskErrorCategory = Message RESX_ShowErrorsInErrorList = True + RESX_TaskErrorCategory = Message + SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} EndGlobalSection EndGlobal diff --git a/VERSION b/VERSION index 68d92dd66..fbb9ea12d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.6 +8.2.0 diff --git a/docs/articles/Core/Serialization/PossibleValues.md b/docs/articles/Core/Serialization/PossibleValues.md index d68bb0bd8..e3d565101 100644 --- a/docs/articles/Core/Serialization/PossibleValues.md +++ b/docs/articles/Core/Serialization/PossibleValues.md @@ -26,15 +26,15 @@ public abstract class PossibleValuesAttribute : Attribute public abstract bool UpdateFromPredecessor { get; } /// - /// All possible values for this member represented as strings. The given container might be null + /// All possible values for this member represented as strings. The given containers might be null /// and can be used to resolve possible values /// - public abstract IEnumerable GetValues(IContainer container); + public virtual IEnumerable GetValues(IContainer container, IServiceProvider serviceProvider); /// /// String to value conversion. Must be override if is set to true"/> /// - public virtual object Parse(IContainer container, string value) + public virtual object Parse(IContainer container, IServiceProvider serviceProvider), string value) { return value; } diff --git a/docs/tutorials/HowToTestAModule.md b/docs/tutorials/HowToTestAModule.md new file mode 100644 index 000000000..0ee512088 --- /dev/null +++ b/docs/tutorials/HowToTestAModule.md @@ -0,0 +1,45 @@ +# Setup a test environment for integration tests of a module + +In order to test a module in its lifecycle with its respective facade we offer the `Moryx.TestTools.IntegrationTest`. +The package brings a `MoryxTestEnvironment`. +With this class you can first create mocks for all module facades your module dependents on using the static `CreateModuleMock` method. +Afterwards you can create the environment using an implementation of the `ServerModuleBase` class, an instance of the `ConfigBase` and the set of dependency mocks. +The first two parameters are usually your `ModuleController` and your `ModuleConfig`. +The following example shows a setup for the `IShiftManagement` facade interface. The module depends on the `IResourceManagement` and `IOperatorManagement` facades. + +```csharp +private ModuleConfig _config; +private Mock _resourceManagementMock; +private Mock _operatorManagementMock; +private MoryxTestEnvironment _env; + +[SetUp] +public void SetUp() +{ + ReflectionTool.TestMode = true; + _config = new(); + _resourceManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _operatorManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _env = new MoryxTestEnvironment(typeof(ModuleController), + new Mock[] { _resourceManagementMock, _operatorManagementMock }, _config); +} +``` + +Using the created environment you can start and stop the module as you please. +You can also retrieve the facade of the module to test all the functionalities the running module should provide. + +```csharp +[Test] +public void Start_WhenModuleIsStopped_StartsModule() +{ + // Arrange + var facade = _env.GetTestModule(); + + // Act + var module = _env.StartTestModule(); + var module = _env.StopTestModule(); + + // Assert + Assert.That(module.State, Is.EqualTo(ServerModuleState.Stopped)); +} +``` \ No newline at end of file diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj b/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj index b5eeaf342..6c7673222 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs index 8b4dea2c7..42adb63f5 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Moryx.Configuration; +using Moryx.Container; using Moryx.Serialization; namespace Moryx.AbstractionLayer.Products.Endpoints @@ -31,7 +32,18 @@ static PartialSerialization() /// /// Creates a new instance /// - public PartialSerialization() : base(null, new EmptyValueProvider()) + [Obsolete("Use serialization with containers instead")] + public PartialSerialization() : this(null, null) + { + } + + /// + /// Create serialization with access to global and local container + /// + /// + /// + public PartialSerialization(IContainer localContainer, IServiceProvider serviceProvider) + : base(localContainer, serviceProvider, new EmptyValueProvider()) { } diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs index 545315451..d1b5c14d5 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0 using Moryx.AbstractionLayer.Recipes; +using Moryx.Container; using Moryx.Serialization; using Moryx.Tools; using Moryx.Workplans; @@ -20,12 +21,19 @@ public class ProductConverter // Null object pattern for identity private static readonly ProductIdentity EmptyIdentity = new ProductIdentity(string.Empty, 0); - private static readonly ICustomSerialization ProductSerialization = new PartialSerialization(); - private static readonly ICustomSerialization RecipeSerialization = new PartialSerialization(); + private readonly ICustomSerialization _productSerialization; + private readonly ICustomSerialization _recipeSerialization; - public ProductConverter(IProductManagement productManagement) + public IContainer ProductManagerContainer { get; } + public IServiceProvider GlobalContainer { get; } + + public ProductConverter(IProductManagement productManagement, IContainer localContainer, IServiceProvider globalContainer) { _productManagement = productManagement; + ProductManagerContainer = localContainer; + GlobalContainer = globalContainer; + _productSerialization = new PartialSerialization(localContainer, globalContainer); + _recipeSerialization = new PartialSerialization(localContainer, globalContainer); } public ProductDefinitionModel ConvertProductType(Type productType) @@ -39,7 +47,7 @@ public ProductDefinitionModel ConvertProductType(Type productType) Name = productType.FullName, DisplayName = productType.GetDisplayName() ?? productType.Name, BaseDefinition = baseTypeName, - Properties = EntryConvert.EncodeClass(productType, ProductSerialization) + Properties = EntryConvert.EncodeClass(productType, _productSerialization) }; } @@ -73,7 +81,7 @@ public ProductModel ConvertProduct(IProductType productType, bool flat) // Properties var typeWrapper = _productManagement.GetTypeWrapper(productType.GetType().FullName); var properties = typeWrapper != null ? typeWrapper.Properties.ToArray() : productType.GetType().GetProperties(); - converted.Properties = EntryConvert.EncodeObject(productType, ProductSerialization); + converted.Properties = EntryConvert.EncodeObject(productType, _productSerialization); // Files converted.Files = ConvertFiles(productType, properties); @@ -124,7 +132,7 @@ private void ConvertParts(IProductType productType, IEnumerable pr DisplayName = displayName, Type = FetchProductType(property.PropertyType), Parts = partModel is null ? new PartModel[0] : new[] { partModel }, - PropertyTemplates = EntryConvert.EncodeClass(property.PropertyType, ProductSerialization) + PropertyTemplates = EntryConvert.EncodeClass(property.PropertyType, _productSerialization) }; connectors.Add(connector); } @@ -139,7 +147,7 @@ private void ConvertParts(IProductType productType, IEnumerable pr DisplayName = displayName, Type = FetchProductType(linkType), Parts = links?.Select(ConvertPart).ToArray(), - PropertyTemplates = EntryConvert.EncodeClass(linkType, ProductSerialization) + PropertyTemplates = EntryConvert.EncodeClass(linkType, _productSerialization) }; connectors.Add(connector); } @@ -165,7 +173,7 @@ private PartModel ConvertPart(IProductPartLink link) { Id = link.Id, Product = ConvertProduct(link.Product, true), - Properties = EntryConvert.EncodeObject(link, ProductSerialization) + Properties = EntryConvert.EncodeObject(link, _productSerialization) }; return part; } @@ -214,7 +222,7 @@ public IProductType ConvertProductBack(ProductModel source, ProductType converte // Copy extended properties var typeWrapper = _productManagement.GetTypeWrapper(converted.GetType().FullName); var properties = typeWrapper != null ? typeWrapper.Properties.ToArray() : converted.GetType().GetProperties(); - EntryConvert.UpdateInstance(converted, source.Properties, ProductSerialization); + EntryConvert.UpdateInstance(converted, source.Properties, _productSerialization); // Copy Files ConvertFilesBack(converted, source, properties); @@ -318,7 +326,12 @@ private static void ConvertFilesBack(object converted, ProductModel product, Pro } } - public static RecipeModel ConvertRecipe(IRecipe recipe) + [Obsolete("Use ConvertRecipe on instance")] + public static RecipeModel ConvertRecipe(IRecipe recipe) => ConvertRecipe(recipe, new PartialSerialization(null, null)); + + public RecipeModel ConvertRecipeV2(IRecipe recipe) => ConvertRecipe(recipe, _recipeSerialization); + + private static RecipeModel ConvertRecipe(IRecipe recipe, ICustomSerialization serialization) { // Transform to DTO and transmit var converted = new RecipeModel @@ -328,7 +341,7 @@ public static RecipeModel ConvertRecipe(IRecipe recipe) Type = recipe.GetType().Name, State = recipe.State, Revision = recipe.Revision, - Properties = EntryConvert.EncodeObject(recipe, RecipeSerialization), + Properties = EntryConvert.EncodeObject(recipe, serialization), IsClone = recipe.Classification.HasFlag(RecipeClassification.Clone) }; @@ -377,7 +390,7 @@ public IProductRecipe ConvertRecipeBack(RecipeModel recipe, IProductRecipe produ productRecipe.Product = productType; } - EntryConvert.UpdateInstance(productRecipe, recipe.Properties, RecipeSerialization); + EntryConvert.UpdateInstance(productRecipe, recipe.Properties, _recipeSerialization); // Do not update a clones classification if (productRecipe.Classification.HasFlag(RecipeClassification.Clone)) diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs index 981aad47b..516876d7e 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs @@ -14,6 +14,10 @@ using Moryx.Configuration; using Moryx.Asp.Extensions; using Moryx.AbstractionLayer.Properties; +using Microsoft.Extensions.DependencyInjection; +using Moryx.AbstractionLayer.Resources; +using Moryx.Runtime.Modules; +using System.ComponentModel; namespace Moryx.AbstractionLayer.Products.Endpoints { @@ -27,10 +31,14 @@ public class ProductManagementController : ControllerBase { private readonly IProductManagement _productManagement; private readonly ProductConverter _productConverter; - public ProductManagementController(IProductManagement productManagement) + public ProductManagementController(IProductManagement productManagement, + IModuleManager moduleManager, + IServiceProvider serviceProvider) { _productManagement = productManagement; - _productConverter = new ProductConverter(_productManagement); + + var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); + _productConverter = new ProductConverter(_productManagement, module.Container, serviceProvider); } #region importers @@ -39,6 +47,8 @@ public ProductManagementController(IProductManagement productManagement) [Authorize(Policy = ProductPermissions.CanViewTypes)] public ActionResult GetProductCustomization() { + var parameterSerialization = new PossibleValuesSerialization(_productConverter.ProductManagerContainer, _productConverter.GlobalContainer, + new ValueProviderExecutor(new ValueProviderExecutorSettings().AddDefaultValueProvider())); return new ProductCustomization { ProductTypes = GetProductTypes(), @@ -46,7 +56,7 @@ public ActionResult GetProductCustomization() Importers = _productManagement.Importers.Select(i => new ProductImporter { Name = i.Key, - Parameters = EntryConvert.EncodeObject(i.Value, new PossibleValuesSerialization(null, new ValueProviderExecutor(new ValueProviderExecutorSettings().AddDefaultValueProvider()))) + Parameters = EntryConvert.EncodeObject(i.Value, parameterSerialization) }).ToArray() }; } @@ -247,7 +257,7 @@ public ActionResult GetRecipes(long id, int classification) var recipes = _productManagement.GetRecipes(productType, (RecipeClassification)classification); var recipeModels = new List(); foreach (var recipe in recipes) - recipeModels.Add(ProductConverter.ConvertRecipe(recipe)); + recipeModels.Add(_productConverter.ConvertRecipeV2(recipe)); return recipeModels.ToArray(); } #endregion @@ -330,7 +340,7 @@ public ActionResult GetRecipe(long id) var recipe = _productManagement.LoadRecipe(id); if (recipe == null) return NotFound(new MoryxExceptionResponse {Title= string.Format(Strings.RecipeNotFoundException_Message, id) }); - return ProductConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipeV2(recipe); } [HttpPost] @@ -380,7 +390,7 @@ public ActionResult CreateRecipe(string recipeType) var recipe = _productManagement.CreateRecipe(recipeType); if (recipe == null) recipe = (IProductRecipe)TypeTool.CreateInstance(recipeType); - return ProductConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipeV2(recipe); } #endregion } diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs new file mode 100644 index 000000000..3b15660fd --- /dev/null +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Reflection; +using System.Runtime.Serialization; +using Moryx.Configuration; + +namespace Moryx.AbstractionLayer.Resources.Endpoints +{ + internal class DataMemberAttributeValueProviderFilter : IValueProviderFilter + { + private readonly bool _filterDataMembers; + + public DataMemberAttributeValueProviderFilter(bool filterDataMembers) + { + _filterDataMembers = filterDataMembers; + } + + public bool CheckProperty(PropertyInfo propertyInfo) + { + if (_filterDataMembers) + return propertyInfo.GetCustomAttribute() == null; + return propertyInfo.GetCustomAttribute() != null; + } + } +} diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj index 689fe70c9..21907fa30 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index 15e8d1458..15aca8484 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -16,7 +16,6 @@ using Moryx.AbstractionLayer.Properties; using Moryx.Runtime.Modules; using Moryx.Configuration; -using Moryx.Resources.Management; namespace Moryx.AbstractionLayer.Resources.Endpoints { @@ -32,12 +31,15 @@ public class ResourceModificationController : ControllerBase private readonly IResourceTypeTree _resourceTypeTree; private readonly ResourceSerialization _serialization; - public ResourceModificationController(IResourceManagement resourceManagement, IResourceTypeTree resourceTypeTree, IModuleManager moduleManager) + public ResourceModificationController(IResourceManagement resourceManagement, + IResourceTypeTree resourceTypeTree, + IModuleManager moduleManager, + IServiceProvider serviceProvider) { _resourceManagement = resourceManagement ?? throw new ArgumentNullException(nameof(resourceManagement)); _resourceTypeTree = resourceTypeTree ?? throw new ArgumentNullException(nameof(resourceTypeTree)); var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); - _serialization = new ResourceSerialization(module.Container); + _serialization = new ResourceSerialization(module.Container, serviceProvider); } [HttpGet] diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs index 2af89c41a..cf4eb70d5 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs @@ -18,7 +18,7 @@ internal class ResourceSerialization : PossibleValuesSerialization /// private EntrySerializeSerialization _memberFilter = new(); - public ResourceSerialization(IContainer container) : base(container, new ValueProviderExecutor(new ValueProviderExecutorSettings())) + public ResourceSerialization(IContainer container, IServiceProvider serviceProvider) : base(container, serviceProvider, new ValueProviderExecutor(new ValueProviderExecutorSettings())) { } diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index e5441ec98..19dba0c84 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.11.5", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", - "@mui/material": "^5.15.15", + "@mui/material": "^6.1.1", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@types/react-redux": "^7.1.33", @@ -24,8 +24,8 @@ "moment": "^2.30.1", "path-scurry": "^1.10.2", "query-string": "^9.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-redux": "^9.1.0", "react-router": "^6.22.0", "react-router-dom": "^6.22.0", @@ -41,11 +41,11 @@ "@types/react-router-redux": "^5.0.27", "css-loader": "^6.10.0", "html-webpack-plugin": "^5.6.0", - "rimraf": "5.0.5", + "rimraf": "5.0.7", "sass": "^1.72.0", "sass-loader": "^14.1.0", "source-map-loader": "^5.0.0", - "style-loader": "^3.3.4", + "style-loader": "^4.0.0", "tslint": "^6.1.3", "tslint-loader": "^3.5.4", "tslint-react": "^5.0.0", diff --git a/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj b/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj index ccd43227d..2a9435d73 100644 --- a/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj +++ b/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj @@ -9,6 +9,7 @@ + \ No newline at end of file diff --git a/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs b/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs new file mode 100644 index 000000000..6e3412027 --- /dev/null +++ b/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Moryx.Container; +using Moryx.Serialization; +using Moryx.TestModule; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Moryx.Products.Samples.Recipe +{ + public class FacacadeRecipeValueAttribute : PossibleValuesAttribute + { + public override bool OverridesConversion => false; + + public override bool UpdateFromPredecessor => false; + + public override IEnumerable GetValues(IContainer localContainer, IServiceProvider serviceProvider) + { + var module = serviceProvider.GetRequiredService(); + return new[] { module.Bla.ToString("D") }; + } + } +} diff --git a/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs b/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs index d1f33565c..4fbcd4e52 100644 --- a/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs +++ b/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs @@ -21,7 +21,7 @@ public WatchProductRecipe(WatchProductRecipe source) : base(source) Case = source.Case; } - [EntrySerialize] + [EntrySerialize, FacacadeRecipeValue] [DisplayName("Cores Installed")] public int CoresInstalled { get; set; } diff --git a/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs b/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs index 20278dccd..777c35110 100644 --- a/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs +++ b/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs @@ -1,12 +1,14 @@ // Copyright (c) 2023, Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 +using System; using System.Reflection; using System.Runtime.Serialization; using Moryx.Configuration; namespace Moryx.Resources.Management { + [Obsolete("No longer needed within the ResourceManagement")] public class DataMemberAttributeValueProviderFilter : IValueProviderFilter { private readonly bool _filterDataMembers; diff --git a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs index 7a3637f7f..95071c66d 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs @@ -71,9 +71,8 @@ public Resource Instantiate(IResourceTypeController typeController, IResourceGra Instance.Name = Name; Instance.Description = Description; - // Copy extended data from json - if (ExtensionData != null) - JsonConvert.PopulateObject(ExtensionData, Instance, JsonSettings.Minimal); + // Copy extended data from json, or simply use JSON to provide defaults + JsonConvert.PopulateObject(ExtensionData ?? "{}", Instance, JsonSettings.Minimal); return Instance; } @@ -110,14 +109,14 @@ public static ICollection FetchResourceTemplates(IUnitOf { var resourceRepo = uow.GetRepository(); - var resources = + var resources = (from res in resourceRepo.Linq where res.Deleted == null select res) .ToList(); var resourcEntityAccessors = resources - .Select(res => + .Select(res => new ResourceEntityAccessor { Id = res.Id, @@ -126,13 +125,13 @@ public static ICollection FetchResourceTemplates(IUnitOf Description = res.Description, ExtensionData = res.ExtensionData, Relations = (from target in res.Targets - where target.Target.Deleted == null - // Attention: This is Copy&Paste because of LinQ limitations - select new ResourceRelationAccessor - { - Entity = target, - Role = ResourceReferenceRole.Target, - }).Concat( + where target.Target.Deleted == null + // Attention: This is Copy&Paste because of LinQ limitations + select new ResourceRelationAccessor + { + Entity = target, + Role = ResourceReferenceRole.Target, + }).Concat( from source in res.Sources where source.Source.Deleted == null // Attention: This is Copy&Paste because of LinQ limitations @@ -180,7 +179,7 @@ internal class ResourceRelationAccessor /// Type of the reference relation /// public ResourceRelationType RelationType => (ResourceRelationType)Entity.RelationType; - + /// /// Id of the referenced resource /// diff --git a/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs b/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs index 9d8b03114..73d68e1c3 100644 --- a/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs +++ b/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs @@ -26,12 +26,14 @@ public class ModulesController : ControllerBase private readonly IModuleManager _moduleManager; private readonly IConfigManager _configManager; private readonly IParallelOperations _parallelOperations; + private readonly IServiceProvider _serviceProvider; - public ModulesController(IModuleManager moduleManager, IConfigManager configManager, IParallelOperations parallelOperations) + public ModulesController(IModuleManager moduleManager, IConfigManager configManager, IParallelOperations parallelOperations, IServiceProvider serviceProvider) { _moduleManager = moduleManager; _configManager = configManager; _parallelOperations = parallelOperations; + _serviceProvider = serviceProvider; } [HttpGet("dependencies")] @@ -264,7 +266,7 @@ public ActionResult InvokeMethod([FromRoute] string moduleName, [FromBody /// private ICustomSerialization CreateSerialization(IServerModule module) { - return new PossibleValuesSerialization(module.Container, (IEmptyPropertyProvider)_configManager) + return new PossibleValuesSerialization(module.Container, _serviceProvider, (IEmptyPropertyProvider)_configManager) { FormatProvider = Thread.CurrentThread.CurrentUICulture }; diff --git a/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj new file mode 100644 index 000000000..bf257e324 --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Library with helper classes for integration tests. + true + MORYX;Tests;IntegrationTest + true + + + + + + + + + + + + diff --git a/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs new file mode 100644 index 000000000..3b83c7c08 --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs @@ -0,0 +1,132 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Moq; +using Moryx.Configuration; +using Moryx.Model.InMemory; +using Moryx.Model; +using Moryx.Runtime.Kernel; +using Moryx.Runtime.Modules; +using Moryx.TestTools.UnitTest; +using Moryx.Threading; +using System; +using System.Linq; +using Moryx.Tools; +using System.Collections.Generic; + +namespace Moryx.TestTools.IntegrationTest +{ + /// + /// A test environment for MORYX modules to test the module lifecycle as well as its + /// facade and component orchestration. The environment must be filled with mocked + /// dependencies. + /// + /// Type of the facade to be tested. + public class MoryxTestEnvironment + { + private readonly Type _moduleType; + + public IServiceProvider Services { get; private set; } + + /// + /// Creates an for integration tests of moryx. We prepare the + /// service collection to hold all kernel components (a mocked IConfigManager providing only the , + /// , an , a and the + /// ). Additionally all provided mocks are registered as moryx modules. + /// + /// Type of the ModuleController of the module to be tested + /// An enumeration of mocks for all dependencies of the module to be tested. + /// We recommend using the method to properly create the mocks. + /// The config for the module to be tested. + /// Throw if is not a server module + public MoryxTestEnvironment(Type serverModuleType, IEnumerable dependencyMocks, ConfigBase config) + { + _moduleType = serverModuleType; + + if (!serverModuleType.IsAssignableTo(typeof(IServerModule))) + throw new ArgumentException("Provided parameter is no server module", nameof(serverModuleType)); + + var dependencyTypes = serverModuleType.GetProperties() + .Where(p => p.GetCustomAttribute() is not null) + .Select(p => p.PropertyType); + + var services = new ServiceCollection(); + foreach (var type in dependencyTypes) + { + var mock = dependencyMocks.SingleOrDefault(m => type.IsAssignableFrom(m.Object.GetType())) ?? + throw new ArgumentException($"Missing {nameof(Mock)} for dependency of type {type} of facade type {serverModuleType}", nameof(dependencyMocks)); + services.AddSingleton(type, mock.Object); + services.AddSingleton(typeof(IServerModule), mock.Object); + } + + services.AddMoryxKernel(); + var configManagerMock = new Mock(); + configManagerMock.Setup(c => c.GetConfiguration(config.GetType(), It.IsAny(), false)).Returns(config); + services.AddSingleton(configManagerMock.Object); + + var parallelOpsDescriptor = services.Single(d => d.ServiceType == typeof(IParallelOperations)); + services.Remove(parallelOpsDescriptor); + services.AddTransient(); + services.AddSingleton(new InMemoryDbContextManager(Guid.NewGuid().ToString())); + services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new Mock>().Object); + services.AddMoryxModules(); + + Services = services.BuildServiceProvider(); + _ = Services.GetRequiredService(); + } + + /// + /// Creates a mock of a server module with a facade interface of type . + /// The mock can be used in setting up a service collection for test purposes. + /// + /// Type of the facade interface + /// The mock of the + public static Mock CreateModuleMock() where FacadeType : class + { + var mock = new Mock(); + var moduleMock = mock.As(); + moduleMock.SetupGet(m => m.State).Returns(ServerModuleState.Running); + var containerMock = moduleMock.As>(); + containerMock.SetupGet(x => x.Facade).Returns(mock.Object); + return mock; + } + + /// + /// Initializes and starts the module with the facade interface of type + /// . + /// + /// The started module. + public IServerModule StartTestModule() + { + var module = (IServerModule)Services.GetService(_moduleType); + + module.Initialize(); + module.Container.Register(typeof(NotSoParallelOps), [typeof(IParallelOperations)], nameof(NotSoParallelOps), Container.LifeCycle.Singleton); + + var strategies = module.GetType().GetProperty(nameof(ServerModuleBase.Strategies)).GetValue(module) as Dictionary; + if (strategies is not null && !strategies.Any(s => s.Value == nameof(NotSoParallelOps))) + strategies.Add(typeof(IParallelOperations), nameof(NotSoParallelOps)); + + module.Start(); + return module; + } + + /// + /// Stops the module with the facade interface of type . + /// + /// The stopped module. + public IServerModule StopTestModule() + { + var module = (IServerModule)Services.GetService(_moduleType); + module.Stop(); + + return module; + } + + /// + /// Returns the service for the facade of type to be tested. + /// + public TModule GetTestModule() => Services.GetRequiredService(); + } +} \ No newline at end of file diff --git a/src/Moryx.TestTools.NUnit/MAssert.cs b/src/Moryx.TestTools.NUnit/MAssert.cs new file mode 100644 index 000000000..8c75378d0 --- /dev/null +++ b/src/Moryx.TestTools.NUnit/MAssert.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using NUnit.Framework.Constraints; +using NUnit.Framework.Internal; +using System; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Moryx.TestTools.NUnit +{ + public abstract class MAssert + { + public static void That(bool condition, string? message = null, [CallerArgumentExpression(nameof(condition))] string? predicateExpression = null) + { + That(condition, Is.True, message, predicateExpression); + } + public static void That(Func predicate, string? message = null, [CallerArgumentExpression(nameof(predicate))]string? predicateExpression = null) + { + That(predicate, Is.True, message, predicateExpression); + } + + public static void That(T actual, IResolveConstraint constraint, string? message = null, [CallerArgumentExpression(nameof(actual))] string? predicateExpression = null) + { + if (message != null) + { + message = $"{message}\nExpression: {predicateExpression}"; + } + else + { + message = predicateExpression; + } + Assert.That(actual, constraint, message); + } + + public static void That(Func actualExpression, Constraint constraint, string? message = null, [CallerArgumentExpression(nameof(actualExpression))] string? predicateExpression = null) + { + if (message != null) + { + message = $"{message}\nExpression: {predicateExpression}"; + } + else + { + message = predicateExpression; + } + int fails = TestExecutionContext.CurrentContext.CurrentResult.PendingFailures; + T value = default(T)!; + Assert.That(() => value = actualExpression(), new ThrowsNothingConstraint(), $"{message}\nExpected {constraint.Description} and"); + if (TestExecutionContext.CurrentContext.CurrentResult.PendingFailures > fails) return; // TODO: Check if we there could be multithreading issues and whether or not we care + Assert.That(value, constraint, message); + } + } + +} diff --git a/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj new file mode 100644 index 000000000..7fea020cc --- /dev/null +++ b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Moryx/Configuration/PossibleValuesSerialization.cs b/src/Moryx/Configuration/PossibleValuesSerialization.cs index 0c1b07112..fc9987f3a 100644 --- a/src/Moryx/Configuration/PossibleValuesSerialization.cs +++ b/src/Moryx/Configuration/PossibleValuesSerialization.cs @@ -21,6 +21,11 @@ public class PossibleValuesSerialization : DefaultSerialization /// protected IContainer Container { get; } + /// + /// Access to level 1 service registration + /// + public IServiceProvider ServiceProvider { get; } + /// /// Empty property provider to pre-fill newley created objects /// @@ -29,9 +34,17 @@ public class PossibleValuesSerialization : DefaultSerialization /// /// Initialize base class /// + [Obsolete("Construct possible values with ServiceProvider for attributes that rely on it")] public PossibleValuesSerialization(IContainer container, IEmptyPropertyProvider emptyPropertyProvider) + :this(container, null, emptyPropertyProvider) { } + + /// + /// Initialize base class + /// + public PossibleValuesSerialization(IContainer container, IServiceProvider serviceProvider, IEmptyPropertyProvider emptyPropertyProvider) { Container = container; + ServiceProvider = serviceProvider; EmptyPropertyProvider = emptyPropertyProvider; } @@ -45,9 +58,9 @@ public override EntryPrototype[] Prototypes(Type memberType, ICustomAttributePro // Create prototypes from possible values var list = new List(); - foreach (var value in possibleValuesAtt.GetValues(Container)) + foreach (var value in possibleValuesAtt.GetValues(Container, ServiceProvider)) { - var prototype = possibleValuesAtt.Parse(Container, value); + var prototype = possibleValuesAtt.Parse(Container, ServiceProvider, value); EmptyPropertyProvider.FillEmpty(prototype); list.Add(new EntryPrototype(value, prototype)); } @@ -63,7 +76,7 @@ public override string[] PossibleValues(Type memberType, ICustomAttributeProvide return base.PossibleValues(memberType, attributeProvider); // Use attribute - var values = valuesAttribute.GetValues(Container); + var values = valuesAttribute.GetValues(Container, ServiceProvider); return values?.Distinct().ToArray(); } @@ -90,7 +103,7 @@ public override string[] PossibleElementValues(Type memberType, ICustomAttribute } // Use attribute - var values = valuesAttribute.GetValues(Container); + var values = valuesAttribute.GetValues(Container, ServiceProvider); return values?.Distinct().ToArray(); } @@ -99,7 +112,7 @@ public override object CreateInstance(Type memberType, ICustomAttributeProvider { var possibleValuesAtt = attributeProvider.GetCustomAttribute(); var instance = possibleValuesAtt != null - ? possibleValuesAtt.Parse(Container, encoded.Value.Current) + ? possibleValuesAtt.Parse(Container, ServiceProvider, encoded.Value.Current) : base.CreateInstance(memberType, attributeProvider, encoded); EmptyPropertyProvider.FillEmpty(instance); @@ -120,7 +133,7 @@ public override object ConvertValue(Type memberType, ICustomAttributeProvider at if (value.Type == EntryValueType.Class && currentValue != null && currentValue.GetType().Name == value.Current) return currentValue; - var instance = att.Parse(Container, mappedEntry.Value.Current); + var instance = att.Parse(Container, ServiceProvider, mappedEntry.Value.Current); if (mappedEntry.Value.Type == EntryValueType.Class) { EmptyPropertyProvider.FillEmpty(instance); diff --git a/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs b/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs index 328682bbe..144c28c8a 100644 --- a/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs +++ b/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs @@ -27,14 +27,39 @@ public abstract class PossibleValuesAttribute : Attribute /// All possible values for this member represented as strings. The given container might be null /// and can be used to resolve possible values /// - public abstract IEnumerable GetValues(IContainer container); + [Obsolete("Replaced by PossibleValues with access to global service registration")] + public virtual IEnumerable GetValues(IContainer container) + { + return Array.Empty(); + } + + /// + /// Extract possible values from local or global DI registration + /// + // TODO: Make abstract in MORYX 10 + public virtual IEnumerable GetValues(IContainer localContainer, IServiceProvider serviceProvider) + { + return GetValues(localContainer); + } /// /// String to value conversion /// + [Obsolete("Replaced by Parse with ServiceProvider reference")] public virtual object Parse(IContainer container, string value) { return value; } + + /// + /// Parse value from string using local or global DI container + /// + /// Module local DI container + /// Global service registration + /// Value to parse + public virtual object Parse(IContainer container, IServiceProvider serviceProvider, string value) + { + return Parse(container, value); + } } } diff --git a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs index 403e99218..2d6338722 100644 --- a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs +++ b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs @@ -27,7 +27,7 @@ public void Setup() { _productManagerMock = new Mock(); - _productConverter = new ProductConverter(_productManagerMock.Object); + _productConverter = new ProductConverter(_productManagerMock.Object, null, null); } #region Products @@ -227,7 +227,7 @@ public void ForwardBackwardRecipeConversionWithoutInformationLoss(DummyProductRe // Act - var convertedModel = ProductConverter.ConvertRecipe(originalRecipe); + var convertedModel = _productConverter.ConvertRecipeV2(originalRecipe); var recoveredOriginal = _productConverter.ConvertRecipeBack(convertedModel, targetDummyRecipe, backupProductType); diff --git a/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs b/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs index 32b9dc8b1..f8701a9d0 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs @@ -9,6 +9,7 @@ using Moryx.Model.Repositories; using Newtonsoft.Json; using NUnit.Framework; +using System.ComponentModel; namespace Moryx.Resources.Management.Tests { @@ -22,7 +23,8 @@ public class ResourceEntityAccessorTests public void Setup() { var typeControllerMock = new Mock(); - typeControllerMock.Setup(tc => tc.Create(It.IsAny())).Returns(new TestResource()); + typeControllerMock.Setup(tc => tc.Create(It.Is(type => type == typeof(TestResource).ResourceType()))).Returns(new TestResource()); + typeControllerMock.Setup(tc => tc.Create(It.Is(type => type == typeof(DefaultTestResource).ResourceType()))).Returns(new DefaultTestResource()); _typeControllerMock = typeControllerMock.Object; @@ -30,6 +32,26 @@ public void Setup() _resourceGraph = resourceCreator.Object; } + [Test(Description = "Calling Instantiate without an entity sets default value")] + public void InstantiateWithoutEntitySetsDefaults() + { + // Arrange + var accessor = new ResourceEntityAccessor + { + Type = typeof(DefaultTestResource).ResourceType() + }; + + // Act + var resource = accessor.Instantiate(_typeControllerMock, _resourceGraph) as DefaultTestResource; + + // Assert + Assert.NotNull(resource); + Assert.AreEqual(accessor.Type, resource.GetType().ResourceType()); + + Assert.IsTrue(resource.Enabled); + Assert.AreEqual(42, resource.Number); + } + [Test(Description = "Instantiates a resource")] public void InstantiateCreatesAValidResourceObject() { @@ -118,6 +140,15 @@ private class ExtensionDataInherited : ExtensionDataTestBase public long Value3 => 42; } + private class DefaultTestResource : Resource + { + [DataMember, DefaultValue(42)] + public int Number { get; set; } + + [DataMember, DefaultValue(true)] + public bool Enabled { get; set; } + } + private class TestResource : Resource { [DataMember] diff --git a/src/Tests/Moryx.Tests/Moryx.Tests.csproj b/src/Tests/Moryx.Tests/Moryx.Tests.csproj index 5587d8866..39faf789d 100644 --- a/src/Tests/Moryx.Tests/Moryx.Tests.csproj +++ b/src/Tests/Moryx.Tests/Moryx.Tests.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs b/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs index a001cf612..c5852a3ac 100644 --- a/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs +++ b/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs @@ -1,8 +1,10 @@ // Copyright (c) 2023, Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 +using System; using System.Collections.Generic; using System.Linq; +using Moryx.TestTools.NUnit; using Moryx.Workplans; using Moryx.Workplans.Transitions; using NUnit.Framework; @@ -56,14 +58,15 @@ public void SplitTransition() // Act trans.Initialize(); _inputs[0].Add(_token); - + // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.IsTrue(_outputs.All(o => o.Tokens.Count() == 1)); - Assert.IsInstanceOf(_outputs[0].Tokens.First()); - Assert.IsInstanceOf(_outputs[1].Tokens.First()); - Assert.AreEqual(_token, ((SplitToken)_outputs[0].Tokens.First()).Original); - Assert.AreEqual(_token, ((SplitToken)_outputs[1].Tokens.First()).Original); + Assert.Multiple(() => + { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(_outputs.Select(o => o.Tokens), Has.All.Count.EqualTo(1)); + MAssert.That(() => ((SplitToken)_outputs[0].Tokens.First()).Original, Is.EqualTo(_token)); + MAssert.That(() => ((SplitToken)_outputs[1].Tokens.First()).Original, Is.EqualTo(_token)); + }); } [Test] @@ -83,11 +86,13 @@ public void JoinTransition() trans.Initialize(); _inputs[0].Add(split1); _inputs[1].Add(split2); - // Assert - Assert.IsTrue(_inputs.All(i => !i.Tokens.Any())); - Assert.AreEqual(1, _outputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); + Assert.Multiple(() => + { + MAssert.That(_inputs.All(i => !i.Tokens.Any())); + MAssert.That(_outputs[0].Tokens.Count(), Is.EqualTo(1), "The split token should be joined into one"); + MAssert.That(_outputs[0].Tokens.First(), Is.EqualTo(_token)); + }); } [TestCase(0, Description = "Place only one split token on the first input")] @@ -108,9 +113,12 @@ public void IncompleteJoinTransition(int index) _inputs[index].Add(split); // Assert - Assert.AreEqual(1, _inputs[index].Tokens.Count()); - Assert.AreEqual(0, _inputs[(index + 1) % 2].Tokens.Count()); - Assert.AreEqual(0, _outputs[0].Tokens.Count()); + Assert.Multiple(() => + { + MAssert.That(_inputs[index].Tokens, Has.Count.EqualTo(1)); + MAssert.That(_inputs[(index + 1) % 2].Tokens, Is.Empty); + MAssert.That(_outputs[0].Tokens, Is.Empty); + }); } [Test] @@ -138,10 +146,12 @@ public void SubWorkplanTransition() _inputs[0].Add(_token); // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); - Assert.AreEqual(2, triggered.Count); - Assert.IsTrue(triggered.All(t => t is DummyTransition)); + Assert.Multiple(() => { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(() => _outputs[0].Tokens.First(), Is.EqualTo(_token)); + MAssert.That(triggered.Count, Is.EqualTo(2)); + MAssert.That(triggered, Has.All.InstanceOf()); + }); } [Test] @@ -171,14 +181,17 @@ public void SubworkplanPause() trans.Resume(); // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); - Assert.AreEqual(1, triggered.Count); - Assert.IsInstanceOf(state); - var snapshot = (WorkplanSnapshot)state; - Assert.AreEqual(1, snapshot.Holders.Length); - var stepId = workplan.Steps.First(s => s is PausableStep).Id; - Assert.AreEqual(stepId, snapshot.Holders[0].HolderId); + Assert.Multiple(() => + { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(_outputs[0].Tokens.First(), Is.EqualTo(_token)); + MAssert.That(triggered, Has.Count.EqualTo(1)); + MAssert.That(state, Is.InstanceOf()); + var snapshot = (WorkplanSnapshot)state; + MAssert.That(snapshot.Holders, Has.Length.EqualTo(1)); + var stepId = workplan.Steps.First(s => s is PausableStep).Id; + MAssert.That(snapshot.Holders[0].HolderId, Is.EqualTo(stepId)); + }); } } }