diff --git a/src/6.0/SpecFlowToMarkdown.Domain/TestAssembly/SpecFlowAssembly.cs b/src/6.0/SpecFlowToMarkdown.Domain/TestAssembly/SpecFlowAssembly.cs index c9a44bd..01bbbf9 100644 --- a/src/6.0/SpecFlowToMarkdown.Domain/TestAssembly/SpecFlowAssembly.cs +++ b/src/6.0/SpecFlowToMarkdown.Domain/TestAssembly/SpecFlowAssembly.cs @@ -5,6 +5,8 @@ namespace SpecFlowToMarkdown.Domain.TestAssembly public class SpecFlowAssembly { public string AssemblyName { get; set; } + + public string BuildConfiguration { get; set; } public IEnumerable Features { get; set; } } diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/AssemblyScanner.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/AssemblyScanner.cs index 6a8a04f..8481aea 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/AssemblyScanner.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/AssemblyScanner.cs @@ -2,6 +2,7 @@ using SpecFlowToMarkdown.Domain; using SpecFlowToMarkdown.Domain.TestAssembly; using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature; using SpecFlowToMarkdown.Infrastructure.Io; namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad @@ -35,7 +36,7 @@ public SpecFlowAssembly Perform(ProgramArguments arguments) var result = _featureExtractor - .ExtractFeatures(assembly); + .Perform(assembly); return result; } diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/BuildConfiguration.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/BuildConfiguration.cs new file mode 100644 index 0000000..ded7a09 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/BuildConfiguration.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration +{ + public class BuildConfiguration : IBuildConfiguration + { + public IEnumerable> Get() + { + var buildConfigurations = new List> + { + new( + "Debug", + 3, + 5, + 30 + ), // debug + new( + "Release", + 2, + 4, + 19 + ) // release + }; + + return buildConfigurations; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/IBuildConfiguration.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/IBuildConfiguration.cs new file mode 100644 index 0000000..616b519 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Configuration/IBuildConfiguration.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration +{ + public interface IBuildConfiguration + { + public IEnumerable> Get(); + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Extensions/InstructionEx.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extensions/InstructionEx.cs similarity index 83% rename from src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Extensions/InstructionEx.cs rename to src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extensions/InstructionEx.cs index 32652f3..af68da7 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Extensions/InstructionEx.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extensions/InstructionEx.cs @@ -1,6 +1,6 @@ using Mono.Cecil.Cil; -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Extensions +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions { internal static class InstructionEx { diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/FeatureExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/FeatureExtractor.cs new file mode 100644 index 0000000..ae56d49 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/FeatureExtractor.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Cecil.Cil; +using SpecFlowToMarkdown.Domain.TestAssembly; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Scenario; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Utils; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature +{ + public class FeatureExtractor : IFeatureExtractor + { + private readonly IEnumerable _extractors; + private readonly ILogger _logger; + + public FeatureExtractor( + ILogger logger, + IEnumerable extractors + ) + { + _logger = logger; + _extractors = extractors; + } + + public SpecFlowAssembly Perform(AssemblyDefinition assembly) + { + var assemblyName = + assembly + .Name + .Name; + + var inferredBuildConfiguration = "Unknown"; + + _logger + .LogInformation($"Assembly name: [{assemblyName}]"); + + var result = new SpecFlowAssembly + { + AssemblyName = assemblyName + }; + + var resultFeatures = new List(); + + foreach (var module in assembly.Modules) + { + foreach (var type in module.Types) + { + if (type.CustomAttributes + .Any( + o => + o + .ConstructorArguments + .Any( + arg => + arg.Value != null && arg.Value.ToString() == Constants.CustomFeatureAttributeValue + ) + )) + { + _logger + .LogInformation($"Found {type.Methods.Count} feature methods in assembly [{assemblyName}]"); + + foreach (var method in type.Methods) + { + if (method.Name != Constants.FeatureSetupMethodName) continue; + + foreach (var instruction in + method + .Body + .Instructions + .Where(o => o.OpCode == OpCodes.Newobj)) + { + if (instruction.Operand is not MethodReference methodReference) continue; + + if (methodReference.DeclaringType.FullName != Constants.FeatureInfoTypeName) continue; + + var description = string.Empty; + var title = string.Empty; + var folderPath = string.Empty; + + var currInstr = + instruction + .StepPrevious(3); + + if (currInstr.OpCode == OpCodes.Ldstr) + { + description = + currInstr + .Operand + .ToString(); + + currInstr = + currInstr + .Previous; + } + + if (currInstr.OpCode == OpCodes.Ldstr) + { + title = + currInstr + .Operand + .ToString(); + + _logger + .LogInformation($"Extracted feature: [{title}]"); + + currInstr = + currInstr + .Previous; + } + + if (currInstr.OpCode == OpCodes.Ldstr) + { + folderPath = + currInstr + .Operand + .ToString(); + } + + var feature = new SpecFlowFeature + { + FolderPath = folderPath, + Title = title, + Description = description + }; + + var scenarios = new List(); + + foreach (var typeMethod in type.Methods) + { + var applicableExtractor = + _extractors + .FirstOrDefault(o => o.IsApplicable(typeMethod)); + + if (applicableExtractor == null) + { + _logger + .LogWarning($"Could not find applicable scenario extractor for method type [{typeMethod.Name}]"); + + continue; + } + + var scenario = + applicableExtractor + .Perform( + typeMethod, + type, + ref inferredBuildConfiguration + ); + + scenarios + .Add(scenario); + } + + feature.Scenarios = scenarios; + + resultFeatures + .Add(feature); + } + } + } + } + } + + result.Features = resultFeatures; + + result.BuildConfiguration = inferredBuildConfiguration; + + return result; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IFeatureExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/IFeatureExtractor.cs similarity index 69% rename from src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IFeatureExtractor.cs rename to src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/IFeatureExtractor.cs index 7f73eb9..f6fd9d2 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IFeatureExtractor.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Feature/IFeatureExtractor.cs @@ -1,10 +1,10 @@ using Mono.Cecil; using SpecFlowToMarkdown.Domain.TestAssembly; -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature { public interface IFeatureExtractor { - public SpecFlowAssembly ExtractFeatures(AssemblyDefinition assembly); + public SpecFlowAssembly Perform(AssemblyDefinition assembly); } } \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/FeatureExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/FeatureExtractor.cs deleted file mode 100644 index d09bfe1..0000000 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/FeatureExtractor.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Mono.Cecil; -using Mono.Cecil.Cil; -using SpecFlowToMarkdown.Domain.TestAssembly; -using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Extensions; - -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors -{ - public class FeatureExtractor : IFeatureExtractor - { - private const string CustomFeatureAttributeValue = "TechTalk.SpecFlow"; - private const string FeatureSetupMethodName = "FeatureSetup"; - private const string FeatureInfoTypeName = "TechTalk.SpecFlow.FeatureInfo"; - - private readonly IScenarioExtractionHandler _scenarioExtractionHandler; - private readonly ILogger _logger; - - public FeatureExtractor( - IScenarioExtractionHandler scenarioExtractionHandler, - ILogger logger) - { - _scenarioExtractionHandler = scenarioExtractionHandler; - _logger = logger; - } - - public SpecFlowAssembly ExtractFeatures(AssemblyDefinition assembly) - { - var assemblyName = - assembly - .Name - .Name; - - _logger - .LogInformation($"Assembly name: [{assemblyName}]"); - - var result = new SpecFlowAssembly - { - AssemblyName = assemblyName - }; - - var resultFeatures = new List(); - - foreach (var module in assembly.Modules) - { - foreach (var type in module.Types) - { - if (type.CustomAttributes - .Any( - o => - o - .ConstructorArguments - .Any( - arg => - arg.Value != null && arg.Value.ToString() == CustomFeatureAttributeValue - ) - )) - { - _logger - .LogInformation($"Found {type.Methods.Count} feature methods in assembly [{assemblyName}]"); - - foreach (var method in type.Methods) - { - if (method.Name == FeatureSetupMethodName) - { - foreach (var instruction in - method - .Body - .Instructions - .Where(o => o.OpCode == OpCodes.Newobj)) - { - if (instruction.Operand is MethodReference methodReference) - { - if (methodReference.DeclaringType.FullName == FeatureInfoTypeName) - { - var description = string.Empty; - var title = string.Empty; - var folderPath = string.Empty; - - var currInstr = - instruction - .StepPrevious(3); - - if (currInstr.OpCode == OpCodes.Ldstr) - { - description = - currInstr - .Operand - .ToString(); - - currInstr = - currInstr - .Previous; - } - - if (currInstr.OpCode == OpCodes.Ldstr) - { - title = - currInstr - .Operand - .ToString(); - - _logger - .LogInformation($"Extracted feature: [{title}]"); - - currInstr = - currInstr - .Previous; - } - - if (currInstr.OpCode == OpCodes.Ldstr) - { - folderPath = - currInstr - .Operand - .ToString(); - } - - var feature = new SpecFlowFeature - { - FolderPath = folderPath, - Title = title, - Description = description - }; - - var scenarios = - _scenarioExtractionHandler - .ExtractScenarios(type); - - feature.Scenarios = scenarios; - - resultFeatures - .Add(feature); - } - } - } - } - } - } - } - } - - result.Features = resultFeatures; - - return result; - } - } -} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioArgumentBuilder.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioArgumentBuilder.cs new file mode 100644 index 0000000..2b3ecf5 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioArgumentBuilder.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Mono.Cecil.Cil; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors +{ + public interface IScenarioArgumentBuilder + { + Dictionary Build(Instruction currInstr); + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractionHandler.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractionHandler.cs deleted file mode 100644 index 85f87d3..0000000 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractionHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Mono.Cecil; -using SpecFlowToMarkdown.Domain.TestAssembly; - -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors -{ - public interface IScenarioExtractionHandler - { - public IEnumerable ExtractScenarios(TypeDefinition type); - } -} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/MsTestScenarioExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/MsTestScenarioExtractor.cs deleted file mode 100644 index 964d9e3..0000000 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/MsTestScenarioExtractor.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Mono.Cecil; -using Mono.Cecil.Cil; -using SpecFlowToMarkdown.Domain.TestAssembly; -using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Extensions; - -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Providers -{ - public class MsTestScenarioExtractor : IScenarioExtractor - { - private readonly ILogger _logger; - - private readonly IEnumerable _testCaseAttributes = new[] - { - Constants.MsTestTestPropertyAttribute - }; - - private const string ParameterDeclarationSplit = "Parameter\\:.*"; - - public MsTestScenarioExtractor(ILogger logger) - { - _logger = logger; - } - - public bool IsApplicable(MethodDefinition method) - { - return - method.IsVirtual || - method - .CustomAttributes - .Any( - o => - o - .AttributeType.FullName == Constants.MsTestTestAttribute - ); - } - - public SpecFlowScenario ExtractScenario(MethodDefinition method, TypeDefinition type) - { - // Extract Scenario - var title = string.Empty; - var description = string.Empty; - var tags = new List(); - var scenarioCases = new List(); - - var scenarioArgumentNames = new Dictionary(); - var scenarioArgumentValues = new List>(); - - foreach (var instruction in - method - .Body - .Instructions - .Where(o => o.OpCode == OpCodes.Newobj)) - { - if (instruction.Operand is MethodReference mr) - { - if (mr.DeclaringType.FullName == Constants.ScenarioInfoTypeName) - { - var currInstr = - instruction - .StepPrevious(4); - - if (currInstr.OpCode == OpCodes.Ldstr) - { - description = - currInstr - .Operand - .ToString(); - } - - currInstr = - currInstr - .Previous; - - if (currInstr.OpCode == OpCodes.Ldstr) - { - title = - currInstr - .Operand - .ToString(); - - _logger - .LogInformation($"Extracted scenario: [{title}]"); - } - - var testCaseFunctions = - type - .Methods - .Where( - x => - x.Body - .Instructions - .Any( - o => - { - if (o.OpCode == OpCodes.Callvirt && o.Operand is MethodReference methodReference) - { - var methodName = methodReference.Name; - return methodName == method.Name; - } - - return false; - } - ) - ) - .ToList(); - - var testCases = - testCaseFunctions - .Count; - - if (testCases > 0) - { - _logger - .LogInformation($"Scenario [{title}]; found {testCases} test cases"); - - var buildConfigurations = new List> - { - new( - 3, - 5 - ), // debug - new( - 2, - 4 - ) // release - }; - - var startingInstruction = currInstr; - - foreach (var buildConfiguration in buildConfigurations) - { - currInstr = startingInstruction; - - // Get test case argument names - currInstr = - currInstr - .StepPrevious(buildConfiguration.Item1); - - while (true) - { - string argKey = null; - string argValue = null; - - if (currInstr.OpCode == OpCodes.Ldarg_S) - { - argKey = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Ldarg_3) - argKey = "3"; - else if (currInstr.OpCode == OpCodes.Ldarg_2) - argKey = "2"; - else if (currInstr.OpCode == OpCodes.Ldarg_1) - argKey = "1"; - else if (currInstr.OpCode == OpCodes.Ldarg_0) - argKey = "0"; - - var stringLoadInstruction = - currInstr - .Previous; - - if (stringLoadInstruction.OpCode == OpCodes.Ldstr) - { - argValue = - stringLoadInstruction - .Operand - .ToString(); - } - - if (!string.IsNullOrEmpty(argKey) && !string.IsNullOrEmpty(argValue)) - { - scenarioArgumentNames - .Add( - argKey, - argValue - ); - - currInstr = - currInstr - .StepPrevious(buildConfiguration.Item2); - } - else - break; - } - - if (scenarioArgumentNames.Count > 0) - break; - } - - if (scenarioArgumentNames.Count == 0) - throw new Exception( - "Could not extract test cases from assembly; please try again with a different build configuration" - ); - - currInstr = - currInstr - .StepPrevious(15); - - // Get test case argument values - foreach (var testCaseFunction in testCaseFunctions) - { - var specFlowArguments = new List(); - var caseAttributes = - testCaseFunction - .CustomAttributes - .Where(o => _testCaseAttributes.Contains(o.AttributeType.FullName)) - .ToList(); - - foreach (var caseAttribute in caseAttributes) - { - var constructorArguments = - caseAttribute - .ConstructorArguments; - - if (constructorArguments.Count == 2) - { - var parameterNameArg = - constructorArguments[0] - .Value - .ToString(); - - var fieldMatch = - new Regex(ParameterDeclarationSplit) - .Matches(parameterNameArg); - - if (fieldMatch.Count > 0) - { - var argName = - fieldMatch[0] - .Value - .Split(":") - [1]; - - var argValue = - constructorArguments[1] - .Value - .ToString(); - - specFlowArguments - .Add(new SpecFlowArgument - { - ArgumentName = argName, - ArgumentValue = argValue - } - ); - } - } - } - - var scenarioCase = new SpecFlowCase - { - Arguments = specFlowArguments - }; - - _logger - .LogInformation( - $"Extracted test case: {{{string.Join(", ", scenarioCase.Arguments.Select(o => $"\"{o.ArgumentName}\":\"{o.ArgumentValue}\""))}}}" - ); - - scenarioCases - .Add(scenarioCase); - } - } - else - { - currInstr = - currInstr - .StepPrevious(5); - } - - while (currInstr?.OpCode == OpCodes.Ldstr) - { - if (currInstr.Operand != null) - { - tags.Add( - currInstr - .Operand.ToString()? - .Replace( - ",", - "" - ) - ); - } - - currInstr = - currInstr - .StepPrevious(4); - } - } - } - } - - var scenario = new SpecFlowScenario - { - Title = title, - Description = description, - Tags = tags - }; - - // Extract Steps - var scenarioSteps = new List(); - - foreach (var instruction in - method - .Body - .Instructions - .Where(o => o.OpCode == OpCodes.Callvirt)) - { - if (instruction.Operand is MethodReference methodReference) - { - if (Constants.ScenarioStepFunctions.Any(o => methodReference.Name == o)) - { - var keyword = methodReference.Name; - var text = string.Empty; - - var currInstr = - instruction - .StepPrevious(4); - - if (currInstr.OpCode == OpCodes.Ldstr) - { - text = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Call) - { - if (currInstr.Operand is MethodReference mr) - { - if (mr.Name == Constants.StringFormatFunctionName) - { - var stringFormatArguments = new List(); - string preFormatText = null; - - while (true) - { - string argKey = null; - - currInstr = - currInstr - .Previous; - - if (currInstr.OpCode == OpCodes.Ldarg_S) - { - argKey = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Ldarg_3) - argKey = "3"; - else if (currInstr.OpCode == OpCodes.Ldarg_2) - argKey = "2"; - else if (currInstr.OpCode == OpCodes.Ldarg_1) - argKey = "1"; - else if (currInstr.OpCode == OpCodes.Ldarg_0) - argKey = "0"; - else if (currInstr.OpCode == OpCodes.Ldstr) - preFormatText = - currInstr - .Operand - .ToString(); - - if (string.IsNullOrEmpty(argKey)) - break; - - if (scenarioArgumentNames.TryGetValue( - argKey, - out var argumentName - )) - { - stringFormatArguments - .Add($"<{argumentName}>"); - } - } - - if (!string.IsNullOrEmpty(preFormatText) && stringFormatArguments.Any()) - { - stringFormatArguments - .Reverse(); - - text = - string - .Format( - preFormatText, - stringFormatArguments - .ToArray() - ); - } - } - else - { - if (currInstr.OpCode == OpCodes.Ldstr) - { - text = - currInstr - .Operand - .ToString(); - } - } - } - } - - var executionStep = new SpecFlowExecutionStep - { - Keyword = keyword, - Text = text - }; - - _logger - .LogInformation($"Extracted test step [{executionStep.Keyword} {executionStep.Text}]"); - - scenarioSteps - .Add(executionStep); - } - } - } - - scenario.Steps = scenarioSteps; - scenario.Cases = scenarioCases; - - return scenario; - } - } -} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/UnitScenarioExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/UnitScenarioExtractor.cs deleted file mode 100644 index 193e620..0000000 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Providers/UnitScenarioExtractor.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Mono.Cecil; -using Mono.Cecil.Cil; -using SpecFlowToMarkdown.Domain.TestAssembly; -using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Extensions; - -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Providers -{ - public class UnitScenarioExtractor : IScenarioExtractor - { - private readonly IEnumerable _testCaseAttributes = new[] - { - Constants.NUnitTestCaseAttribute, - Constants.XUnitInlineDataAttribute - }; - - private static readonly string[] XOrNUnitTestAttributeValues = - { - Constants.NUnitTestAttribute, - Constants.XUnitFactAttribute, - Constants.XUnitTheoryAttribute - }; - - private readonly ILogger _logger; - - public UnitScenarioExtractor(ILogger logger) - { - _logger = logger; - } - - public bool IsApplicable(MethodDefinition method) - { - return - method - .CustomAttributes - .Any( - o => - XOrNUnitTestAttributeValues - .Contains( - o - .AttributeType.FullName - ) - ); - } - - public SpecFlowScenario ExtractScenario(MethodDefinition method, TypeDefinition type) - { - // Extract Scenario - var title = string.Empty; - var description = string.Empty; - var tags = new List(); - var scenarioCases = new List(); - - var scenarioArgumentNames = new Dictionary(); - var scenarioArgumentValues = new List>(); - - var testCases = - method - .CustomAttributes - .Count(o => _testCaseAttributes.Contains(o.AttributeType.FullName)); - - foreach (var instruction in - method - .Body - .Instructions - .Where(o => o.OpCode == OpCodes.Newobj)) - { - if (instruction.Operand is MethodReference mr) - { - if (mr.DeclaringType.FullName == Constants.ScenarioInfoTypeName) - { - var currInstr = - instruction - .StepPrevious(5); - - if (currInstr.OpCode == OpCodes.Ldstr) - { - description = - currInstr - .Operand - .ToString(); - } - - currInstr = - currInstr - .Previous; - - if (currInstr.OpCode == OpCodes.Ldstr) - { - title = - currInstr - .Operand - .ToString(); - - _logger - .LogInformation($"Extracted scenario: [{title}]"); - } - - // Extract test cases - if (testCases > 0) - { - _logger - .LogInformation($"Scenario [{title}]; found {testCases} test cases"); - - var buildConfigurations = new List> - { - new(3, 5), // debug - new(2, 4) // release - }; - - var startingInstruction = currInstr; - - foreach (var buildConfiguration in buildConfigurations) - { - currInstr = startingInstruction; - - // Get test case argument names - currInstr = - currInstr - .StepPrevious(buildConfiguration.Item1); - - while (true) - { - string argKey = null; - string argValue = null; - - if (currInstr.OpCode == OpCodes.Ldarg_S) - { - argKey = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Ldarg_3) - argKey = "3"; - else if (currInstr.OpCode == OpCodes.Ldarg_2) - argKey = "2"; - else if (currInstr.OpCode == OpCodes.Ldarg_1) - argKey = "1"; - else if (currInstr.OpCode == OpCodes.Ldarg_0) - argKey = "0"; - - var stringLoadInstruction = - currInstr - .Previous; - - if (stringLoadInstruction.OpCode == OpCodes.Ldstr) - { - argValue = - stringLoadInstruction - .Operand - .ToString(); - } - - if (!string.IsNullOrEmpty(argKey) && !string.IsNullOrEmpty(argValue)) - { - scenarioArgumentNames - .Add( - argKey, - argValue - ); - - currInstr = - currInstr - .StepPrevious(buildConfiguration.Item2); - } - else - break; - } - - if (scenarioArgumentNames.Count > 0) - break; - } - - if (scenarioArgumentNames.Count == 0) - throw new Exception("Could not extract test cases from assembly; please try again with a different build configuration"); - - currInstr = - currInstr - .StepPrevious(15); - - // Get test case argument values - var caseAttributes = - method - .CustomAttributes - .Where(o => _testCaseAttributes.Contains(o.AttributeType.FullName)) - .ToList(); - - foreach (var caseAttribute in caseAttributes) - { - var constructorArguments = - caseAttribute - .ConstructorArguments; - - if (constructorArguments.Count == 1) - { - var constructorArgument = constructorArguments[0]; - if (constructorArgument.Type.IsArray) - { - if (constructorArgument.Value is CustomAttributeArgument[] args) - { - var values = - args - .Select( - o => - { - if (o.Value is CustomAttributeArgument ca) - { - return - ca - .Value? - .ToString(); - } - - return null; - } - ) - .ToList(); - - scenarioArgumentValues - .Add(values); - } - } - } - } - - scenarioArgumentNames = - scenarioArgumentNames - .Reverse() - .ToDictionary( - o => o.Key, - o => o.Value - ); - - for (var i = 0; i < scenarioArgumentValues.Count; i++) - { - var argumentList = - scenarioArgumentNames - .Select( - o => - new SpecFlowArgument - { - ArgumentName = o.Value, - ArgumentValue = - scenarioArgumentValues[i] - .ElementAt( - scenarioArgumentNames - .Keys - .ToList() - .IndexOf(o.Key) - ) - } - ) - .ToList(); - - var scenarioCase = new SpecFlowCase - { - Arguments = argumentList - }; - - _logger - .LogInformation($"Extracted test case: {{{string.Join(", ", scenarioCase.Arguments.Select(o => $"\"{o.ArgumentName}\":\"{o.ArgumentValue}\""))}}}"); - - scenarioCases - .Add(scenarioCase); - } - } - else - { - currInstr = - currInstr - .StepPrevious(5); - } - - while (currInstr?.OpCode == OpCodes.Ldstr) - { - if (currInstr.Operand != null) - { - tags.Add( - currInstr - .Operand.ToString()? - .Replace( - ",", - "" - ) - ); - } - - currInstr = - currInstr - .StepPrevious(4); - } - } - } - } - - var scenario = new SpecFlowScenario - { - Title = title, - Description = description, - Tags = tags - }; - - // Extract Steps - var scenarioSteps = new List(); - - foreach (var instruction in - method - .Body - .Instructions - .Where(o => o.OpCode == OpCodes.Callvirt)) - { - if (instruction.Operand is MethodReference methodReference) - { - if (Constants.ScenarioStepFunctions.Any(o => methodReference.Name == o)) - { - var keyword = methodReference.Name; - var text = string.Empty; - - var currInstr = - instruction - .StepPrevious(4); - - if (currInstr.OpCode == OpCodes.Ldstr) - { - text = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Call) - { - if (currInstr.Operand is MethodReference mr) - { - if (mr.Name == Constants.StringFormatFunctionName) - { - var stringFormatArguments = new List(); - string preFormatText = null; - - while (true) - { - string argKey = null; - - currInstr = - currInstr - .Previous; - - if (currInstr.OpCode == OpCodes.Ldarg_S) - { - argKey = - currInstr - .Operand - .ToString(); - } - else if (currInstr.OpCode == OpCodes.Ldarg_3) - argKey = "3"; - else if (currInstr.OpCode == OpCodes.Ldarg_2) - argKey = "2"; - else if (currInstr.OpCode == OpCodes.Ldarg_1) - argKey = "1"; - else if (currInstr.OpCode == OpCodes.Ldarg_0) - argKey = "0"; - else if (currInstr.OpCode == OpCodes.Ldstr) - preFormatText = - currInstr - .Operand - .ToString(); - - if (string.IsNullOrEmpty(argKey)) - break; - - if (scenarioArgumentNames.TryGetValue( - argKey, - out var argumentName - )) - { - stringFormatArguments - .Add($"<{argumentName}>"); - } - } - - if (!string.IsNullOrEmpty(preFormatText) && stringFormatArguments.Any()) - { - stringFormatArguments - .Reverse(); - - text = - string - .Format( - preFormatText, - stringFormatArguments - .ToArray() - ); - } - } - else - { - if (currInstr.OpCode == OpCodes.Ldstr) - { - text = - currInstr - .Operand - .ToString(); - } - } - } - } - - var executionStep = new SpecFlowExecutionStep - { - Keyword = keyword, - Text = text - }; - - _logger - .LogInformation($"Extracted test step [{executionStep.Keyword} {executionStep.Text}]"); - - scenarioSteps - .Add(executionStep); - } - } - } - - scenario.Steps = scenarioSteps; - scenario.Cases = scenarioCases; - - return scenario; - } - } -} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/IScenarioExtractor.cs similarity index 66% rename from src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractor.cs rename to src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/IScenarioExtractor.cs index ec35bf1..ead12dd 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/IScenarioExtractor.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/IScenarioExtractor.cs @@ -1,12 +1,12 @@ using Mono.Cecil; using SpecFlowToMarkdown.Domain.TestAssembly; -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Scenario { public interface IScenarioExtractor { public bool IsApplicable(MethodDefinition method); - public SpecFlowScenario ExtractScenario(MethodDefinition method, TypeDefinition type); + public SpecFlowScenario Perform(MethodDefinition method, TypeDefinition type, ref string environment); } } \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/MsTestScenarioExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/MsTestScenarioExtractor.cs new file mode 100644 index 0000000..3ef8ac7 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/MsTestScenarioExtractor.cs @@ -0,0 +1,274 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Cecil.Cil; +using SpecFlowToMarkdown.Domain.TestAssembly; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Step; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Utils; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Scenario +{ + public class MsTestScenarioExtractor : IScenarioExtractor + { + private const string ParameterDeclarationSplit = "Parameter\\:.*"; + private readonly IBuildConfiguration _buildConfiguration; + private readonly ILogger _logger; + private readonly IScenarioArgumentBuilder _scenarioArgumentBuilder; + private readonly IStepExtractor _stepExtractor; + + private readonly IEnumerable _testCaseAttributes = new[] + { + Constants.MsTestTestPropertyAttribute + }; + + public MsTestScenarioExtractor( + ILogger logger, + IStepExtractor stepExtractor, + IScenarioArgumentBuilder scenarioArgumentBuilder, + IBuildConfiguration buildConfiguration + ) + { + _logger = logger; + _stepExtractor = stepExtractor; + _scenarioArgumentBuilder = scenarioArgumentBuilder; + _buildConfiguration = buildConfiguration; + } + + public bool IsApplicable(MethodDefinition method) + { + return + method.IsVirtual || + method + .CustomAttributes + .Any( + o => + o + .AttributeType.FullName == Constants.MsTestTestAttribute + ); + } + + public SpecFlowScenario Perform(MethodDefinition method, TypeDefinition type, ref string environment) + { + // Extract Scenario + var title = string.Empty; + var description = string.Empty; + var tags = new List(); + var scenarioCases = new List(); + + var scenarioArgumentNames = new Dictionary(); + + foreach (var instruction in + method + .Body + .Instructions + .Where(o => o.OpCode == OpCodes.Newobj)) + { + if (instruction.Operand is MethodReference mr) + { + if (mr.DeclaringType.FullName == Constants.ScenarioInfoTypeName) + { + var currInstr = + instruction + .StepPrevious(4); + + if (currInstr.OpCode == OpCodes.Ldstr) + { + description = + currInstr + .Operand + .ToString(); + } + + currInstr = + currInstr + .Previous; + + if (currInstr.OpCode == OpCodes.Ldstr) + { + title = + currInstr + .Operand + .ToString(); + + _logger + .LogInformation($"Extracted scenario: [{title}]"); + } + + var testCaseFunctions = + type + .Methods + .Where( + x => + x.Body + .Instructions + .Any( + o => + { + if (o.OpCode == OpCodes.Callvirt && o.Operand is MethodReference methodReference) + { + var methodName = methodReference.Name; + return methodName == method.Name; + } + + return false; + } + ) + ) + .ToList(); + + var testCases = + testCaseFunctions + .Count; + + if (testCases > 0) + { + _logger + .LogInformation($"Scenario [{title}]; found {testCases} test cases"); + + scenarioArgumentNames = + _scenarioArgumentBuilder + .Build(currInstr); + + currInstr = + currInstr + .StepPrevious(15); + + // Get test case argument values + foreach (var testCaseFunction in testCaseFunctions) + { + var specFlowArguments = new List(); + var caseAttributes = + testCaseFunction + .CustomAttributes + .Where(o => _testCaseAttributes.Contains(o.AttributeType.FullName)) + .ToList(); + + foreach (var caseAttribute in caseAttributes) + { + var constructorArguments = + caseAttribute + .ConstructorArguments; + + if (constructorArguments.Count == 2) + { + var parameterNameArg = + constructorArguments[0] + .Value + .ToString(); + + var fieldMatch = + new Regex(ParameterDeclarationSplit) + .Matches(parameterNameArg); + + if (fieldMatch.Count > 0) + { + var argName = + fieldMatch[0] + .Value + .Split(":") + [1]; + + var argValue = + constructorArguments[1] + .Value + .ToString(); + + specFlowArguments + .Add( + new SpecFlowArgument + { + ArgumentName = argName, + ArgumentValue = argValue + } + ); + } + } + } + + var scenarioCase = new SpecFlowCase + { + Arguments = specFlowArguments + }; + + _logger + .LogInformation( + $"Extracted test case: {{{string.Join(", ", scenarioCase.Arguments.Select(o => $"\"{o.ArgumentName}\":\"{o.ArgumentValue}\""))}}}" + ); + + scenarioCases + .Add(scenarioCase); + } + + var startingInstruction = currInstr; + + foreach (var buildConfiguration in _buildConfiguration.Get()) + { + currInstr = startingInstruction; + + currInstr = + currInstr + .StepPrevious(buildConfiguration.Item4); + + if (currInstr?.OpCode != OpCodes.Ldstr) + continue; + + environment = buildConfiguration.Item1; + break; + } + } + else + { + currInstr = + currInstr + .StepPrevious(5); + } + + while (currInstr?.OpCode == OpCodes.Ldstr) + { + if (currInstr.Operand != null) + { + tags.Add( + currInstr + .Operand.ToString()? + .Replace( + ",", + "" + ) + ); + } + + currInstr = + currInstr + .StepPrevious(4); + } + } + } + } + + var scenario = new SpecFlowScenario + { + Title = title, + Description = description, + Tags = tags + }; + + // Extract Steps + var scenarioSteps = + _stepExtractor + .Perform( + method, + scenarioArgumentNames + ); + + scenario.Steps = scenarioSteps; + scenario.Cases = scenarioCases; + + return scenario; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/XorNUnitScenarioExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/XorNUnitScenarioExtractor.cs new file mode 100644 index 0000000..7f33659 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Scenario/XorNUnitScenarioExtractor.cs @@ -0,0 +1,287 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Cecil.Cil; +using SpecFlowToMarkdown.Domain.TestAssembly; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Step; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Utils; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Scenario +{ + public class XorNUnitScenarioExtractor : IScenarioExtractor + { + private static readonly string[] XOrNUnitTestAttributeValues = + { + Constants.NUnitTestAttribute, + Constants.XUnitFactAttribute, + Constants.XUnitTheoryAttribute + }; + + private readonly ILogger _logger; + private readonly IScenarioArgumentBuilder _scenarioArgumentBuilder; + private readonly IStepExtractor _stepExtractor; + private readonly IBuildConfiguration _buildConfiguration; + + private readonly IEnumerable _testCaseAttributes = new[] + { + Constants.NUnitTestCaseAttribute, + Constants.XUnitInlineDataAttribute + }; + + public XorNUnitScenarioExtractor( + ILogger logger, + IStepExtractor stepExtractor, + IScenarioArgumentBuilder scenarioArgumentBuilder, + IBuildConfiguration buildConfiguration + ) + { + _logger = logger; + _stepExtractor = stepExtractor; + _scenarioArgumentBuilder = scenarioArgumentBuilder; + _buildConfiguration = buildConfiguration; + } + + public bool IsApplicable(MethodDefinition method) + { + return + method + .CustomAttributes + .Any( + o => + XOrNUnitTestAttributeValues + .Contains( + o + .AttributeType.FullName + ) + ); + } + + public SpecFlowScenario Perform(MethodDefinition method, TypeDefinition type, ref string environment) + { + // Extract Scenario + var title = string.Empty; + var description = string.Empty; + var tags = new List(); + var scenarioCases = new List(); + + var scenarioArgumentNames = new Dictionary(); + var scenarioArgumentValues = new List>(); + + var testCases = + method + .CustomAttributes + .Count( + o => + _testCaseAttributes + .Contains(o.AttributeType.FullName) + ); + + foreach (var instruction in + method + .Body + .Instructions + .Where(o => o.OpCode == OpCodes.Newobj)) + { + if (instruction.Operand is not MethodReference mr) continue; + + if (mr.DeclaringType.FullName != Constants.ScenarioInfoTypeName) continue; + + var currInstr = + instruction + .StepPrevious(5); + + if (currInstr.OpCode == OpCodes.Ldstr) + { + description = + currInstr + .Operand + .ToString(); + } + + currInstr = + currInstr + .Previous; + + if (currInstr.OpCode == OpCodes.Ldstr) + { + title = + currInstr + .Operand + .ToString(); + + _logger + .LogInformation($"Extracted scenario: [{title}]"); + } + + // Extract test cases + if (testCases > 0) + { + _logger + .LogInformation($"Scenario [{title}]; found {testCases} test cases"); + + scenarioArgumentNames = + _scenarioArgumentBuilder + .Build(currInstr); + + currInstr = + currInstr + .StepPrevious(15); + + // Get test case argument values + var caseAttributes = + method + .CustomAttributes + .Where(o => _testCaseAttributes.Contains(o.AttributeType.FullName)) + .ToList(); + + foreach (var caseAttribute in caseAttributes) + { + var constructorArguments = + caseAttribute + .ConstructorArguments; + + if (constructorArguments.Count == 1) + { + var constructorArgument = constructorArguments[0]; + if (constructorArgument.Type.IsArray) + { + if (constructorArgument.Value is CustomAttributeArgument[] args) + { + var values = + args + .Select( + o => + { + if (o.Value is CustomAttributeArgument ca) + { + return + ca + .Value? + .ToString(); + } + + return null; + } + ) + .ToList(); + + scenarioArgumentValues + .Add(values); + } + } + } + } + + scenarioArgumentNames = + scenarioArgumentNames + .Reverse() + .ToDictionary( + o => o.Key, + o => o.Value + ); + + for (var i = 0; i < scenarioArgumentValues.Count; i++) + { + var argumentList = + scenarioArgumentNames + .Select( + o => + new SpecFlowArgument + { + ArgumentName = o.Value, + ArgumentValue = + scenarioArgumentValues[i] + .ElementAt( + scenarioArgumentNames + .Keys + .ToList() + .IndexOf(o.Key) + ) + } + ) + .ToList(); + + var scenarioCase = new SpecFlowCase + { + Arguments = argumentList + }; + + _logger + .LogInformation( + $"Extracted test case: {{{string.Join(", ", scenarioCase.Arguments.Select(o => $"\"{o.ArgumentName}\":\"{o.ArgumentValue}\""))}}}" + ); + + scenarioCases + .Add(scenarioCase); + } + + var startingInstruction = currInstr; + + foreach (var buildConfiguration in _buildConfiguration.Get()) + { + currInstr = startingInstruction; + + currInstr = + currInstr + .StepPrevious(buildConfiguration.Item4); + + if (currInstr?.OpCode != OpCodes.Ldstr) + continue; + + environment = buildConfiguration.Item1; + break; + } + } + else + { + currInstr = + currInstr + .StepPrevious(5); + } + + while (currInstr?.OpCode == OpCodes.Ldstr) + { + if (currInstr.Operand != null) + { + tags.Add( + currInstr + .Operand.ToString()? + .Replace( + ",", + "" + ) + ); + } + + currInstr = + currInstr + .StepPrevious(4); + } + } + + var scenario = new SpecFlowScenario + { + Title = title, + Description = description, + Tags = tags + }; + + // Extract Steps + var scenarioSteps = + _stepExtractor + .Perform( + method, + scenarioArgumentNames + ); + + scenario.Steps = scenarioSteps; + scenario.Cases = scenarioCases; + + return scenario; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioArgumentBuilder.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioArgumentBuilder.cs new file mode 100644 index 0000000..5e46038 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioArgumentBuilder.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Mono.Cecil.Cil; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors +{ + public class ScenarioArgumentBuilder : IScenarioArgumentBuilder + { + private readonly IBuildConfiguration _buildConfiguration; + + public ScenarioArgumentBuilder(IBuildConfiguration buildConfiguration) + { + _buildConfiguration = buildConfiguration; + } + + public Dictionary Build(Instruction currInstr) + { + var startingInstruction = currInstr; + var scenarioArgumentNames = new Dictionary(); + + foreach (var buildConfiguration in _buildConfiguration.Get()) + { + currInstr = startingInstruction; + + // Get test case argument names + currInstr = + currInstr + .StepPrevious(buildConfiguration.Item2); + + while (true) + { + string argKey = null; + string argValue = null; + + if (currInstr.OpCode == OpCodes.Ldarg_S) + { + argKey = + currInstr + .Operand + .ToString(); + } + else if (currInstr.OpCode == OpCodes.Ldarg_3) + argKey = "3"; + else if (currInstr.OpCode == OpCodes.Ldarg_2) + argKey = "2"; + else if (currInstr.OpCode == OpCodes.Ldarg_1) + argKey = "1"; + else if (currInstr.OpCode == OpCodes.Ldarg_0) + argKey = "0"; + + var stringLoadInstruction = + currInstr + .Previous; + + if (stringLoadInstruction.OpCode == OpCodes.Ldstr) + { + argValue = + stringLoadInstruction + .Operand + .ToString(); + } + + if (!string.IsNullOrEmpty(argKey) && !string.IsNullOrEmpty(argValue)) + { + scenarioArgumentNames + .Add( + argKey, + argValue + ); + + currInstr = + currInstr + .StepPrevious(buildConfiguration.Item3); + } + else + break; + } + + if (scenarioArgumentNames.Count > 0) + break; + } + + if (scenarioArgumentNames.Count == 0) + throw new Exception( + "Could not extract test cases from assembly; please try again with a different build configuration" + ); + + return + scenarioArgumentNames; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioExtractionHandler.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioExtractionHandler.cs deleted file mode 100644 index 10f95bf..0000000 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/ScenarioExtractionHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Mono.Cecil; -using SpecFlowToMarkdown.Domain.TestAssembly; - -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors -{ - public class ScenarioExtractionHandler : IScenarioExtractionHandler - { - private static readonly string[] CustomTestAttributeValues = - { - Constants.NUnitTestAttribute, - Constants.XUnitFactAttribute, - Constants.XUnitTheoryAttribute, - Constants.MsTestTestAttribute - }; - - private readonly IEnumerable _extractors; - - public ScenarioExtractionHandler(IEnumerable extractors) - { - _extractors = extractors; - } - - public IEnumerable ExtractScenarios(TypeDefinition type) - { - var results = new List(); - - foreach (var method in type.Methods) - { - var applicableExtractor = - _extractors - .FirstOrDefault(o => o.IsApplicable(method)); - // .FirstOrDefault(o => o.IsApplicable(attribute.AttributeType.FullName)); - - if (applicableExtractor == null) - continue; - - var scenario = - applicableExtractor - .ExtractScenario( - method, - type - ); - - results - .Add(scenario); - } - - return results; - } - } -} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/IStepExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/IStepExtractor.cs new file mode 100644 index 0000000..d95c530 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/IStepExtractor.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Mono.Cecil; +using SpecFlowToMarkdown.Domain.TestAssembly; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Step +{ + public interface IStepExtractor + { + IEnumerable Perform( + MethodDefinition method, + Dictionary argumentNames); + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/StepExtractor.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/StepExtractor.cs new file mode 100644 index 0000000..9545e24 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Step/StepExtractor.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Cecil.Cil; +using SpecFlowToMarkdown.Domain.TestAssembly; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extensions; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Utils; + +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Step +{ + public class StepExtractor : IStepExtractor + { + private readonly ILogger _logger; + + public StepExtractor(ILogger logger) + { + _logger = logger; + } + + public IEnumerable Perform(MethodDefinition method, Dictionary argumentNames) + { + // Extract Steps + var scenarioSteps = new List(); + + foreach (var instruction in + method + .Body + .Instructions + .Where(o => o.OpCode == OpCodes.Callvirt)) + { + if (instruction.Operand is MethodReference methodReference) + { + if (Constants.ScenarioStepFunctions.Any(o => methodReference.Name == o)) + { + var keyword = methodReference.Name; + var text = string.Empty; + + var currInstr = + instruction + .StepPrevious(4); + + if (currInstr.OpCode == OpCodes.Ldstr) + { + text = + currInstr + .Operand + .ToString(); + } + else if (currInstr.OpCode == OpCodes.Call) + { + if (currInstr.Operand is MethodReference mr) + { + if (mr.Name == Constants.StringFormatFunctionName) + { + var stringFormatArguments = new List(); + string preFormatText = null; + + while (true) + { + string argKey = null; + + currInstr = + currInstr + .Previous; + + if (currInstr.OpCode == OpCodes.Ldarg_S) + { + argKey = + currInstr + .Operand + .ToString(); + } + else if (currInstr.OpCode == OpCodes.Ldarg_3) + argKey = "3"; + else if (currInstr.OpCode == OpCodes.Ldarg_2) + argKey = "2"; + else if (currInstr.OpCode == OpCodes.Ldarg_1) + argKey = "1"; + else if (currInstr.OpCode == OpCodes.Ldarg_0) + argKey = "0"; + else if (currInstr.OpCode == OpCodes.Ldstr) + preFormatText = + currInstr + .Operand + .ToString(); + + if (string.IsNullOrEmpty(argKey)) + break; + + if (argumentNames.TryGetValue( + argKey, + out var argumentName + )) + { + stringFormatArguments + .Add($"<{argumentName}>"); + } + } + + if (!string.IsNullOrEmpty(preFormatText) && stringFormatArguments.Any()) + { + stringFormatArguments + .Reverse(); + + text = + string + .Format( + preFormatText, + stringFormatArguments + .ToArray() + ); + } + } + else + { + if (currInstr.OpCode == OpCodes.Ldstr) + { + text = + currInstr + .Operand + .ToString(); + } + } + } + } + + var executionStep = new SpecFlowExecutionStep + { + Keyword = keyword, + Text = text + }; + + _logger + .LogInformation($"Extracted test step [{executionStep.Keyword} {executionStep.Text}]"); + + scenarioSteps + .Add(executionStep); + } + } + } + + return + scenarioSteps; + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Constants.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Utils/Constants.cs similarity index 76% rename from src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Constants.cs rename to src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Utils/Constants.cs index 97915a5..95f0939 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Extractors/Constants.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.AssemblyLoad/Utils/Constants.cs @@ -1,7 +1,11 @@ -namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors +namespace SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Utils { internal static class Constants { + public const string CustomFeatureAttributeValue = "TechTalk.SpecFlow"; + public const string FeatureSetupMethodName = "FeatureSetup"; + public const string FeatureInfoTypeName = "TechTalk.SpecFlow.FeatureInfo"; + public const string MsTestTestAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute"; public const string NUnitTestAttribute = "NUnit.Framework.TestAttribute"; public const string XUnitFactAttribute = "Xunit.SkippableFactAttribute"; diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ColourSorter.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ColourSorter.cs index fed70bd..7ab92a8 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ColourSorter.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ColourSorter.cs @@ -9,9 +9,9 @@ public class ColourSorter : IColourSorter public const string PassColourTransparent = "#16c60c88"; public const string FailColourTransparent = "#f03a1788"; public const string OtherColourTransparent = "#fff8"; - public const string PassColourSolid = "#676a6d"; - public const string FailColourSolid = "#105512"; - public const string OtherColourSolid = "#622116"; + public const string PassColourSolid = "#105512"; + public const string FailColourSolid = "#622116"; + public const string OtherColourSolid = "#676a6d"; public ICollection Sort(int passCount, int failCount, int otherCount) { diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Extensions/ChartBuilders.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Extensions/ChartBuilders.cs index 51a7433..3ecd84b 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Extensions/ChartBuilders.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Extensions/ChartBuilders.cs @@ -31,7 +31,7 @@ IDictionary results .AppendLine("\t\t\t\t'yAxisTickColor': \"#fff\",") .AppendLine("\t\t\t\t'yAxisLineColor': \"#fff\",") .AppendLine($"\t\t\t\t'backgroundColor': \"#0000\",") - .AppendLine($"\t\t\t\t'plotColorPalette': \"{ColourSorter.OtherColourSolid}, {ColourSorter.PassColourSolid}, {ColourSorter.FailColourSolid}\""); + .AppendLine($"\t\t\t\t'plotColorPalette': \"{ColourSorter.PassColourSolid}, {ColourSorter.FailColourSolid}, {ColourSorter.OtherColourSolid}\""); stringBuilder .AppendLine("\t\t\t}") @@ -54,25 +54,28 @@ IDictionary results var values = results.Values; - var totalValues = string.Join( + // Reworked slightly to a 'stacked' chart instead of a standard bar chart. + // The next value in the series will be a concurrent sum so as not to 'overlap'. + // Successes first, then failures, then other. + var successValues = string.Join( ", ", values.Select(o => o.Successes + o.Failures + o.Others) ); - var successValues = string.Join( + var failureValues = string.Join( ", ", - values.Select(o => o.Successes) + values.Select(o => o.Failures + o.Others) ); - - var failureValues = string.Join( + + var otherValues = string.Join( ", ", - values.Select(o => o.Failures) + values.Select(o => o.Others) ); stringBuilder - .AppendLine($"bar [{totalValues}]") .AppendLine($"bar [{successValues}]") - .AppendLine($"bar [{failureValues}]"); + .AppendLine($"bar [{failureValues}]") + .AppendLine($"bar [{otherValues}]"); stringBuilder .AppendLine("```"); diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/MarkdownRenderer.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/MarkdownRenderer.cs index 80bd3d3..639321b 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/MarkdownRenderer.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/MarkdownRenderer.cs @@ -44,7 +44,8 @@ TestExecution execution // Render header headerBuilder - .AppendLine($"# {assembly.AssemblyName}"); + .AppendLine($"# {assembly.AssemblyName}") + .AppendLine($"##### *Build configuration: {assembly.BuildConfiguration}*"); headerBuilder .AppendLine("") diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Renderer/CasesRenderer.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Renderer/CasesRenderer.cs index 789d1a2..092149e 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Renderer/CasesRenderer.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/Renderer/CasesRenderer.cs @@ -135,6 +135,9 @@ TestExecution execution .AppendLine( $"

:{caseStatus.ToStatusIcon()}: Case #{caseIndex}

" ); + + contentBuilder + .AppendTags(specFlowScenario); contentBuilder .Append(argumentsBuilder); diff --git a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ResultSummariser.cs b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ResultSummariser.cs index 0b564ac..5099434 100644 --- a/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ResultSummariser.cs +++ b/src/6.0/SpecFlowToMarkdown.Infrastructure.Markdown/ResultSummariser.cs @@ -90,7 +90,10 @@ public static TestSummary SummariseAllScenarios(TestExecution execution) .Count(o => o.Status == StatusError), Others = executionResults - .Count(o => o.Status != StatusOk && o.Status != StatusError) + .Count(o => o.Status != StatusOk && o.Status != StatusError), + Duration = + executionResults + .Sum(o => o.StepResults.Sum(x => x.Duration.GetValueOrDefault().TotalSeconds)) }; } @@ -112,7 +115,10 @@ public static TestSummary SummariseAllSteps(TestExecution execution) .Count(o => o.Status == StatusError), Others = stepResults - .Count(o => o.Status != StatusOk && o.Status != StatusError) + .Count(o => o.Status != StatusOk && o.Status != StatusError), + Duration = + stepResults + .Sum(o => o.Duration.GetValueOrDefault().TotalSeconds) }; } @@ -145,27 +151,40 @@ public static IDictionary SummariseAllTags(TestExecution ex foreach (var taggedScenario in allTaggedScenarios) { - var executionResult = - execution - .ExecutionResults - .FirstOrDefault( - o => o.FeatureTitle == feature.Title && - o.ScenarioTitle == taggedScenario.Title - ); - - if (executionResult != null) + foreach (var taggedCase in + (taggedScenario.Cases ?? new List()).Any() + ? taggedScenario.Cases + : new List { new() }) { - switch (executionResult.Status.ToStatusEnum()) + var executionResult = + execution + .ExecutionResults + .FirstOrDefault( + o => + { + return o.FeatureTitle == feature.Title && + o.ScenarioTitle == taggedScenario.Title && + MatchesTestCase( + taggedCase?.Arguments?.ToList(), + o.ScenarioArguments.ToList() + ); + } + ); + + if (executionResult != null) { - case TestStatusEnum.Success: - result.Successes++; - break; - case TestStatusEnum.Failure: - result.Failures++; - break; - default: - result.Others++; - break; + switch (executionResult.Status.ToStatusEnum()) + { + case TestStatusEnum.Success: + result.Successes++; + break; + case TestStatusEnum.Failure: + result.Failures++; + break; + default: + result.Others++; + break; + } } } } @@ -194,5 +213,18 @@ public static TestStatusEnum Assess(int successes, int failures, int others) if (successes > 0) return TestStatusEnum.Success; return TestStatusEnum.Other; } + + private static bool MatchesTestCase(ICollection taggedCaseArguments, ICollection scenarioArguments) + { + if (taggedCaseArguments == null) return true; + var isMatch = true; + + for (var i = 0; i < scenarioArguments.Count(); i++) + { + isMatch &= scenarioArguments.ElementAt(i) == taggedCaseArguments.ElementAt(i).ArgumentValue.ToString(); + } + + return isMatch; + } } } \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Tests.Unit/ResultSummariserTests.cs b/src/6.0/SpecFlowToMarkdown.Tests.Unit/ResultSummariserTests.cs new file mode 100644 index 0000000..68d5275 --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Tests.Unit/ResultSummariserTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using SpecFlowToMarkdown.Domain.Result; +using SpecFlowToMarkdown.Infrastructure.Markdown; +using SpecFlowToMarkdown.Infrastructure.Markdown.Definition; +using Xunit; + +namespace SpecFlowToMarkdown.Tests.Unit +{ + public class ResultSummariserTests + { + private readonly TestContext _context = new(); + + [Fact] + public void Test_Summarise_Scenario_One_By_Feature() + { + _context.ArrangeScenarioOne(); + _context.ActSummariseResultByFeature(); + _context.AssertScenarioOneSummarisedByFeature(); + } + + [Fact] + public void Test_Summarise_Scenario_One_By_Scenario() + { + _context.ArrangeScenarioOne(); + _context.ActSummariseResultByScenario(); + _context.AssertScenarioOneSummarisedByScenario(); + } + + [Fact] + public void Test_Summarise_Scenario_One_By_Steps() + { + _context.ArrangeScenarioOne(); + _context.ActSummariseResultBySteps(); + _context.AssertScenarioOneSummarisedBySteps(); + } + + private class TestContext + { + private TestExecution _value; + private TestSummary _result; + + public void ArrangeScenarioOne() + { + _value = SampleExecutionResults.TestRunOne; + } + + public void ActSummariseResultByFeature() => + _result = + ResultSummariser + .SummariseAllFeatures(_value); + + public void ActSummariseResultByScenario() => + _result = + ResultSummariser + .SummariseAllScenarios(_value); + + public void ActSummariseResultBySteps() => + _result = + ResultSummariser + .SummariseAllSteps(_value); + + public void AssertScenarioOneSummarisedByFeature() + { + Assert.Equal(1, _result.Successes); + Assert.Equal(1, _result.Failures); + Assert.Equal(0, _result.Others); + Assert.Equal(10, _result.Duration); + } + + public void AssertScenarioOneSummarisedByScenario() + { + Assert.Equal(3, _result.Successes); + Assert.Equal(1, _result.Failures); + Assert.Equal(1, _result.Others); + Assert.Equal(10, _result.Duration); + } + + public void AssertScenarioOneSummarisedBySteps() + { + Assert.Equal(6, _result.Successes); + Assert.Equal(1, _result.Failures); + Assert.Equal(2, _result.Others); + Assert.Equal(10, _result.Duration); + } + } + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Tests.Unit/SampleExecutionResults.cs b/src/6.0/SpecFlowToMarkdown.Tests.Unit/SampleExecutionResults.cs new file mode 100644 index 0000000..2d9755f --- /dev/null +++ b/src/6.0/SpecFlowToMarkdown.Tests.Unit/SampleExecutionResults.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using SpecFlowToMarkdown.Domain.Result; + +namespace SpecFlowToMarkdown.Tests.Unit +{ + public static class SampleExecutionResults + { + private static readonly TimeSpan OneSecond = new( + 0, + 0, + 0, + 0, + 1000 + ); + + private static readonly TimeSpan TwoSeconds = new( + 0, + 0, + 0, + 0, + 2000 + ); + + public static readonly TestExecution TestRunOne = new() + { + ExecutionResults = new List + { + new() + { + FeatureTitle = "Pets", + ScenarioTitle = "Pet my cat", + ScenarioArguments = new List + { + "my_argument_1", + "my_argument_2", + "my_argument_3" + }, + Status = "OK", + StepResults = new List + { + new() + { + Duration = OneSecond, + Status = "OK" + } + } + }, + new() + { + FeatureTitle = "Pets", + ScenarioTitle = "Wash my cat", + Status = "OK", + StepResults = new List + { + new() + { + Duration = OneSecond, + Status = "OK" + }, + new() + { + Duration = OneSecond, + Status = "OK" + } + } + }, + new() + { + FeatureTitle = "Cars", + ScenarioTitle = "Wash my car", + Status = "OK", + StepResults = new List + { + new() + { + Duration = OneSecond, + Status = "OK" + }, + new() + { + Duration = OneSecond, + Status = "OK" + } + } + }, + new() + { + FeatureTitle = "Cars", + ScenarioTitle = "Pet my car", + Status = "TestError", + StepResults = new List + { + new() + { + Duration = OneSecond, + Status = "OK" + }, + new() + { + Duration = OneSecond, + Status = "TestError" + } + } + }, + new() + { + FeatureTitle = "Cars", + ScenarioTitle = "Repair my car", + Status = "Skipped", + StepResults = new List + { + new() + { + Duration = TwoSeconds, + Status = "Skipped" + }, + new() + { + Duration = OneSecond, + Status = "Skipped" + } + } + } + } + }; + } +} \ No newline at end of file diff --git a/src/6.0/SpecFlowToMarkdown.Tests.Unit/SpecFlowToMarkdown.Tests.Unit.csproj b/src/6.0/SpecFlowToMarkdown.Tests.Unit/SpecFlowToMarkdown.Tests.Unit.csproj index 7519a73..9e3af31 100644 --- a/src/6.0/SpecFlowToMarkdown.Tests.Unit/SpecFlowToMarkdown.Tests.Unit.csproj +++ b/src/6.0/SpecFlowToMarkdown.Tests.Unit/SpecFlowToMarkdown.Tests.Unit.csproj @@ -20,6 +20,7 @@ + diff --git a/src/6.0/SpecFlowToMarkdown.Tool/Startup.cs b/src/6.0/SpecFlowToMarkdown.Tool/Startup.cs index d92b8d3..b7fd352 100644 --- a/src/6.0/SpecFlowToMarkdown.Tool/Startup.cs +++ b/src/6.0/SpecFlowToMarkdown.Tool/Startup.cs @@ -2,8 +2,11 @@ using Microsoft.Extensions.Logging; using SpecFlowToMarkdown.Application; using SpecFlowToMarkdown.Infrastructure.AssemblyLoad; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Configuration; using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors; -using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Providers; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Feature; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Scenario; +using SpecFlowToMarkdown.Infrastructure.AssemblyLoad.Extractors.Step; using SpecFlowToMarkdown.Infrastructure.Io; using SpecFlowToMarkdown.Infrastructure.Markdown; using SpecFlowToMarkdown.Infrastructure.Parsing.Arguments; @@ -21,9 +24,11 @@ public static IServiceCollection AddServices() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton()