diff --git a/.gitignore b/.gitignore index b31d83a12..45b958610 100644 --- a/.gitignore +++ b/.gitignore @@ -340,7 +340,6 @@ obj/ [Rr]elease*/ _ReSharper*/ _NCrunch*/ -[Tt]est[Rr]esult* *.pidb *.userprefs *.resharper @@ -382,6 +381,7 @@ GitExtensions.settings.backup /Installer/NuGetPackages/SpecFlow.Tools.MsBuild.Generation/build/SpecFlow.Tools.MsBuild.Generation.props /Tests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests/Features/dummy.feature.cs *.feature.cs +/Tests/TechTalk.SpecFlow.Specs/Features/CucumberMessages # Nerdbank.GitVersioning Tests/TechTalk.SpecFlow.Specs/NuGetPackageVersion.cs \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d8e7bb8c4..211eee196 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ExternalRepositories/SpecFlow.TestProjectGenerator"] path = ExternalRepositories/SpecFlow.TestProjectGenerator url = https://github.com/techtalk/SpecFlow.TestProjectGenerator.git +[submodule "ExternalRepositories/cucumber"] + path = ExternalRepositories/cucumber + url = https://github.com/techtalk/cucumber.git diff --git a/ExternalRepositories/SpecFlow.TestProjectGenerator b/ExternalRepositories/SpecFlow.TestProjectGenerator index d14fd47c1..567409cc2 160000 --- a/ExternalRepositories/SpecFlow.TestProjectGenerator +++ b/ExternalRepositories/SpecFlow.TestProjectGenerator @@ -1 +1 @@ -Subproject commit d14fd47c1d19c1182f063f9628c593ab267fa666 +Subproject commit 567409cc23a5fd318efdf39b0bd1f6461a7b9a7c diff --git a/ExternalRepositories/cucumber b/ExternalRepositories/cucumber new file mode 160000 index 000000000..c254bffdd --- /dev/null +++ b/ExternalRepositories/cucumber @@ -0,0 +1 @@ +Subproject commit c254bffdd5819c1d0537b8098f0981a4de7015fa diff --git a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin.csproj b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin.csproj index 57359238e..77ec45afc 100644 --- a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin.csproj +++ b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin.csproj @@ -11,9 +11,15 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + - + diff --git a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.cs b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.cs new file mode 100644 index 000000000..a86a25453 --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using global::Microsoft.VisualStudio.TestTools.UnitTesting; +using global::TechTalk.SpecFlow; + +[TestClass] +public class MSTestAssemblyHooks +{ + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext testContext) + { + var currentAssembly = typeof(MSTestAssemblyHooks).Assembly; + + TestRunnerManager.OnTestRunStart(currentAssembly); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + var currentAssembly = typeof(MSTestAssemblyHooks).Assembly; + + TestRunnerManager.OnTestRunEnd(currentAssembly); + } +} diff --git a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.vb b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.vb new file mode 100644 index 000000000..e12e7bfd7 --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/MSTest.AssemblyHooks.vb @@ -0,0 +1,25 @@ +Imports Microsoft.VisualStudio.TestTools.UnitTesting +Imports TechTalk.SpecFlow +Imports System +Imports System.Reflection + + + + Public NotInheritable Class MSTestAssemblyHooks + + Public Shared Sub AssemblyInitialize(testContext As TestContext) + + Dim currentAssembly As Assembly = GetType(MSTestAssemblyHooks).Assembly + + TestRunnerManager.OnTestRunStart(currentAssembly) + End Sub + + + Public Shared Sub AssemblyCleanup() + + Dim currentAssembly As Assembly = GetType(MSTestAssemblyHooks).Assembly + + TestRunnerManager.OnTestRunEnd(currentAssembly) + End Sub + + End Class diff --git a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/SpecFlow.MsTest.targets b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/SpecFlow.MsTest.targets index 4beca440c..b7ef68899 100644 --- a/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/SpecFlow.MsTest.targets +++ b/Plugins/TechTalk.SpecFlow.MSTest.Generator.SpecFlowPlugin/build/SpecFlow.MsTest.targets @@ -1,5 +1,20 @@  + + + GenerateSpecFlowAssemblyHooksFileTask; + $(BuildDependsOn) + + + $(CleanDependsOn) + + + GenerateSpecFlowAssemblyHooksFileTask; + $(RebuildDependsOn) + + + + <_SpecFlow_MsTestGeneratorPlugin Condition=" '$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 @@ -10,8 +25,16 @@ <_SpecFlow_MsTestRuntimePlugin Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">net45 <_SpecFlow_MsTestRuntimePluginPath>$(MSBuildThisFileDirectory)\..\lib\$(_SpecFlow_MsTestRuntimePlugin)\TechTalk.SpecFlow.MSTest.SpecFlowPlugin.dll + $(MSBuildThisFileDirectory)MSTest.AssemblyHooks$(DefaultLanguageSourceExtension) + true + + + + + + + - \ No newline at end of file diff --git a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin.csproj b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin.csproj index 9e5ea4e84..10045c195 100644 --- a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin.csproj +++ b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin.csproj @@ -11,9 +11,15 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + - + diff --git a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.cs b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.cs new file mode 100644 index 000000000..102c74347 --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using global::NUnit.Framework; +using global::TechTalk.SpecFlow; + +[SetUpFixture] +public class NUnitAssemblyHooks +{ + [OneTimeSetUp] + public void AssemblyInitialize() + { + var currentAssembly = typeof(NUnitAssemblyHooks).Assembly; + + TestRunnerManager.OnTestRunStart(currentAssembly); + } + + [OneTimeTearDown] + public void AssemblyCleanup() + { + var currentAssembly = typeof(NUnitAssemblyHooks).Assembly; + + TestRunnerManager.OnTestRunEnd(currentAssembly); + } +} diff --git a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.vb b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.vb new file mode 100644 index 000000000..002061d47 --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/NUnit.AssemblyHooks.vb @@ -0,0 +1,22 @@ +Imports NUnit.Framework +Imports TechTalk.SpecFlow +Imports System +Imports System.Reflection + + +Public NotInheritable Class NUnitAssemblyHooks + + Public Shared Sub AssemblyInitialize() + Dim currentAssembly As Assembly = GetType(NUnitAssemblyHooks).Assembly + + TestRunnerManager.OnTestRunStart(currentAssembly) + End Sub + + + Public Shared Sub AssemblyCleanup() + Dim currentAssembly As Assembly = GetType(NUnitAssemblyHooks).Assembly + + TestRunnerManager.OnTestRunEnd(currentAssembly) + End Sub + +End Class diff --git a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/SpecFlow.NUnit.targets b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/SpecFlow.NUnit.targets index d20404fd9..a52cb1a1a 100644 --- a/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/SpecFlow.NUnit.targets +++ b/Plugins/TechTalk.SpecFlow.NUnit.Generator.SpecFlowPlugin/build/SpecFlow.NUnit.targets @@ -1,5 +1,20 @@  + + + GenerateSpecFlowAssemblyHooksFileTask; + $(BuildDependsOn) + + + $(CleanDependsOn) + + + GenerateSpecFlowAssemblyHooksFileTask; + $(RebuildDependsOn) + + + + <_SpecFlow_NUnitGeneratorPlugin Condition=" '$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 @@ -10,8 +25,16 @@ <_SpecFlow_NUnitRuntimePlugin Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">net45 <_SpecFlow_NUnitRuntimePluginPath>$(MSBuildThisFileDirectory)\..\lib\$(_SpecFlow_NUnitRuntimePlugin)\TechTalk.SpecFlow.NUnit.SpecFlowPlugin.dll + $(MSBuildThisFileDirectory)NUnit.AssemblyHooks$(DefaultLanguageSourceExtension) + true + + + + + + + - \ No newline at end of file diff --git a/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/NUnitNetFrameworkTestRunContext.cs b/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/NUnitNetFrameworkTestRunContext.cs new file mode 100644 index 000000000..b50a883bb --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/NUnitNetFrameworkTestRunContext.cs @@ -0,0 +1,18 @@ +using System.IO; +using TechTalk.SpecFlow.Plugins; +using TechTalk.SpecFlow.TestFramework; + +namespace TechTalk.SpecFlow.NUnit.SpecFlowPlugin +{ + public class NUnitNetFrameworkTestRunContext : ITestRunContext + { + private readonly ISpecFlowPath _specFlowPath; + + public NUnitNetFrameworkTestRunContext(ISpecFlowPath specFlowPath) + { + _specFlowPath = specFlowPath; + } + + public string GetTestDirectory() => Path.GetDirectoryName(_specFlowPath.GetPathToSpecFlowDll()); + } +} diff --git a/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/RuntimePlugin.cs b/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/RuntimePlugin.cs index 33a133b06..ec4fcc273 100644 --- a/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/RuntimePlugin.cs +++ b/Plugins/TechTalk.SpecFlow.NUnit.SpecFlowPlugin/RuntimePlugin.cs @@ -1,10 +1,10 @@ using TechTalk.SpecFlow.Infrastructure; using TechTalk.SpecFlow.NUnit.SpecFlowPlugin; using TechTalk.SpecFlow.Plugins; +using TechTalk.SpecFlow.TestFramework; using TechTalk.SpecFlow.Tracing; using TechTalk.SpecFlow.UnitTestProvider; - [assembly: RuntimePlugin(typeof(RuntimePlugin))] namespace TechTalk.SpecFlow.NUnit.SpecFlowPlugin @@ -13,10 +13,18 @@ public class RuntimePlugin : IRuntimePlugin { public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + runtimePluginEvents.CustomizeGlobalDependencies += RuntimePluginEvents_CustomizeGlobalDependencies; runtimePluginEvents.CustomizeScenarioDependencies += RuntimePluginEvents_CustomizeScenarioDependencies; unitTestProviderConfiguration.UseUnitTestProvider("nunit"); } + private void RuntimePluginEvents_CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEventArgs e) + { +#if NETFRAMEWORK + e.ObjectContainer.RegisterTypeAs(); +#endif + } + private void RuntimePluginEvents_CustomizeScenarioDependencies(object sender, CustomizeScenarioDependenciesEventArgs e) { var container = e.ObjectContainer; diff --git a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin.csproj b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin.csproj index 55469dc4c..d1dff60de 100644 --- a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin.csproj +++ b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin.csproj @@ -11,9 +11,15 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + - + diff --git a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/SpecFlow.xUnit.targets b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/SpecFlow.xUnit.targets index 483e6a6f6..b30e8de41 100644 --- a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/SpecFlow.xUnit.targets +++ b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/SpecFlow.xUnit.targets @@ -1,5 +1,19 @@  + + + GenerateSpecFlowAssemblyHooksFileTask; + $(BuildDependsOn) + + + $(CleanDependsOn) + + + GenerateSpecFlowAssemblyHooksFileTask; + $(RebuildDependsOn) + + + <_SpecFlow_xUnitGeneratorPlugin Condition=" '$(MSBuildRuntimeType)' == 'Core'" >netstandard2.0 @@ -10,8 +24,16 @@ <_SpecFlow_xUnitRuntimePlugin Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">net45 <_SpecFlow_xUnitRuntimePluginPath>$(MSBuildThisFileDirectory)\..\lib\$(_SpecFlow_xUnitRuntimePlugin)\TechTalk.SpecFlow.xUnit.SpecFlowPlugin.dll + $(MSBuildThisFileDirectory)xUnit.AssemblyHooks$(DefaultLanguageSourceExtension) + true + + + + + + \ No newline at end of file diff --git a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.cs b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.cs new file mode 100644 index 000000000..be46c8ec6 --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.cs @@ -0,0 +1,17 @@ +using global::System; +using global::Xunit; +using global::TechTalk.SpecFlow; + +namespace InternalSpecFlow +{ + public class XUnitAssemblyFixture + { + static XUnitAssemblyFixture() + { + var currentAssembly = typeof(XUnitAssemblyFixture).Assembly; + + TestRunnerManager.OnTestRunStart(currentAssembly); + } + } +} + diff --git a/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.vb b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.vb new file mode 100644 index 000000000..77bd0696d --- /dev/null +++ b/Plugins/TechTalk.SpecFlow.xUnit.Generator.SpecFlowPlugin/build/xUnit.AssemblyHooks.vb @@ -0,0 +1,13 @@ +Namespace InternalSpecFlow + + Public Class XUnitAssemblyFixture + + Shared Sub New() + Dim currentAssembly As System.Reflection.Assembly = GetType(XUnitAssemblyFixture).Assembly + + Global.TechTalk.SpecFlow.TestRunnerManager.OnTestRunStart(currentAssembly) + End Sub + + End Class + +End Namespace diff --git a/TechTalk.SpecFlow.Generator/UnitTestProvider/UnitTestGeneratorProviders.cs b/TechTalk.SpecFlow.Generator/UnitTestProvider/UnitTestGeneratorProviders.cs index cd1ede6d7..e1e9cc6f8 100644 --- a/TechTalk.SpecFlow.Generator/UnitTestProvider/UnitTestGeneratorProviders.cs +++ b/TechTalk.SpecFlow.Generator/UnitTestProvider/UnitTestGeneratorProviders.cs @@ -1,24 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BoDi; -using TechTalk.SpecFlow.Generator.UnitTestProvider; - -namespace TechTalk.SpecFlow.Generator -{ - partial class DefaultDependencyProvider - { - partial void RegisterUnitTestGeneratorProviders(ObjectContainer container) - { - container.RegisterTypeAs("nunit.2"); - container.RegisterTypeAs("nunit"); - container.RegisterTypeAs("mbunit"); - container.RegisterTypeAs("mbunit.3"); - container.RegisterTypeAs("xunit.1"); - container.RegisterTypeAs("xunit"); +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using BoDi; +using TechTalk.SpecFlow.Generator.UnitTestProvider; + +namespace TechTalk.SpecFlow.Generator +{ + partial class DefaultDependencyProvider + { + partial void RegisterUnitTestGeneratorProviders(ObjectContainer container) + { + container.RegisterTypeAs("nunit.2"); + container.RegisterTypeAs("nunit"); + container.RegisterTypeAs("mbunit"); + container.RegisterTypeAs("mbunit.3"); + container.RegisterTypeAs("xunit.1"); + container.RegisterTypeAs("xunit"); container.RegisterTypeAs("mstest"); - container.RegisterTypeAs("mstest.v1"); - } - } -} + container.RegisterTypeAs("mstest.v1"); + } + } +} diff --git a/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs b/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs index 2ff8206ae..0af212d3e 100644 --- a/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs +++ b/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnit2TestGeneratorProvider.cs @@ -22,12 +22,18 @@ public class XUnit2TestGeneratorProvider : XUnitTestGeneratorProvider protected const string COLLECTION_DEF = "Xunit.Collection"; protected const string COLLECTION_TAG = "xunit:collection"; - public XUnit2TestGeneratorProvider(CodeDomHelper codeDomHelper) - :base(codeDomHelper) + public XUnit2TestGeneratorProvider(CodeDomHelper codeDomHelper) : base(codeDomHelper) { CodeDomHelper = codeDomHelper; } + public override void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription) + { + //SetTestClassCollection(generationContext, "xunit:collection(SpecFlowXUnitHooks)"); + + base.SetTestClass(generationContext, featureTitle, featureDescription); + } + public override UnitTestGeneratorTraits GetTraits() { return UnitTestGeneratorTraits.RowTests | UnitTestGeneratorTraits.ParallelExecution; @@ -82,6 +88,17 @@ protected override void SetTestConstructor(TestClassGenerationContext generation new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), OUTPUT_INTERFACE_FIELD_NAME), new CodeVariableReferenceExpression(OUTPUT_INTERFACE_PARAMETER_NAME))); + var typeName = "InternalSpecFlow.XUnitAssemblyFixture"; + //if (CodeDomHelper.TargetLanguage == CodeDomProviderLanguage.VB) + //{ + // typeName = "Global.XUnitAssemblyFixture"; + //} + + ctorMethod.Statements.Add( + new CodeVariableDeclarationStatement(new CodeTypeReference(typeName), "assemblyFixture", + new CodeObjectCreateExpression(new CodeTypeReference(typeName)))); + + base.SetTestConstructor(generationContext, ctorMethod); } @@ -111,14 +128,15 @@ public override void SetTestMethodIgnore(TestClassGenerationContext generationCo ); } } - public override void SetTestClassCategories(TestClassGenerationContext generationContext, IEnumerable featureCategories) + public override void SetTestClassCategories(TestClassGenerationContext generationContext, IEnumerable featureCategories) { IEnumerable collection = featureCategories.Where(f => f.StartsWith(COLLECTION_TAG, StringComparison.InvariantCultureIgnoreCase)).ToList(); if (collection.Any()) { //Only one 'Xunit.Collection' can exist per class. - SetTestClassCollection(generationContext, collection.FirstOrDefault()); + SetTestClassCollection(generationContext, collection.FirstOrDefault()); } + base.SetTestClassCategories(generationContext, featureCategories); } diff --git a/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnitTestGeneratorProvider.cs b/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnitTestGeneratorProvider.cs index e9661f870..03e68334d 100644 --- a/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnitTestGeneratorProvider.cs +++ b/TechTalk.SpecFlow.Generator/UnitTestProvider/XUnitTestGeneratorProvider.cs @@ -24,21 +24,21 @@ public class XUnitTestGeneratorProvider : IUnitTestGeneratorProvider private CodeTypeDeclaration _currentFixtureDataTypeDeclaration = null; - protected CodeDomHelper CodeDomHelper { get; set; } - - public virtual UnitTestGeneratorTraits GetTraits() + public XUnitTestGeneratorProvider(CodeDomHelper codeDomHelper) { - return UnitTestGeneratorTraits.RowTests; + CodeDomHelper = codeDomHelper; } + protected CodeDomHelper CodeDomHelper { get; set; } + public bool GenerateParallelCodeForFeature { get; set; } - public XUnitTestGeneratorProvider(CodeDomHelper codeDomHelper) + public virtual UnitTestGeneratorTraits GetTraits() { - CodeDomHelper = codeDomHelper; + return UnitTestGeneratorTraits.RowTests; } - public void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription) + public virtual void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription) { // xUnit does not use an attribute for the TestFixture, all public classes are potential fixtures } @@ -47,7 +47,9 @@ public virtual void SetTestClassCategories(TestClassGenerationContext generation { // Set Category trait which can be used with the /trait or /-trait xunit flags to include/exclude tests foreach (string str in featureCategories) + { SetProperty(generationContext.TestClass, CATEGORY_PROPERTY_NAME, str); + } } public virtual void SetTestClassParallelize(TestClassGenerationContext generationContext) @@ -127,7 +129,9 @@ public virtual void SetRow(TestClassGenerationContext generationContext, CodeMem { //TODO: better handle "ignored" if (isIgnored) + { return; + } var args = arguments.Select( arg => new CodeAttributeArgument(new CodePrimitiveExpression(arg))).ToList(); @@ -142,7 +146,9 @@ public virtual void SetRow(TestClassGenerationContext generationContext, CodeMem public virtual void SetTestMethodCategories(TestClassGenerationContext generationContext, CodeMemberMethod testMethod, IEnumerable scenarioCategories) { foreach (string str in scenarioCategories) + { SetProperty((CodeTypeMember)testMethod, "Category", str); + } } public void SetTestInitializeMethod(TestClassGenerationContext generationContext) diff --git a/TechTalk.SpecFlow.sln b/TechTalk.SpecFlow.sln index d16db9e8b..ebbf06f68 100644 --- a/TechTalk.SpecFlow.sln +++ b/TechTalk.SpecFlow.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.572 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28917.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Setup", "Setup", "{DCE0C3C4-5BC6-4A30-86BE-3FEFF4677A01}" EndProject diff --git a/TechTalk.SpecFlow/CommonModels/ExceptionFailure.cs b/TechTalk.SpecFlow/CommonModels/ExceptionFailure.cs new file mode 100644 index 000000000..1461ad754 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/ExceptionFailure.cs @@ -0,0 +1,23 @@ +using System; + +namespace TechTalk.SpecFlow.CommonModels +{ + public class ExceptionFailure : IFailure + { + public ExceptionFailure(Exception exception) + { + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } + + public Exception Exception { get; } + + public override string ToString() => Exception.ToString(); + } + + public class ExceptionFailure : ExceptionFailure, IFailure + { + public ExceptionFailure(Exception exception) : base(exception) + { + } + } +} diff --git a/TechTalk.SpecFlow/CommonModels/Failure.cs b/TechTalk.SpecFlow/CommonModels/Failure.cs new file mode 100644 index 000000000..5d4e60c67 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/Failure.cs @@ -0,0 +1,21 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public class Failure : IFailure + { + public Failure(string description) + { + Description = description; + } + + public string Description { get; } + + public override string ToString() => Description; + } + + public class Failure : Failure, IFailure + { + public Failure(string description) : base(description) + { + } + } +} diff --git a/TechTalk.SpecFlow/CommonModels/IFailure.cs b/TechTalk.SpecFlow/CommonModels/IFailure.cs new file mode 100644 index 000000000..ae14c15b2 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/IFailure.cs @@ -0,0 +1,10 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public interface IFailure : IResult + { + } + + public interface IFailure : IFailure, IResult + { + } +} diff --git a/TechTalk.SpecFlow/CommonModels/IResult.cs b/TechTalk.SpecFlow/CommonModels/IResult.cs new file mode 100644 index 000000000..1b50d521a --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/IResult.cs @@ -0,0 +1,10 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public interface IResult + { + } + + public interface IResult : IResult + { + } +} diff --git a/TechTalk.SpecFlow/CommonModels/ISuccess.cs b/TechTalk.SpecFlow/CommonModels/ISuccess.cs new file mode 100644 index 000000000..80d762230 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/ISuccess.cs @@ -0,0 +1,11 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public interface ISuccess : IResult + { + } + + public interface ISuccess : ISuccess, IResult + { + T Result { get; } + } +} diff --git a/TechTalk.SpecFlow/CommonModels/Result.cs b/TechTalk.SpecFlow/CommonModels/Result.cs new file mode 100644 index 000000000..febedd093 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/Result.cs @@ -0,0 +1,50 @@ +using System; + +namespace TechTalk.SpecFlow.CommonModels +{ + public static class Result + { + public static IResult Success() + { + return new Success(); + } + + public static IResult Failure(string description) + { + return new Failure(description); + } + + public static IResult Failure(Exception exception) + { + return new ExceptionFailure(exception); + } + + public static IResult Failure(string description, IFailure innerFailure) + { + return new WrappedFailure(description, innerFailure); + } + } + + public static class Result + { + public static IResult Success(T value) + { + return new Success(value); + } + + public static IResult Failure(string description) + { + return new Failure(description); + } + + public static IResult Failure(Exception exception) + { + return new ExceptionFailure(exception); + } + + public static IResult Failure(string description, IFailure innerFailure) + { + return new WrappedFailure(description, innerFailure); + } + } +} diff --git a/TechTalk.SpecFlow/CommonModels/Success.cs b/TechTalk.SpecFlow/CommonModels/Success.cs new file mode 100644 index 000000000..4b4e4e7ec --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/Success.cs @@ -0,0 +1,16 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public class Success : ISuccess + { + } + + public class Success : Success, ISuccess + { + public Success(T result) + { + Result = result; + } + + public T Result { get; } + } +} diff --git a/TechTalk.SpecFlow/CommonModels/WrappedFailure.cs b/TechTalk.SpecFlow/CommonModels/WrappedFailure.cs new file mode 100644 index 000000000..27f18aa65 --- /dev/null +++ b/TechTalk.SpecFlow/CommonModels/WrappedFailure.cs @@ -0,0 +1,34 @@ +namespace TechTalk.SpecFlow.CommonModels +{ + public class WrappedFailure : Failure + { + public WrappedFailure(string description, IFailure innerFailure) : base(description) + { + InnerFailure = innerFailure; + } + + public IFailure InnerFailure { get; } + + public string GetStringOfInnerFailure() + { + switch (InnerFailure) + { + case WrappedFailure wrappedFailure: return wrappedFailure.ToString(); + case Failure failure: return failure.Description; + default: return InnerFailure?.ToString(); + } + } + + public override string ToString() + { + return $"{Description}; {GetStringOfInnerFailure()}"; + } + } + + public class WrappedFailure : WrappedFailure, IFailure + { + public WrappedFailure(string description, IFailure innerFailure) : base(description, innerFailure) + { + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/CucumberMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/CucumberMessageFactory.cs new file mode 100644 index 000000000..58ffe3f35 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/CucumberMessageFactory.cs @@ -0,0 +1,110 @@ +using System; +using Google.Protobuf.WellKnownTypes; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class CucumberMessageFactory : ICucumberMessageFactory + { + private const string UsedCucumberImplementationString = @"SpecFlow"; + + public string ConvertToPickleIdString(Guid id) + { + return $"{id:D}"; + } + + public IResult BuildTestRunStartedMessage(DateTime timeStamp) + { + if (timeStamp.Kind != DateTimeKind.Utc) + { + return Result.Failure($"{nameof(timeStamp)} must be an UTC {nameof(DateTime)}. It is {timeStamp.Kind}"); + } + + var testRunStarted = new TestRunStarted + { + Timestamp = Timestamp.FromDateTime(timeStamp), + CucumberImplementation = UsedCucumberImplementationString + }; + + return Result.Success(testRunStarted); + } + + public IResult BuildTestCaseStartedMessage(Guid pickleId, DateTime timeStamp) + { + if (timeStamp.Kind != DateTimeKind.Utc) + { + return Result.Failure($"{nameof(timeStamp)} must be an UTC {nameof(DateTime)}. It is {timeStamp.Kind}"); + } + + var testCaseStarted = new TestCaseStarted + { + Timestamp = Timestamp.FromDateTime(timeStamp), + PickleId = ConvertToPickleIdString(pickleId) + }; + + return Result.Success(testCaseStarted); + } + + public IResult BuildTestCaseFinishedMessage(Guid pickleId, DateTime timeStamp, TestResult testResult) + { + if (testResult is null) + { + return Result.Failure(new ArgumentNullException(nameof(testResult))); + } + + if (timeStamp.Kind != DateTimeKind.Utc) + { + return Result.Failure($"{nameof(timeStamp)} must be an UTC {nameof(DateTime)}. It is {timeStamp.Kind}"); + } + + var testCaseFinished = new TestCaseFinished + { + PickleId = ConvertToPickleIdString(pickleId), + Timestamp = Timestamp.FromDateTime(timeStamp), + TestResult = testResult + }; + + return Result.Success(testCaseFinished); + } + + public IResult BuildWrapperMessage(IResult testRunStarted) + { + switch (testRunStarted) + { + case ISuccess success: + return Result.Success(new Wrapper { TestRunStarted = success.Result }); + case IFailure failure: + return Result.Failure($"{nameof(testRunStarted)} must be an {nameof(ISuccess)}.", failure); + default: + return Result.Failure($"{nameof(testRunStarted)} must be an {nameof(ISuccess)}."); + } + } + + public IResult BuildWrapperMessage(IResult testCaseStarted) + { + switch (testCaseStarted) + { + case ISuccess success: + return Result.Success(new Wrapper { TestCaseStarted = success.Result }); + case IFailure failure: + return Result.Failure($"{nameof(testCaseStarted)} must be an {nameof(ISuccess)}.", failure); + default: + return Result.Failure($"{nameof(testCaseStarted)} must be an {nameof(ISuccess)}."); + } + } + + public IResult BuildWrapperMessage(IResult testCaseFinished) + { + switch (testCaseFinished) + { + case ISuccess success: + return Result.Success(new Wrapper { TestCaseFinished = success.Result }); + case IFailure failure: + return Result.Failure($"{nameof(testCaseFinished)} must be an {nameof(ISuccess)}.", failure); + default: + return Result.Failure($"{nameof(testCaseFinished)} must be an {nameof(ISuccess)}."); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/CucumberMessageSender.cs b/TechTalk.SpecFlow/CucumberMessages/CucumberMessageSender.cs new file mode 100644 index 000000000..cc821509e --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/CucumberMessageSender.cs @@ -0,0 +1,63 @@ +using System; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class CucumberMessageSender : ICucumberMessageSender + { + private readonly ICucumberMessageFactory _cucumberMessageFactory; + private readonly ICucumberMessageSink _cucumberMessageSink; + private readonly IFieldValueProvider _fieldValueProvider; + + public CucumberMessageSender(ICucumberMessageFactory cucumberMessageFactory, ICucumberMessageSink cucumberMessageSink, IFieldValueProvider fieldValueProvider) + { + _cucumberMessageFactory = cucumberMessageFactory; + _cucumberMessageSink = cucumberMessageSink; + _fieldValueProvider = fieldValueProvider; + } + + public void SendTestRunStarted() + { + var nowDateAndTime = _fieldValueProvider.GetTestRunStartedTime(); + var testRunStartedMessageResult = _cucumberMessageFactory.BuildTestRunStartedMessage(nowDateAndTime); + var wrapper = _cucumberMessageFactory.BuildWrapperMessage(testRunStartedMessageResult); + SendMessageOrThrowException(wrapper); + } + + public void SendTestCaseStarted(ScenarioInfo scenarioInfo) + { + var actualPickleId = _fieldValueProvider.GetTestCaseStartedPickleId(scenarioInfo); + var nowDateAndTime = _fieldValueProvider.GetTestCaseStartedTime(); + + var testCaseStartedMessageResult = _cucumberMessageFactory.BuildTestCaseStartedMessage(actualPickleId, nowDateAndTime); + var wrapper = _cucumberMessageFactory.BuildWrapperMessage(testCaseStartedMessageResult); + SendMessageOrThrowException(wrapper); + } + + public void SendTestCaseFinished(ScenarioInfo scenarioInfo, TestResult testResult) + { + var actualPickleId = _fieldValueProvider.GetTestCaseFinishedPickleId(scenarioInfo); + var nowDateAndTime = _fieldValueProvider.GetTestCaseFinishedTime(); + + var testCaseFinishedMessageResult = _cucumberMessageFactory.BuildTestCaseFinishedMessage(actualPickleId, nowDateAndTime, testResult); + var wrapper = _cucumberMessageFactory.BuildWrapperMessage(testCaseFinishedMessageResult); + SendMessageOrThrowException(wrapper); + } + + public void SendMessageOrThrowException(IResult messageResult) + { + switch (messageResult) + { + case ISuccess success: + _cucumberMessageSink.SendMessage(success.Result); + break; + + case WrappedFailure failure: throw new InvalidOperationException($"The message could not be created. {failure}"); + case ExceptionFailure failure: throw failure.Exception; + case Failure failure: throw new InvalidOperationException($"The message could not be created. {failure.Description}"); + default: throw new InvalidOperationException("The message could not be created."); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/FieldValueProvider.cs b/TechTalk.SpecFlow/CucumberMessages/FieldValueProvider.cs new file mode 100644 index 000000000..ef322a662 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/FieldValueProvider.cs @@ -0,0 +1,87 @@ +using System; +using System.Globalization; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.EnvironmentAccess; +using TechTalk.SpecFlow.Time; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class FieldValueProvider : IFieldValueProvider + { + private const string SpecFlowMessagesTestRunStartedTimeOverrideName = "SpecFlow_Messages_TestRunStartedTimeOverride"; + private const string SpecFlowMessagesTestCaseStartedTimeOverrideName = "SpecFlow_Messages_TestCaseStartedTimeOverride"; + private const string SpecFlowMessagesTestCaseStartedPickleIdOverrideName = "SpecFlow_Messages_TestCaseStartedPickleIdOverride"; + private const string SpecFlowMessagesTestCaseFinishedTimeOverrideName = "SpecFlow_Messages_TestCaseFinishedTimeOverride"; + private const string SpecFlowMessagesTestCaseFinishedPickleIdOverrideName = "SpecFlow_Messages_TestCaseFinishedPickleIdOverride"; + private readonly IEnvironmentWrapper _environmentWrapper; + private readonly IClock _clock; + private readonly IPickleIdStore _pickleIdStore; + + public FieldValueProvider(IEnvironmentWrapper environmentWrapper, IClock clock, IPickleIdStore pickleIdStore) + { + _environmentWrapper = environmentWrapper; + _clock = clock; + _pickleIdStore = pickleIdStore; + } + + public bool TryParseUniversalDateTime(string source, out DateTime result) + { + return DateTime.TryParse(source, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out result); + } + + public DateTime GetTestRunStartedTime() + { + if (_environmentWrapper.GetEnvironmentVariable(SpecFlowMessagesTestRunStartedTimeOverrideName) is ISuccess success + && TryParseUniversalDateTime(success.Result, out var dateTime)) + { + return dateTime; + } + + return _clock.GetNowDateAndTime(); + } + + public DateTime GetTestCaseStartedTime() + { + if (_environmentWrapper.GetEnvironmentVariable(SpecFlowMessagesTestCaseStartedTimeOverrideName) is ISuccess success + && TryParseUniversalDateTime(success.Result, out var dateTime)) + { + return dateTime; + } + + return _clock.GetNowDateAndTime(); + } + + public Guid GetTestCaseStartedPickleId(ScenarioInfo scenarioInfo) + { + if (_environmentWrapper.GetEnvironmentVariable(SpecFlowMessagesTestCaseStartedPickleIdOverrideName) is ISuccess success + && Guid.TryParse(success.Result, out var pickleId)) + { + return pickleId; + } + + return _pickleIdStore.GetPickleIdForScenario(scenarioInfo); + } + + public DateTime GetTestCaseFinishedTime() + { + if (_environmentWrapper.GetEnvironmentVariable(SpecFlowMessagesTestCaseFinishedTimeOverrideName) is ISuccess success + && TryParseUniversalDateTime(success.Result, out var dateTime)) + { + return dateTime; + } + + return _clock.GetNowDateAndTime(); + } + + public Guid GetTestCaseFinishedPickleId(ScenarioInfo scenarioInfo) + { + if (_environmentWrapper.GetEnvironmentVariable(SpecFlowMessagesTestCaseFinishedPickleIdOverrideName) is ISuccess success + && Guid.TryParse(success.Result, out var pickleId)) + { + return pickleId; + } + + return _pickleIdStore.GetPickleIdForScenario(scenarioInfo); + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageFactory.cs new file mode 100644 index 000000000..8908c86f8 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageFactory.cs @@ -0,0 +1,21 @@ +using System; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ICucumberMessageFactory + { + IResult BuildTestRunStartedMessage(DateTime timeStamp); + + IResult BuildTestCaseStartedMessage(Guid pickleId, DateTime timeStamp); + + IResult BuildTestCaseFinishedMessage(Guid pickleId, DateTime timeStamp, TestResult testResult); + + IResult BuildWrapperMessage(IResult testRunStarted); + + IResult BuildWrapperMessage(IResult testCaseStarted); + + IResult BuildWrapperMessage(IResult testCaseFinished); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSender.cs b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSender.cs new file mode 100644 index 000000000..e8398cb95 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSender.cs @@ -0,0 +1,14 @@ +using System; +using Io.Cucumber.Messages; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ICucumberMessageSender + { + void SendTestRunStarted(); + + void SendTestCaseStarted(ScenarioInfo scenarioInfo); + + void SendTestCaseFinished(ScenarioInfo scenarioInfo, TestResult testResult); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSink.cs b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSink.cs new file mode 100644 index 000000000..b82103108 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ICucumberMessageSink.cs @@ -0,0 +1,9 @@ +using Io.Cucumber.Messages; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ICucumberMessageSink + { + void SendMessage(Wrapper message); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/IFieldValueProvider.cs b/TechTalk.SpecFlow/CucumberMessages/IFieldValueProvider.cs new file mode 100644 index 000000000..d215f2f85 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/IFieldValueProvider.cs @@ -0,0 +1,13 @@ +using System; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface IFieldValueProvider + { + DateTime GetTestRunStartedTime(); + DateTime GetTestCaseStartedTime(); + Guid GetTestCaseStartedPickleId(ScenarioInfo scenarioInfo); + DateTime GetTestCaseFinishedTime(); + Guid GetTestCaseFinishedPickleId(ScenarioInfo scenarioInfo); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/IPickleIdGenerator.cs b/TechTalk.SpecFlow/CucumberMessages/IPickleIdGenerator.cs new file mode 100644 index 000000000..a3514c484 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/IPickleIdGenerator.cs @@ -0,0 +1,9 @@ +using System; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface IPickleIdGenerator + { + Guid GeneratePickleId(); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/IPickleIdStore.cs b/TechTalk.SpecFlow/CucumberMessages/IPickleIdStore.cs new file mode 100644 index 000000000..236ad9866 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/IPickleIdStore.cs @@ -0,0 +1,9 @@ +using System; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface IPickleIdStore + { + Guid GetPickleIdForScenario(ScenarioInfo scenarioInfo); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/IPickleIdStoreDictionaryFactory.cs b/TechTalk.SpecFlow/CucumberMessages/IPickleIdStoreDictionaryFactory.cs new file mode 100644 index 000000000..cb3e56cd5 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/IPickleIdStoreDictionaryFactory.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface IPickleIdStoreDictionaryFactory + { + IDictionary BuildDictionary(); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ITestAmbiguousMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ITestAmbiguousMessageFactory.cs new file mode 100644 index 000000000..f9562154f --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ITestAmbiguousMessageFactory.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ITestAmbiguousMessageFactory + { + string BuildFromScenarioContext(ScenarioContext scenarioContext); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ITestErrorMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ITestErrorMessageFactory.cs new file mode 100644 index 000000000..8f7207db3 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ITestErrorMessageFactory.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ITestErrorMessageFactory + { + string BuildFromScenarioContext(ScenarioContext scenarioContext); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ITestPendingMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ITestPendingMessageFactory.cs new file mode 100644 index 000000000..c8942301e --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ITestPendingMessageFactory.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ITestPendingMessageFactory + { + string BuildFromScenarioContext(ScenarioContext scenarioContext); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ITestResultFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ITestResultFactory.cs new file mode 100644 index 000000000..e422449e9 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ITestResultFactory.cs @@ -0,0 +1,22 @@ +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ITestResultFactory + { + IResult BuildPassedResult(ulong durationInNanoseconds); + + IResult BuildFailedResult(ulong durationInNanoseconds, string message); + + IResult BuildAmbiguousResult(ulong durationInNanoseconds, string message); + + IResult BuildPendingResult(ulong durationInNanoseconds, string message); + + IResult BuildSkippedResult(ulong durationInNanoseconds, string message); + + IResult BuildUndefinedResult(ulong durationInNanoseconds, string message); + + IResult BuildFromContext(ScenarioContext scenarioContext, FeatureContext featureContext); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/ITestUndefinedMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/ITestUndefinedMessageFactory.cs new file mode 100644 index 000000000..643ee349d --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/ITestUndefinedMessageFactory.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.CucumberMessages +{ + public interface ITestUndefinedMessageFactory + { + string BuildFromContext(ScenarioContext scenarioContext, FeatureContext featureContext); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/PickleIdGenerator.cs b/TechTalk.SpecFlow/CucumberMessages/PickleIdGenerator.cs new file mode 100644 index 000000000..1d8f429ef --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/PickleIdGenerator.cs @@ -0,0 +1,12 @@ +using System; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class PickleIdGenerator : IPickleIdGenerator + { + public Guid GeneratePickleId() + { + return Guid.NewGuid(); + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/PickleIdStore.cs b/TechTalk.SpecFlow/CucumberMessages/PickleIdStore.cs new file mode 100644 index 000000000..33def04b2 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/PickleIdStore.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class PickleIdStore : IPickleIdStore + { + private readonly IPickleIdGenerator _pickleIdGenerator; + private readonly IPickleIdStoreDictionaryFactory _pickleIdStoreDictionaryFactory; + private readonly object _initializationLock = new object(); + private IDictionary _scenarioInfoMappings; + + public PickleIdStore(IPickleIdGenerator pickleIdGenerator, IPickleIdStoreDictionaryFactory pickleIdStoreDictionaryFactory) + { + _pickleIdGenerator = pickleIdGenerator; + _pickleIdStoreDictionaryFactory = pickleIdStoreDictionaryFactory; + } + + public Guid GetPickleIdForScenario(ScenarioInfo scenarioInfo) + { + EnsureIsInitialized(); + + if (_scenarioInfoMappings.ContainsKey(scenarioInfo)) + { + return _scenarioInfoMappings[scenarioInfo]; + } + + var pickleId = _pickleIdGenerator.GeneratePickleId(); + _scenarioInfoMappings.Add(scenarioInfo, pickleId); + return pickleId; + } + + public void EnsureIsInitialized() + { + if (_scenarioInfoMappings != null) + { + return; + } + + lock (_initializationLock) + { + if (_scenarioInfoMappings != null) + { + return; + } + + _scenarioInfoMappings = _pickleIdStoreDictionaryFactory.BuildDictionary(); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/PickleIdStoreDictionaryFactory.cs b/TechTalk.SpecFlow/CucumberMessages/PickleIdStoreDictionaryFactory.cs new file mode 100644 index 000000000..37fdfbd32 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/PickleIdStoreDictionaryFactory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class PickleIdStoreDictionaryFactory : IPickleIdStoreDictionaryFactory + { + public IDictionary BuildDictionary() + { + return new ConcurrentDictionary(); + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileNameResolver.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileNameResolver.cs new file mode 100644 index 000000000..ae39a72ad --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileNameResolver.cs @@ -0,0 +1,9 @@ +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public interface IProtobufFileNameResolver + { + IResult Resolve(string targetPath); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileSinkOutput.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileSinkOutput.cs new file mode 100644 index 000000000..44be88a4a --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/IProtobufFileSinkOutput.cs @@ -0,0 +1,11 @@ +using Google.Protobuf; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public interface IProtobufFileSinkOutput + { + IResult WriteMessage(Wrapper message); + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileNameResolver.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileNameResolver.cs new file mode 100644 index 000000000..a19b3359c --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileNameResolver.cs @@ -0,0 +1,39 @@ +using System.IO; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.EnvironmentAccess; +using TechTalk.SpecFlow.TestFramework; + +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public class ProtobufFileNameResolver : IProtobufFileNameResolver + { + private readonly ITestRunContext _testRunContext; + private readonly IEnvironmentWrapper _environmentWrapper; + + public ProtobufFileNameResolver(ITestRunContext testRunContext, IEnvironmentWrapper environmentWrapper) + { + _testRunContext = testRunContext; + _environmentWrapper = environmentWrapper; + } + + public IResult Resolve(string targetPath) + { + var resolveEnvironmentVariablesResult = _environmentWrapper.ResolveEnvironmentVariables(targetPath); + switch (resolveEnvironmentVariablesResult) + { + case ISuccess success when Path.IsPathRooted(success.Result): + return Result.Success(success.Result); + + case ISuccess success: + string combinedPath = Path.Combine(_testRunContext.GetTestDirectory(), success.Result); + return Result.Success(combinedPath); + + case IFailure failure: + return Result.Failure($"Failed resolving environment variables from string '{targetPath}'", failure); + + default: + return Result.Failure($"Failed resolving environment variables from string '{targetPath}'"); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSink.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSink.cs new file mode 100644 index 000000000..e63c757c1 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSink.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Threading; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public class ProtobufFileSink : ICucumberMessageSink + { + private readonly IProtobufFileSinkOutput _protobufFileSinkOutput; + private readonly ProtobufFileSinkConfiguration _protobufFileSinkConfiguration; + + public ProtobufFileSink(IProtobufFileSinkOutput protobufFileSinkOutput, ProtobufFileSinkConfiguration protobufFileSinkConfiguration) + { + _protobufFileSinkOutput = protobufFileSinkOutput; + _protobufFileSinkConfiguration = protobufFileSinkConfiguration; + } + + public void SendMessage(Wrapper message) + { + string absoluteTargetFilePath = Path.GetFullPath(_protobufFileSinkConfiguration.TargetFilePath) + .Replace('\\', '_') + .Replace('/', '_') + .Replace(':', '_'); + using (var mutex = new Mutex(false, $@"Global\SpecFlowTestExecution_{absoluteTargetFilePath}")) + { + mutex.WaitOne(); + + try + { + var result = _protobufFileSinkOutput.WriteMessage(message); + switch (result) + { + case ExceptionFailure exceptionFailure: throw exceptionFailure.Exception; + case Failure failure: throw new InvalidOperationException($"Could not write to file {_protobufFileSinkConfiguration.TargetFilePath}. {failure.Description}"); + case IFailure _: throw new InvalidOperationException($"Could not write to file {_protobufFileSinkConfiguration.TargetFilePath}."); + } + } + finally + { + mutex.ReleaseMutex(); + } + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkConfiguration.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkConfiguration.cs new file mode 100644 index 000000000..4244d7444 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkConfiguration.cs @@ -0,0 +1,12 @@ +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public class ProtobufFileSinkConfiguration + { + public ProtobufFileSinkConfiguration(string targetFilePath) + { + TargetFilePath = targetFilePath; + } + + public string TargetFilePath { get; } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkOutput.cs b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkOutput.cs new file mode 100644 index 000000000..0e6e96907 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/Sinks/ProtobufFileSinkOutput.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using Google.Protobuf; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.FileAccess; + +namespace TechTalk.SpecFlow.CucumberMessages.Sinks +{ + public class ProtobufFileSinkOutput : IProtobufFileSinkOutput + { + private readonly IBinaryFileAccessor _binaryFileAccessor; + private readonly ProtobufFileSinkConfiguration _protobufFileSinkConfiguration; + private readonly IProtobufFileNameResolver _protobufFileNameResolver; + + public ProtobufFileSinkOutput(IBinaryFileAccessor binaryFileAccessor, ProtobufFileSinkConfiguration protobufFileSinkConfiguration, IProtobufFileNameResolver protobufFileNameResolver) + { + _binaryFileAccessor = binaryFileAccessor; + _protobufFileSinkConfiguration = protobufFileSinkConfiguration; + _protobufFileNameResolver = protobufFileNameResolver; + } + + public IResult WriteMessage(Wrapper message) + { + var resolveTargetFilePathResult = _protobufFileNameResolver.Resolve(_protobufFileSinkConfiguration.TargetFilePath); + if (!(resolveTargetFilePathResult is ISuccess resolveTargetFilePathSuccess)) + { + switch (resolveTargetFilePathResult) + { + case IFailure innerFailure: return Result.Failure("Stream could not be opened.", innerFailure); + default: return Result.Failure($"Stream could not be opened. File name '{_protobufFileSinkConfiguration.TargetFilePath}' could not be resolved."); + } + } + + var streamResult = _binaryFileAccessor.OpenAppendOrCreateFile(resolveTargetFilePathSuccess.Result); + switch (streamResult) + { + case IFailure failure: return Result.Failure("Stream could not be opened", failure); + case ISuccess success: return WriteMessageToStream(success.Result, message); + default: throw new InvalidOperationException($"The result from {nameof(BinaryFileAccessor.OpenAppendOrCreateFile)} must either implement {nameof(IFailure)} or {nameof(ISuccess)}"); + } + } + + private IResult WriteMessageToStream(Stream target, Wrapper message) + { + try + { + using (target) + { + message.WriteDelimitedTo(target); + target.Flush(); + return Result.Success(); + } + } + catch (Exception e) + { + return Result.Failure(e); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/TestAmbiguousMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/TestAmbiguousMessageFactory.cs new file mode 100644 index 000000000..73d4da605 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/TestAmbiguousMessageFactory.cs @@ -0,0 +1,10 @@ +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class TestAmbiguousMessageFactory : ITestAmbiguousMessageFactory + { + public string BuildFromScenarioContext(ScenarioContext scenarioContext) + { + return scenarioContext.TestError?.ToString() ?? "Duplicate step binding found"; + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/TestErrorMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/TestErrorMessageFactory.cs new file mode 100644 index 000000000..a409fc6b4 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/TestErrorMessageFactory.cs @@ -0,0 +1,12 @@ +using System; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class TestErrorMessageFactory : ITestErrorMessageFactory + { + public string BuildFromScenarioContext(ScenarioContext scenarioContext) + { + return scenarioContext.TestError?.ToString() ?? "Test failed with an unknown error"; + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/TestPendingMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/TestPendingMessageFactory.cs new file mode 100644 index 000000000..5ce9f9b34 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/TestPendingMessageFactory.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using TechTalk.SpecFlow.ErrorHandling; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class TestPendingMessageFactory : ITestPendingMessageFactory + { + private readonly IErrorProvider _errorProvider; + + public TestPendingMessageFactory(IErrorProvider errorProvider) + { + _errorProvider = errorProvider; + } + + public string BuildFromScenarioContext(ScenarioContext scenarioContext) + { + var pendingSteps = scenarioContext.PendingSteps.Distinct().OrderBy(s => s); + return $"{_errorProvider.GetPendingStepDefinitionError().Message}{Environment.NewLine} {string.Join(Environment.NewLine + " ", pendingSteps)}"; + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/TestResultFactory.cs b/TechTalk.SpecFlow/CucumberMessages/TestResultFactory.cs new file mode 100644 index 000000000..3e8b21131 --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/TestResultFactory.cs @@ -0,0 +1,89 @@ +using System; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class TestResultFactory : ITestResultFactory + { + private readonly ITestErrorMessageFactory _testErrorMessageFactory; + private readonly ITestPendingMessageFactory _testPendingMessageFactory; + private readonly ITestAmbiguousMessageFactory _testAmbiguousMessageFactory; + private readonly ITestUndefinedMessageFactory _testUndefinedMessageFactory; + + public TestResultFactory(ITestErrorMessageFactory testErrorMessageFactory, ITestPendingMessageFactory testPendingMessageFactory, ITestAmbiguousMessageFactory testAmbiguousMessageFactory, ITestUndefinedMessageFactory testUndefinedMessageFactory) + { + _testErrorMessageFactory = testErrorMessageFactory; + _testPendingMessageFactory = testPendingMessageFactory; + _testAmbiguousMessageFactory = testAmbiguousMessageFactory; + _testUndefinedMessageFactory = testUndefinedMessageFactory; + } + + public ulong ConvertTicksToPositiveNanoseconds(long ticks) + { + ulong ticksOrZero = (ulong)Math.Min(ticks, 0); + return ticksOrZero * 100; + } + + public IResult BuildPassedResult(ulong durationInNanoseconds) + { + return BuildTestResult(durationInNanoseconds, Status.Passed, ""); + } + + public IResult BuildFailedResult(ulong durationInNanoseconds, string message) + { + return BuildTestResult(durationInNanoseconds, Status.Failed, message); + } + + public IResult BuildAmbiguousResult(ulong durationInNanoseconds, string message) + { + return BuildTestResult(durationInNanoseconds, Status.Ambiguous, message); + } + + public IResult BuildPendingResult(ulong durationInNanoseconds, string message) + { + return BuildTestResult(durationInNanoseconds, Status.Pending, message); + } + + public IResult BuildSkippedResult(ulong durationInNanoseconds, string message) + { + return BuildTestResult(durationInNanoseconds, Status.Skipped, message); + } + + public IResult BuildUndefinedResult(ulong durationInNanoseconds, string message) + { + return BuildTestResult(durationInNanoseconds, Status.Undefined, message); + } + + public IResult BuildTestResult(ulong durationInNanoseconds, Status status, string message) + { + var testResult = new TestResult + { + DurationNanoseconds = durationInNanoseconds, + Status = status, + Message = message ?? "" + }; + + return Result.Success(testResult); + } + + public IResult BuildFromContext(ScenarioContext scenarioContext, FeatureContext featureContext) + { + if (scenarioContext is null) + { + return Result.Failure(new ArgumentNullException(nameof(scenarioContext))); + } + + ulong nanoseconds = ConvertTicksToPositiveNanoseconds(scenarioContext.Stopwatch.Elapsed.Ticks); + switch (scenarioContext.ScenarioExecutionStatus) + { + case ScenarioExecutionStatus.OK: return BuildPassedResult(nanoseconds); + case ScenarioExecutionStatus.TestError: return BuildFailedResult(nanoseconds, _testErrorMessageFactory.BuildFromScenarioContext(scenarioContext)); + case ScenarioExecutionStatus.StepDefinitionPending: return BuildPendingResult(nanoseconds, _testPendingMessageFactory.BuildFromScenarioContext(scenarioContext)); + case ScenarioExecutionStatus.BindingError: return BuildAmbiguousResult(nanoseconds, _testAmbiguousMessageFactory.BuildFromScenarioContext(scenarioContext)); + case ScenarioExecutionStatus.UndefinedStep: return BuildUndefinedResult(nanoseconds, _testUndefinedMessageFactory.BuildFromContext(scenarioContext, featureContext)); + default: return Result.Failure($"Status '{scenarioContext.ScenarioExecutionStatus}' is unknown or not supported."); + } + } + } +} diff --git a/TechTalk.SpecFlow/CucumberMessages/TestUndefinedMessageFactory.cs b/TechTalk.SpecFlow/CucumberMessages/TestUndefinedMessageFactory.cs new file mode 100644 index 000000000..3e56bbd0f --- /dev/null +++ b/TechTalk.SpecFlow/CucumberMessages/TestUndefinedMessageFactory.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using TechTalk.SpecFlow.BindingSkeletons; +using TechTalk.SpecFlow.Configuration; +using TechTalk.SpecFlow.ErrorHandling; + +namespace TechTalk.SpecFlow.CucumberMessages +{ + public class TestUndefinedMessageFactory : ITestUndefinedMessageFactory + { + private readonly IStepDefinitionSkeletonProvider _stepDefinitionSkeletonProvider; + private readonly IErrorProvider _errorProvider; + private readonly SpecFlowConfiguration _specFlowConfiguration; + + public TestUndefinedMessageFactory(IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider, IErrorProvider errorProvider, SpecFlowConfiguration specFlowConfiguration) + { + _stepDefinitionSkeletonProvider = stepDefinitionSkeletonProvider; + _errorProvider = errorProvider; + _specFlowConfiguration = specFlowConfiguration; + } + + public string BuildFromContext(ScenarioContext scenarioContext, FeatureContext featureContext) + { + string skeleton = _stepDefinitionSkeletonProvider.GetBindingClassSkeleton( + featureContext.FeatureInfo.GenerationTargetLanguage, + scenarioContext.MissingSteps.ToArray(), + "MyNamespace", + "StepDefinitions", + _specFlowConfiguration.StepDefinitionSkeletonStyle, + featureContext.BindingCulture ?? CultureInfo.CurrentCulture); + + return $"{_errorProvider.GetMissingStepDefinitionError().Message}{Environment.NewLine}{skeleton}"; + } + } +} diff --git a/TechTalk.SpecFlow/EnvironmentAccess/EnvironmentWrapper.cs b/TechTalk.SpecFlow/EnvironmentAccess/EnvironmentWrapper.cs new file mode 100644 index 000000000..bb02d750a --- /dev/null +++ b/TechTalk.SpecFlow/EnvironmentAccess/EnvironmentWrapper.cs @@ -0,0 +1,40 @@ +using System; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.EnvironmentAccess +{ + public class EnvironmentWrapper : IEnvironmentWrapper + { + public IResult ResolveEnvironmentVariables(string source) + { + if (source is null) + { + return Result.Failure(new ArgumentNullException(nameof(source))); + } + + return Result.Success(Environment.ExpandEnvironmentVariables(source)); + } + + public bool IsEnvironmentVariableSet(string name) + { + return Environment.GetEnvironmentVariables().Contains(name); + } + + public IResult GetEnvironmentVariable(string name) + { + if (IsEnvironmentVariableSet(name)) + { + return Result.Success(Environment.GetEnvironmentVariable(name)); + } + + return Result.Failure($"Environment variable {name} not set"); + } + + public void SetEnvironmentVariable(string name, string value) + { + Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process); + } + + public string GetCurrentDirectory() => Environment.CurrentDirectory; + } +} diff --git a/TechTalk.SpecFlow/EnvironmentAccess/IEnvironmentWrapper.cs b/TechTalk.SpecFlow/EnvironmentAccess/IEnvironmentWrapper.cs new file mode 100644 index 000000000..591658a7d --- /dev/null +++ b/TechTalk.SpecFlow/EnvironmentAccess/IEnvironmentWrapper.cs @@ -0,0 +1,17 @@ +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.EnvironmentAccess +{ + public interface IEnvironmentWrapper + { + IResult ResolveEnvironmentVariables(string source); + + bool IsEnvironmentVariableSet(string name); + + IResult GetEnvironmentVariable(string name); + + void SetEnvironmentVariable(string name, string value); + + string GetCurrentDirectory(); + } +} diff --git a/TechTalk.SpecFlow/FileAccess/BinaryFileAccessor.cs b/TechTalk.SpecFlow/FileAccess/BinaryFileAccessor.cs new file mode 100644 index 000000000..8f3c62314 --- /dev/null +++ b/TechTalk.SpecFlow/FileAccess/BinaryFileAccessor.cs @@ -0,0 +1,28 @@ +using System.IO; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.FileAccess +{ + public class BinaryFileAccessor : IBinaryFileAccessor + { + public IResult OpenAppendOrCreateFile(string filePath) + { + try + { + string parentDirectoryPath = Path.GetDirectoryName(filePath); + + if (!Directory.Exists(parentDirectoryPath)) + { + Directory.CreateDirectory(parentDirectoryPath); + } + + var streamToReturn = File.Open(filePath, FileMode.Append, System.IO.FileAccess.Write, FileShare.Read); + return Result.Success(streamToReturn); + } + catch (IOException exc) + { + return Result.Failure(exc); + } + } + } +} diff --git a/TechTalk.SpecFlow/FileAccess/IBinaryFileAccessor.cs b/TechTalk.SpecFlow/FileAccess/IBinaryFileAccessor.cs new file mode 100644 index 000000000..df9cc07d6 --- /dev/null +++ b/TechTalk.SpecFlow/FileAccess/IBinaryFileAccessor.cs @@ -0,0 +1,10 @@ +using System.IO; +using TechTalk.SpecFlow.CommonModels; + +namespace TechTalk.SpecFlow.FileAccess +{ + public interface IBinaryFileAccessor + { + IResult OpenAppendOrCreateFile(string filePath); + } +} diff --git a/TechTalk.SpecFlow/ITestRunner.cs b/TechTalk.SpecFlow/ITestRunner.cs index e6a19b45a..b30710180 100644 --- a/TechTalk.SpecFlow/ITestRunner.cs +++ b/TechTalk.SpecFlow/ITestRunner.cs @@ -1,59 +1,59 @@ -namespace TechTalk.SpecFlow -{ - public interface ITestRunner +namespace TechTalk.SpecFlow +{ + public interface ITestRunner { - int ThreadId { get; } - FeatureContext FeatureContext { get; } - ScenarioContext ScenarioContext { get; } - + int ThreadId { get; } + FeatureContext FeatureContext { get; } + ScenarioContext ScenarioContext { get; } + void InitializeTestRunner(int threadId); - + void OnTestRunStart(); - void OnTestRunEnd(); - - void OnFeatureStart(FeatureInfo featureInfo); - void OnFeatureEnd(); - - void OnScenarioInitialize(ScenarioInfo scenarioInfo); - void OnScenarioStart(); - - void CollectScenarioErrors(); - void OnScenarioEnd(); - - void Given(string text, string multilineTextArg, Table tableArg, string keyword = null); - void When(string text, string multilineTextArg, Table tableArg, string keyword = null); - void Then(string text, string multilineTextArg, Table tableArg, string keyword = null); - void And(string text, string multilineTextArg, Table tableArg, string keyword = null); - void But(string text, string multilineTextArg, Table tableArg, string keyword = null); - - void Pending(); - } - - public static class TestRunnerDefaultArguments - { - public static void Given(this ITestRunner testRunner, string text) - { - testRunner.Given(text, null, null, null); - } - - public static void When(this ITestRunner testRunner, string text) - { - testRunner.When(text, null, null, null); - } - - public static void Then(this ITestRunner testRunner, string text) - { - testRunner.Then(text, null, null, null); - } - - public static void And(this ITestRunner testRunner, string text) - { - testRunner.And(text, null, null, null); - } - - public static void But(this ITestRunner testRunner, string text) - { - testRunner.But(text, null, null, null); - } - } + void OnTestRunEnd(); + + void OnFeatureStart(FeatureInfo featureInfo); + void OnFeatureEnd(); + + void OnScenarioInitialize(ScenarioInfo scenarioInfo); + void OnScenarioStart(); + + void CollectScenarioErrors(); + void OnScenarioEnd(); + + void Given(string text, string multilineTextArg, Table tableArg, string keyword = null); + void When(string text, string multilineTextArg, Table tableArg, string keyword = null); + void Then(string text, string multilineTextArg, Table tableArg, string keyword = null); + void And(string text, string multilineTextArg, Table tableArg, string keyword = null); + void But(string text, string multilineTextArg, Table tableArg, string keyword = null); + + void Pending(); + } + + public static class TestRunnerDefaultArguments + { + public static void Given(this ITestRunner testRunner, string text) + { + testRunner.Given(text, null, null, null); + } + + public static void When(this ITestRunner testRunner, string text) + { + testRunner.When(text, null, null, null); + } + + public static void Then(this ITestRunner testRunner, string text) + { + testRunner.Then(text, null, null, null); + } + + public static void And(this ITestRunner testRunner, string text) + { + testRunner.And(text, null, null, null); + } + + public static void But(this ITestRunner testRunner, string text) + { + testRunner.But(text, null, null, null); + } + } } \ No newline at end of file diff --git a/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs b/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs index b904f73ff..69c0cfe19 100644 --- a/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs +++ b/TechTalk.SpecFlow/Infrastructure/DefaultDependencyProvider.cs @@ -3,8 +3,14 @@ using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Bindings.Discovery; using TechTalk.SpecFlow.Configuration; +using TechTalk.SpecFlow.CucumberMessages; +using TechTalk.SpecFlow.CucumberMessages.Sinks; +using TechTalk.SpecFlow.EnvironmentAccess; using TechTalk.SpecFlow.ErrorHandling; +using TechTalk.SpecFlow.FileAccess; using TechTalk.SpecFlow.Plugins; +using TechTalk.SpecFlow.TestFramework; +using TechTalk.SpecFlow.Time; using TechTalk.SpecFlow.Tracing; namespace TechTalk.SpecFlow.Infrastructure @@ -51,6 +57,29 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterInstanceAs(new ProtobufFileSinkConfiguration("CucumberMessageQueue/messages")); + container.RegisterTypeAs(); + + container.RegisterTypeAs(); + + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + RegisterUnitTestProviders(container); } diff --git a/TechTalk.SpecFlow/Infrastructure/TestExecutionEngine.cs b/TechTalk.SpecFlow/Infrastructure/TestExecutionEngine.cs index 703f397b7..d66fc65af 100644 --- a/TechTalk.SpecFlow/Infrastructure/TestExecutionEngine.cs +++ b/TechTalk.SpecFlow/Infrastructure/TestExecutionEngine.cs @@ -4,11 +4,13 @@ using System.Globalization; using System.Linq; using BoDi; +using Io.Cucumber.Messages; using TechTalk.SpecFlow.Bindings; using TechTalk.SpecFlow.Bindings.Reflection; -using TechTalk.SpecFlow.BindingSkeletons; +using TechTalk.SpecFlow.CommonModels; using TechTalk.SpecFlow.Compatibility; using TechTalk.SpecFlow.Configuration; +using TechTalk.SpecFlow.CucumberMessages; using TechTalk.SpecFlow.ErrorHandling; using TechTalk.SpecFlow.Tracing; using TechTalk.SpecFlow.UnitTestProvider; @@ -22,10 +24,13 @@ public class TestExecutionEngine : ITestExecutionEngine private readonly IContextManager _contextManager; private readonly IErrorProvider _errorProvider; private readonly IObsoleteStepHandler _obsoleteStepHandler; + private readonly ICucumberMessageSender _cucumberMessageSender; + private readonly ITestResultFactory _testResultFactory; + private readonly ITestPendingMessageFactory _testPendingMessageFactory; + private readonly ITestUndefinedMessageFactory _testUndefinedMessageFactory; private readonly SpecFlowConfiguration _specFlowConfiguration; private readonly IStepArgumentTypeConverter _stepArgumentTypeConverter; private readonly IStepDefinitionMatchService _stepDefinitionMatchService; - private readonly IStepDefinitionSkeletonProvider _stepDefinitionSkeletonProvider; private readonly IStepErrorHandler[] _stepErrorHandlers; private readonly IStepFormatter _stepFormatter; private readonly ITestObjectResolver _testObjectResolver; @@ -36,18 +41,18 @@ public class TestExecutionEngine : ITestExecutionEngine private ProgrammingLanguage _defaultTargetLanguage = ProgrammingLanguage.CSharp; private bool _testRunnerEndExecuted = false; + private bool _testRunnerStartExecuted = false; public TestExecutionEngine(IStepFormatter stepFormatter, ITestTracer testTracer, IErrorProvider errorProvider, IStepArgumentTypeConverter stepArgumentTypeConverter, - SpecFlowConfiguration specFlowConfiguration, IBindingRegistry bindingRegistry, IUnitTestRuntimeProvider unitTestRuntimeProvider, - IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider, IContextManager contextManager, IStepDefinitionMatchService stepDefinitionMatchService, - IDictionary stepErrorHandlers, IBindingInvoker bindingInvoker, IObsoleteStepHandler obsoleteStepHandler, + SpecFlowConfiguration specFlowConfiguration, IBindingRegistry bindingRegistry, IUnitTestRuntimeProvider unitTestRuntimeProvider, IContextManager contextManager, IStepDefinitionMatchService stepDefinitionMatchService, + IDictionary stepErrorHandlers, IBindingInvoker bindingInvoker, IObsoleteStepHandler obsoleteStepHandler, ICucumberMessageSender cucumberMessageSender, ITestResultFactory testResultFactory, + ITestPendingMessageFactory testPendingMessageFactory, ITestUndefinedMessageFactory testUndefinedMessageFactory, ITestObjectResolver testObjectResolver = null, IObjectContainer testThreadContainer = null) //TODO: find a better way to access the container { _errorProvider = errorProvider; _bindingInvoker = bindingInvoker; _contextManager = contextManager; _unitTestRuntimeProvider = unitTestRuntimeProvider; - _stepDefinitionSkeletonProvider = stepDefinitionSkeletonProvider; _bindingRegistry = bindingRegistry; _specFlowConfiguration = specFlowConfiguration; _testTracer = testTracer; @@ -58,6 +63,10 @@ public TestExecutionEngine(IStepFormatter stepFormatter, ITestTracer testTracer, _testObjectResolver = testObjectResolver; TestThreadContainer = testThreadContainer; _obsoleteStepHandler = obsoleteStepHandler; + _cucumberMessageSender = cucumberMessageSender; + _testResultFactory = testResultFactory; + _testPendingMessageFactory = testPendingMessageFactory; + _testUndefinedMessageFactory = testUndefinedMessageFactory; } public FeatureContext FeatureContext => _contextManager.FeatureContext; @@ -66,13 +75,22 @@ public TestExecutionEngine(IStepFormatter stepFormatter, ITestTracer testTracer, public virtual void OnTestRunStart() { + if (_testRunnerStartExecuted) + { + return; + } + + _testRunnerStartExecuted = true; + _cucumberMessageSender.SendTestRunStarted(); FireEvents(HookType.BeforeTestRun); } public virtual void OnTestRunEnd() { if (_testRunnerEndExecuted) + { return; + } _testRunnerEndExecuted = true; FireEvents(HookType.AfterTestRun); @@ -124,6 +142,7 @@ public void OnScenarioInitialize(ScenarioInfo scenarioInfo) public void OnScenarioStart() { + _cucumberMessageSender.SendTestCaseStarted(_contextManager.ScenarioContext.ScenarioInfo); FireScenarioEvents(HookType.BeforeScenario); } @@ -138,34 +157,41 @@ public void OnAfterLastStep() _testTracer.TraceDuration(duration, "Scenario: " + _contextManager.ScenarioContext.ScenarioInfo.Title); } + var testResultResult = _testResultFactory.BuildFromContext(_contextManager.ScenarioContext, _contextManager.FeatureContext); + switch (testResultResult) + { + case ISuccess success: + _cucumberMessageSender.SendTestCaseFinished(_contextManager.ScenarioContext.ScenarioInfo, success.Result); + break; + + case IFailure failure: + _testTracer.TraceWarning(failure.ToString()); + break; + } + if (_contextManager.ScenarioContext.ScenarioExecutionStatus == ScenarioExecutionStatus.OK) + { return; + } if (_contextManager.ScenarioContext.ScenarioExecutionStatus == ScenarioExecutionStatus.StepDefinitionPending) { - var pendingSteps = _contextManager.ScenarioContext.PendingSteps.Distinct().OrderBy(s => s); - _errorProvider.ThrowPendingError(_contextManager.ScenarioContext.ScenarioExecutionStatus, string.Format("{0}{2} {1}", - _errorProvider.GetPendingStepDefinitionError().Message, - string.Join(Environment.NewLine + " ", pendingSteps.ToArray()), - Environment.NewLine)); + string pendingStepExceptionMessage = _testPendingMessageFactory.BuildFromScenarioContext(_contextManager.ScenarioContext); + _errorProvider.ThrowPendingError(_contextManager.ScenarioContext.ScenarioExecutionStatus, pendingStepExceptionMessage); return; } if (_contextManager.ScenarioContext.ScenarioExecutionStatus == ScenarioExecutionStatus.UndefinedStep) { - string skeleton = _stepDefinitionSkeletonProvider.GetBindingClassSkeleton( - _defaultTargetLanguage, - _contextManager.ScenarioContext.MissingSteps.ToArray(), "MyNamespace", "StepDefinitions", _specFlowConfiguration.StepDefinitionSkeletonStyle, _defaultBindingCulture); - - _errorProvider.ThrowPendingError(_contextManager.ScenarioContext.ScenarioExecutionStatus, string.Format("{0}{2}{1}", - _errorProvider.GetMissingStepDefinitionError().Message, - skeleton, - Environment.NewLine)); + string undefinedStepExceptionMessage = _testUndefinedMessageFactory.BuildFromContext(_contextManager.ScenarioContext, _contextManager.FeatureContext); + _errorProvider.ThrowPendingError(_contextManager.ScenarioContext.ScenarioExecutionStatus, undefinedStepExceptionMessage); return; } if (_contextManager.ScenarioContext.TestError == null) + { throw new InvalidOperationException("test failed with an unknown error"); + } _contextManager.ScenarioContext.TestError.PreserveStackTrace(); throw _contextManager.ScenarioContext.TestError; diff --git a/TechTalk.SpecFlow/Plugins/ISpecFlowPath.cs b/TechTalk.SpecFlow/Plugins/ISpecFlowPath.cs new file mode 100644 index 000000000..cd13fe28d --- /dev/null +++ b/TechTalk.SpecFlow/Plugins/ISpecFlowPath.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.Plugins +{ + public interface ISpecFlowPath + { + string GetPathToSpecFlowDll(); + } +} diff --git a/TechTalk.SpecFlow/Plugins/RuntimePluginLocator.cs b/TechTalk.SpecFlow/Plugins/RuntimePluginLocator.cs index a49a6d9d2..201e7a6d7 100644 --- a/TechTalk.SpecFlow/Plugins/RuntimePluginLocator.cs +++ b/TechTalk.SpecFlow/Plugins/RuntimePluginLocator.cs @@ -8,12 +8,12 @@ namespace TechTalk.SpecFlow.Plugins internal class RuntimePluginLocator : IRuntimePluginLocator { private readonly IRuntimePluginLocationMerger _runtimePluginLocationMerger; - private readonly string _pathToFolderWithSpecFlowAssembly; + private readonly ISpecFlowPath _specFlowPath; - public RuntimePluginLocator(IRuntimePluginLocationMerger runtimePluginLocationMerger) + public RuntimePluginLocator(IRuntimePluginLocationMerger runtimePluginLocationMerger, ISpecFlowPath specFlowPath) { _runtimePluginLocationMerger = runtimePluginLocationMerger; - _pathToFolderWithSpecFlowAssembly = Path.GetDirectoryName(typeof(RuntimePluginLocator).Assembly.Location); + _specFlowPath = specFlowPath; } public IReadOnlyList GetAllRuntimePlugins() @@ -26,7 +26,8 @@ public IReadOnlyList GetAllRuntimePlugins(string testAssemblyLocation) var allRuntimePlugins = new List(); allRuntimePlugins.AddRange(SearchPluginsInFolder(Environment.CurrentDirectory)); - allRuntimePlugins.AddRange(SearchPluginsInFolder(_pathToFolderWithSpecFlowAssembly)); + string specFlowAssemblyFolder = Path.GetDirectoryName(_specFlowPath.GetPathToSpecFlowDll()); + allRuntimePlugins.AddRange(SearchPluginsInFolder(specFlowAssemblyFolder)); if (testAssemblyLocation.IsNotNullOrWhiteSpace()) { diff --git a/TechTalk.SpecFlow/Plugins/SpecFlowPath.cs b/TechTalk.SpecFlow/Plugins/SpecFlowPath.cs new file mode 100644 index 000000000..5d4f1fdef --- /dev/null +++ b/TechTalk.SpecFlow/Plugins/SpecFlowPath.cs @@ -0,0 +1,10 @@ +namespace TechTalk.SpecFlow.Plugins +{ + public class SpecFlowPath : ISpecFlowPath + { + public string GetPathToSpecFlowDll() + { + return typeof(SpecFlowPath).Assembly.Location; + } + } +} diff --git a/TechTalk.SpecFlow/ScenarioInfo.cs b/TechTalk.SpecFlow/ScenarioInfo.cs index 917e7d758..8979f8734 100644 --- a/TechTalk.SpecFlow/ScenarioInfo.cs +++ b/TechTalk.SpecFlow/ScenarioInfo.cs @@ -8,7 +8,7 @@ public class ScenarioInfo public string Title { get; private set; } public string Description { get; private set; } - public ScenarioInfo(string title, string description, params string[] tags) + public ScenarioInfo(string title, string description, params string[] tags) { Title = title; Description = description; diff --git a/TechTalk.SpecFlow/SpecFlow.nuspec b/TechTalk.SpecFlow/SpecFlow.nuspec index fa64b3756..2fe2153cf 100644 --- a/TechTalk.SpecFlow/SpecFlow.nuspec +++ b/TechTalk.SpecFlow/SpecFlow.nuspec @@ -20,12 +20,14 @@ + + @@ -34,6 +36,7 @@ + diff --git a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj index d11cb9b23..72a68d13e 100644 --- a/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj +++ b/TechTalk.SpecFlow/TechTalk.SpecFlow.csproj @@ -1,4 +1,4 @@ - + $(SpecFlow_Runtime_TFM) TechTalk.SpecFlow @@ -16,6 +16,7 @@ + diff --git a/TechTalk.SpecFlow/TestFramework/DefaultTestRunContext.cs b/TechTalk.SpecFlow/TestFramework/DefaultTestRunContext.cs new file mode 100644 index 000000000..e6a195897 --- /dev/null +++ b/TechTalk.SpecFlow/TestFramework/DefaultTestRunContext.cs @@ -0,0 +1,16 @@ +using TechTalk.SpecFlow.EnvironmentAccess; + +namespace TechTalk.SpecFlow.TestFramework +{ + public class DefaultTestRunContext : ITestRunContext + { + private readonly IEnvironmentWrapper _environmentWrapper; + + public DefaultTestRunContext(IEnvironmentWrapper environmentWrapper) + { + _environmentWrapper = environmentWrapper; + } + + public string GetTestDirectory() => _environmentWrapper.GetCurrentDirectory(); + } +} diff --git a/TechTalk.SpecFlow/TestFramework/ITestRunContext.cs b/TechTalk.SpecFlow/TestFramework/ITestRunContext.cs new file mode 100644 index 000000000..7318d6bf1 --- /dev/null +++ b/TechTalk.SpecFlow/TestFramework/ITestRunContext.cs @@ -0,0 +1,7 @@ +namespace TechTalk.SpecFlow.TestFramework +{ + public interface ITestRunContext + { + string GetTestDirectory(); + } +} diff --git a/TechTalk.SpecFlow/TestRunnerManager.cs b/TechTalk.SpecFlow/TestRunnerManager.cs index de76168dd..0e9f6d07a 100644 --- a/TechTalk.SpecFlow/TestRunnerManager.cs +++ b/TechTalk.SpecFlow/TestRunnerManager.cs @@ -18,6 +18,8 @@ public interface ITestRunnerManager : IDisposable bool IsMultiThreaded { get; } ITestRunner GetTestRunner(int threadId); void Initialize(Assembly testAssembly); + void FireTestRunEnd(); + void FireTestRunStart(); } public class TestRunnerManager : ITestRunnerManager @@ -30,7 +32,7 @@ public class TestRunnerManager : ITestRunnerManager private readonly ITestTracer testTracer; private readonly Dictionary testRunnerRegistry = new Dictionary(); private readonly object syncRoot = new object(); - private bool isTestRunInitialized; + public bool IsTestRunInitialized { get; private set; } private object disposeLockObj = null; public Assembly TestAssembly { get; private set; } @@ -55,10 +57,10 @@ public virtual ITestRunner CreateTestRunner(int threadId) lock (this) { - if (!isTestRunInitialized) + if (!IsTestRunInitialized) { InitializeBindingRegistry(testRunner); - isTestRunInitialized = true; + IsTestRunInitialized = true; } } @@ -70,8 +72,6 @@ protected virtual void InitializeBindingRegistry(ITestRunner testRunner) BindingAssemblies = GetBindingAssemblies(); BuildBindingRegistry(BindingAssemblies); - testRunner.OnTestRunStart(); - EventHandler domainUnload = delegate { OnDomainUnload(); }; AppDomain.CurrentDomain.DomainUnload += domainUnload; AppDomain.CurrentDomain.ProcessExit += domainUnload; @@ -101,7 +101,7 @@ protected internal virtual void OnDomainUnload() Dispose(); } - private void FireTestRunEnd() + public void FireTestRunEnd() { // this method must not be called multiple times var onTestRunnerEndExecutionHost = testRunnerRegistry.Values.FirstOrDefault(); @@ -109,6 +109,14 @@ private void FireTestRunEnd() onTestRunnerEndExecutionHost.OnTestRunEnd(); } + public void FireTestRunStart() + { + // this method must not be called multiple times + var onTestRunnerEndExecutionHost = testRunnerRegistry.Values.FirstOrDefault(); + if (onTestRunnerEndExecutionHost != null) + onTestRunnerEndExecutionHost.OnTestRunStart(); + } + protected virtual ITestRunner CreateTestRunnerInstance() { var testThreadContainer = containerBuilder.CreateTestThreadContainer(globalContainer); @@ -140,7 +148,7 @@ private ITestRunner GetTestRunnerWithoutExceptionHandling(int threadId) ITestRunner testRunner; if (!testRunnerRegistry.TryGetValue(threadId, out testRunner)) { - lock(syncRoot) + lock (syncRoot) { if (!testRunnerRegistry.TryGetValue(threadId, out testRunner)) { @@ -214,9 +222,19 @@ public static void OnTestRunEnd(Assembly testAssembly = null) { testAssembly = testAssembly ?? Assembly.GetCallingAssembly(); var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: false); + testRunnerManager?.FireTestRunEnd(); testRunnerManager?.Dispose(); } + public static void OnTestRunStart(Assembly testAssembly = null) + { + testAssembly = testAssembly ?? Assembly.GetCallingAssembly(); + var testRunnerManager = GetTestRunnerManager(testAssembly, createIfMissing: true); + testRunnerManager.GetTestRunner(GetLogicalThreadId(null)); + + testRunnerManager?.FireTestRunStart(); + } + public static ITestRunner GetTestRunner(Assembly testAssembly = null, int? managedThreadId = null) { testAssembly = testAssembly ?? Assembly.GetCallingAssembly(); @@ -225,6 +243,7 @@ public static ITestRunner GetTestRunner(Assembly testAssembly = null, int? manag return testRunnerManager.GetTestRunner(managedThreadId.Value); } + private static int GetLogicalThreadId(int? managedThreadId) { if (ParallelExecutionIsDisabled()) diff --git a/TechTalk.SpecFlow/Time/IClock.cs b/TechTalk.SpecFlow/Time/IClock.cs new file mode 100644 index 000000000..1953fac14 --- /dev/null +++ b/TechTalk.SpecFlow/Time/IClock.cs @@ -0,0 +1,11 @@ +using System; + +namespace TechTalk.SpecFlow.Time +{ + public interface IClock + { + DateTime GetToday(); + + DateTime GetNowDateAndTime(); + } +} diff --git a/TechTalk.SpecFlow/Time/UtcDateTimeClock.cs b/TechTalk.SpecFlow/Time/UtcDateTimeClock.cs new file mode 100644 index 000000000..aac3ce022 --- /dev/null +++ b/TechTalk.SpecFlow/Time/UtcDateTimeClock.cs @@ -0,0 +1,17 @@ +using System; + +namespace TechTalk.SpecFlow.Time +{ + public class UtcDateTimeClock : IClock + { + public DateTime GetToday() + { + return DateTime.UtcNow.Date; + } + + public DateTime GetNowDateAndTime() + { + return DateTime.UtcNow; + } + } +} diff --git a/Tests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests.csproj b/Tests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests.csproj index 7c5e35ab7..3366f6b34 100644 --- a/Tests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests.csproj +++ b/Tests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests/TechTalk.SpecFlow.MsBuildNetSdk.IntegrationTests.csproj @@ -59,5 +59,7 @@ + + diff --git a/Tests/TechTalk.SpecFlow.PluginTests/RuntimePluginLocatorTests.cs b/Tests/TechTalk.SpecFlow.PluginTests/RuntimePluginLocatorTests.cs index df2928db9..fe2adf622 100644 --- a/Tests/TechTalk.SpecFlow.PluginTests/RuntimePluginLocatorTests.cs +++ b/Tests/TechTalk.SpecFlow.PluginTests/RuntimePluginLocatorTests.cs @@ -11,13 +11,13 @@ public class RuntimePluginLocatorTests public void LoadPlugins_Find_All_4_Referenced_RuntimePlugins() { //ARRANGE - var runtimePluginLocator = new RuntimePluginLocator(new RuntimePluginLocationMerger()); + var runtimePluginLocator = new RuntimePluginLocator(new RuntimePluginLocationMerger(), new SpecFlowPath()); //ACT var plugins = runtimePluginLocator.GetAllRuntimePlugins(); //ASSERT - plugins.Count.Should().Be(4, $"{String.Join(",", plugins)} were found"); + plugins.Count.Should().Be(4, $"{string.Join(",", plugins)} were found"); } } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Container/RuntimePluginLocatorTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Container/RuntimePluginLocatorTests.cs index d2b27ebe8..1acc5d909 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Container/RuntimePluginLocatorTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Container/RuntimePluginLocatorTests.cs @@ -13,7 +13,7 @@ public void LoadPlugins_Doesnt_load_too_much_assemblies() //ARRANGE var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - var runtimePluginLocator = new RuntimePluginLocator(new RuntimePluginLocationMerger()); + var runtimePluginLocator = new RuntimePluginLocator(new RuntimePluginLocationMerger(), new SpecFlowPath()); //ACT diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageFactoryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageFactoryTests.cs new file mode 100644 index 000000000..45329db64 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageFactoryTests.cs @@ -0,0 +1,254 @@ +using System; +using FluentAssertions; +using Google.Protobuf.WellKnownTypes; +using Io.Cucumber.Messages; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.CucumberMessages; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.CucumberMessages +{ + public class CucumberMessageFactoryTests + { + [Fact(DisplayName = @"BuildTestRunResultMessage should return a TestRunResult message object")] + public void BuildTestRunResultMessage_DateTime_ShouldReturnTestRunResultMessageObject() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + + // ACT + var actualTestRunStartedMessageResult = cucumberMessageFactory.BuildTestRunStartedMessage(dateTime); + + // ASSERT + actualTestRunStartedMessageResult.Should().BeAssignableTo>(); + } + + [Fact(DisplayName = @"BuildTestRunResultMessage should return a TestRunResult message object with the specified date and time")] + public void BuildTestRunResultMessage_DateTime_ShouldReturnTestRunResultMessageObjectWithSpecifiedDateAndTime() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + + // ACT + var actualTestRunStartedMessageResult = cucumberMessageFactory.BuildTestRunStartedMessage(dateTime); + + // ASSERT + actualTestRunStartedMessageResult.Should().BeAssignableTo>() + .Which.Result.Timestamp.ToDateTime().Should().Be(dateTime); + } + + [Fact(DisplayName = @"BuildTestRunResultMessage should return a TestRunResult message object with SpecFlow as used Cucumber implementation")] + public void BuildTestRunResultMessage_ValidParameters_ShouldReturnTestRunResultMessageObjectWithSpecFlowAsUsedCucumberImplementation() + { + // ARRANGE + const string expectedCucumberImplementation = @"SpecFlow"; + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + + // ACT + var actualTestRunStartedMessageResult = cucumberMessageFactory.BuildTestRunStartedMessage(dateTime); + + // ASSERT + + actualTestRunStartedMessageResult.Should().BeAssignableTo>() + .Which.Result.CucumberImplementation.Should().Be(expectedCucumberImplementation); + } + + [Theory(DisplayName = @"BuildTestCaseStarted should return a failure when a non-UTC date has been specified")] + [InlineData(DateTimeKind.Local)] + [InlineData(DateTimeKind.Unspecified)] + public void BuildTestRunResultMessage_NonUtcDate_ShouldReturnFailure(DateTimeKind dateTimeKind) + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, dateTimeKind); + var pickleId = Guid.NewGuid(); + + // ACT + var result = cucumberMessageFactory.BuildTestCaseStartedMessage(pickleId, dateTime); + + // ASSERT + result.Should().BeAssignableTo(); + } + + [Fact(DisplayName = @"BuildTestCaseStarted should return a message with the correct pickle ID")] + public void BuildTestCaseStarted_ValidData_ShouldReturnMessageWithCorrectPickleId() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + + // ACT + var result = cucumberMessageFactory.BuildTestCaseStartedMessage(pickleId, dateTime); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.PickleId.Should().Be(pickleId.ToString("D")); + } + + [Fact(DisplayName = @"BuildTestCaseStarted should return a success when a UTC date has been specified")] + public void BuildTestCaseStarted_UtcDate_ShouldReturnSuccess() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + + // ACT + var result = cucumberMessageFactory.BuildTestCaseStartedMessage(pickleId, dateTime); + + // ASSERT + result.Should().BeAssignableTo>(); + } + + [Fact(DisplayName = @"BuildTestCaseFinished should return a message with the correct pickle ID")] + public void BuildTestCaseFinished_PickleId_ShouldReturnMessageWithCorrectPickleId() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + var testResult = new TestResult + { + DurationNanoseconds = 1000, + Message = "", + Status = Status.Passed + }; + + // ACT + var result = cucumberMessageFactory.BuildTestCaseFinishedMessage(pickleId, dateTime, testResult); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.PickleId.Should().Be(pickleId.ToString("D")); + } + + [Fact(DisplayName = @"BuildTestCaseFinished should return a message with the correct time stamp when a UTC time stamp is passed")] + public void BuildTestCaseFinished_UtcDateTime_ShouldReturnMessageWithCorrectTimeStamp() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + var testResult = new TestResult + { + DurationNanoseconds = 1000, + Message = "", + Status = Status.Passed + }; + + // ACT + var result = cucumberMessageFactory.BuildTestCaseFinishedMessage(pickleId, dateTime, testResult); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.Timestamp.ToDateTime().Should().Be(dateTime); + } + + [Theory(DisplayName = @"BuildTestCaseFinished should return a failure when a non-UTC time stamp is passed")] + [InlineData(DateTimeKind.Local)] + [InlineData(DateTimeKind.Unspecified)] + public void BuildTestCaseFinished_NonUtcDateTime_ShouldReturnFailure(DateTimeKind dateTimeKind) + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, dateTimeKind); + var pickleId = Guid.NewGuid(); + var testResult = new TestResult + { + DurationNanoseconds = 1000, + Message = "", + Status = Status.Passed + }; + + // ACT + var result = cucumberMessageFactory.BuildTestCaseFinishedMessage(pickleId, dateTime, testResult); + + // ASSERT + result.Should().BeAssignableTo>(); + } + + [Fact(DisplayName = @"BuildTestCaseFinished should return a message with the correct TestResult")] + public void BuildTestCaseFinished_TestResult_ShouldReturnMessageWithCorrectTestResult() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + var testResult = new TestResult + { + DurationNanoseconds = 1000, + Message = "", + Status = Status.Passed + }; + + // ACT + var result = cucumberMessageFactory.BuildTestCaseFinishedMessage(pickleId, dateTime, testResult); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.TestResult.Should().Be(testResult); + } + + [Fact(DisplayName = @"BuildTestCaseFinished should return a failure with exception information when null has been specified as TestResult")] + public void BuildTestCaseFinished_NullTestResult_ShouldReturnExceptionFailure() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var pickleId = Guid.NewGuid(); + + // ACT + var result = cucumberMessageFactory.BuildTestCaseFinishedMessage(pickleId, dateTime, default); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Exception.Should().BeOfType(); + } + + [Fact(DisplayName = @"BuildWrapperMessage should return a wrapper of type TestCaseFinished")] + public void BuildWrapperMessage_TestCaseFinishedSuccess_ShouldReturnWrapperOfTypeTestCaseFinished() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var testCaseFinished = new TestCaseFinished + { + PickleId = Guid.NewGuid().ToString(), + TestResult = new TestResult(), + Timestamp = Timestamp.FromDateTime(dateTime) + }; + + // ACT + var result = cucumberMessageFactory.BuildWrapperMessage(new Success(testCaseFinished)); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.MessageCase.Should().Be(Wrapper.MessageOneofCase.TestCaseFinished); + } + + [Fact(DisplayName = @"BuildWrapperMessage should return a wrapper with the passed TestCaseFinished message")] + public void BuildWrapperMessage_TestCaseFinishedSuccess_ShouldReturnWrapperWithTestCaseFinishedMessage() + { + // ARRANGE + var cucumberMessageFactory = new CucumberMessageFactory(); + var dateTime = new DateTime(2019, 5, 9, 14, 27, 48, DateTimeKind.Utc); + var testCaseFinished = new TestCaseFinished + { + PickleId = Guid.NewGuid().ToString(), + TestResult = new TestResult(), + Timestamp = Timestamp.FromDateTime(dateTime) + }; + + // ACT + var result = cucumberMessageFactory.BuildWrapperMessage(new Success(testCaseFinished)); + + // ASSERT + result.Should().BeAssignableTo>().Which + .Result.TestCaseFinished.Should().Be(testCaseFinished); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageSenderTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageSenderTests.cs new file mode 100644 index 000000000..0f70a8458 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/CucumberMessageSenderTests.cs @@ -0,0 +1,156 @@ +using System; +using FluentAssertions; +using Google.Protobuf.WellKnownTypes; +using Io.Cucumber.Messages; +using Moq; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.CucumberMessages; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.CucumberMessages +{ + public class CucumberMessageSenderTests + { + [Fact(DisplayName = @"SendTestCaseStarted should send a TestCaseStarted message to sink")] + public void SendTestCaseStarted_ValidParameters_ShouldSendTestRunStartedToSink() + { + // ARRANGE + Wrapper sentMessage = default; + + var cucumberMessageSinkMock = new Mock(); + cucumberMessageSinkMock.Setup(m => m.SendMessage(It.IsAny())) + .Callback(m => sentMessage = m); + + var cucumberMessageFactoryMock = GetCucumberMessageFactoryMock(); + var fieldValueProviderMock = GetFieldValueProviderMock(); + + var cucumberMessageSender = new CucumberMessageSender(cucumberMessageFactoryMock.Object, cucumberMessageSinkMock.Object, fieldValueProviderMock.Object); + var scenarioInfo = new ScenarioInfo("Test", "Description", "Tag1"); + + // ACT + cucumberMessageSender.SendTestCaseStarted(scenarioInfo); + + // ASSERT + sentMessage.MessageCase.Should().Be(Wrapper.MessageOneofCase.TestCaseStarted); + } + + [Fact(DisplayName = @"SendTestRunStarted should send a TestRunStated message to sink")] + public void SendTestRunStarted_ShouldSendTestRunStartedToSink() + { + // ARRANGE + Wrapper sentMessage = default; + + var cucumberMessageSinkMock = new Mock(); + cucumberMessageSinkMock.Setup(m => m.SendMessage(It.IsAny())) + .Callback(m => sentMessage = m); + + var cucumberMessageFactoryMock = GetCucumberMessageFactoryMock(); + var fieldValueProviderMock = GetFieldValueProviderMock(); + + var cucumberMessageSender = new CucumberMessageSender(cucumberMessageFactoryMock.Object, cucumberMessageSinkMock.Object, fieldValueProviderMock.Object); + + // ACT + cucumberMessageSender.SendTestRunStarted(); + + // ASSERT + sentMessage.MessageCase.Should().Be(Wrapper.MessageOneofCase.TestRunStarted); + } + + [Fact(DisplayName = @"SendTestRunStarted should send a TestRunStated message with correct time stamp to sink")] + public void SendTestRunStarted_ShouldSendTestRunStartedWithCorrectTimeStampToSink() + { + // ARRANGE + var now = new DateTime(2019, 5, 9, 15, 46, 5, DateTimeKind.Utc); + + Wrapper sentMessage = default; + + var cucumberMessageSinkMock = new Mock(); + cucumberMessageSinkMock.Setup(m => m.SendMessage(It.IsAny())) + .Callback(m => sentMessage = m); + + var cucumberMessageFactoryMock = GetCucumberMessageFactoryMock(); + var fieldValueProviderMock = GetFieldValueProviderMock(testRunStartedTimeStamp: now); + + var cucumberMessageSender = new CucumberMessageSender(cucumberMessageFactoryMock.Object, cucumberMessageSinkMock.Object, fieldValueProviderMock.Object); + + // ACT + cucumberMessageSender.SendTestRunStarted(); + + // ASSERT + sentMessage.MessageCase.Should().Be(Wrapper.MessageOneofCase.TestRunStarted); + sentMessage.TestRunStarted.Timestamp.ToDateTime().Should().Be(now); + } + + [Fact(DisplayName = @"SendTestRunStarted should send a TestRunStarted message with SpecFlow as used Cucumber implementation to sink")] + public void SendTestRunStarted_ShouldSendTestRunStartedWithSpecFlowAsUsedCucumberImplementationToSink() + { + // ARRANGE + const string expectedCucumberImplementation = @"SpecFlow"; + Wrapper sentMessage = default; + + var cucumberMessageSinkMock = new Mock(); + cucumberMessageSinkMock.Setup(m => m.SendMessage(It.IsAny())) + .Callback(m => sentMessage = m); + + var cucumberMessageFactoryMock = GetCucumberMessageFactoryMock(); + var fieldValueProviderMock = GetFieldValueProviderMock(); + + var cucumberMessageSender = new CucumberMessageSender(cucumberMessageFactoryMock.Object, cucumberMessageSinkMock.Object, fieldValueProviderMock.Object); + + // ACT + cucumberMessageSender.SendTestRunStarted(); + + // ASSERT + sentMessage.MessageCase.Should().Be(Wrapper.MessageOneofCase.TestRunStarted); + sentMessage.TestRunStarted.CucumberImplementation.Should().Be(expectedCucumberImplementation); + } + + public Mock GetCucumberMessageFactoryMock(string cucumberImplementation = "SpecFlow") + { + var cucumberMessageFactoryMock = new Mock(); + cucumberMessageFactoryMock.Setup(m => m.BuildWrapperMessage(It.IsAny>())) + .Returns>(r => Result.Success(new Wrapper { TestCaseStarted = r.Result })); + + cucumberMessageFactoryMock.Setup(m => m.BuildWrapperMessage(It.IsAny>())) + .Returns>(r => Result.Success(new Wrapper { TestRunStarted = r.Result })); + + cucumberMessageFactoryMock.Setup(m => m.BuildTestRunStartedMessage(It.IsAny())) + .Returns(timeStamp => Result.Success( + new TestRunStarted + { + Timestamp = Timestamp.FromDateTime(timeStamp), + CucumberImplementation = cucumberImplementation + })); + + cucumberMessageFactoryMock.Setup(m => m.BuildTestCaseStartedMessage(It.IsAny(), It.IsAny())) + .Returns((id, timeStamp) => Result.Success( + new TestCaseStarted + { + PickleId = $"{id:D}", + Timestamp = Timestamp.FromDateTime(timeStamp) + })); + return cucumberMessageFactoryMock; + } + + public Mock GetFieldValueProviderMock( + DateTime? testRunStartedTimeStamp = default, + DateTime? testCaseStartedTimeStamp = default, + Guid? testCaseStartedPickleId = default, + DateTime? testCaseFinishedTimeStamp = default, + Guid? testCaseFinishedPickleId = default) + { + var fieldValueProviderMock = new Mock(); + fieldValueProviderMock.Setup(m => m.GetTestRunStartedTime()) + .Returns(() => testRunStartedTimeStamp ?? DateTime.UtcNow); + fieldValueProviderMock.Setup(m => m.GetTestCaseStartedTime()) + .Returns(() => testCaseStartedTimeStamp ?? DateTime.UtcNow); + fieldValueProviderMock.Setup(m => m.GetTestCaseStartedPickleId(It.IsAny())) + .Returns(testCaseStartedPickleId ?? Guid.NewGuid()); + fieldValueProviderMock.Setup(m => m.GetTestCaseFinishedTime()) + .Returns(() => testCaseFinishedTimeStamp ?? DateTime.UtcNow); + fieldValueProviderMock.Setup(m => m.GetTestCaseFinishedPickleId(It.IsAny())) + .Returns(testCaseFinishedPickleId ?? Guid.NewGuid()); + return fieldValueProviderMock; + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/PickleIdStoreTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/PickleIdStoreTests.cs new file mode 100644 index 000000000..26fbcd48c --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/PickleIdStoreTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using TechTalk.SpecFlow.CucumberMessages; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.CucumberMessages +{ + public class PickleIdStoreTests + { + [Fact(DisplayName = @"GetPickleIdForScenarioInfo should add a Pickle ID to the dictionary if it does not already exist")] + public void GetPickleIdForScenarioInfo_ScenarioInfo_ShouldAddPickleIdToDictionaryIfNotExistent() + { + // ARRANGE + var dictionary = new Dictionary(); + var pickleIdStoreDictionaryFactoryMock = GetPickleIdStoreDictionaryFactoryMock(dictionary); + var guidToCreate = new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + var scenarioInfo = new ScenarioInfo("Title", "Description"); + var mock = GetPickleIdGeneratorMock(guidToCreate); + + var pickleIdStore = new PickleIdStore(mock.Object, pickleIdStoreDictionaryFactoryMock.Object); + + // ACT + pickleIdStore.GetPickleIdForScenario(scenarioInfo); + + // ASSERT + dictionary.Should().Contain(scenarioInfo, guidToCreate); + } + + [Fact(DisplayName = @"GetPickleIdForScenarioInfo should return the Pickle ID if it already exists")] + public void GetPickleIdForScenarioInfo_ScenarioInfo_ShouldReturnPickleIdIfAlreadyExists() + { + // ARRANGE + var existingGuid = new Guid(11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); + var guidToCreate = new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + var scenarioInfo = new ScenarioInfo("Title", "Description"); + var dictionary = new Dictionary { [scenarioInfo] = existingGuid }; + var pickleIdStoreDictionaryFactoryMock = GetPickleIdStoreDictionaryFactoryMock(dictionary); + var mock = GetPickleIdGeneratorMock(guidToCreate); + + var pickleIdStore = new PickleIdStore(mock.Object, pickleIdStoreDictionaryFactoryMock.Object); + + // ACT + var actualReturnedGuid = pickleIdStore.GetPickleIdForScenario(scenarioInfo); + + // ASSERT + actualReturnedGuid.Should().Be(existingGuid); + } + + [Fact(DisplayName = @"GetPickleIdForScenarioInfo should not overwrite the Pickle ID for already existing entries")] + public void GetPickleIdForScenarioInfo_ScenarioInfo_ShouldNotOverwritePickleIdIfAlreadyExists() + { + // ARRANGE + var existingGuid = new Guid(11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); + var guidToCreate = new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + var scenarioInfo = new ScenarioInfo("Title", "Description"); + var dictionary = new Dictionary { [scenarioInfo] = existingGuid }; + var pickleIdStoreDictionaryFactoryMock = GetPickleIdStoreDictionaryFactoryMock(dictionary); + var mock = GetPickleIdGeneratorMock(guidToCreate); + + var pickleIdStore = new PickleIdStore(mock.Object, pickleIdStoreDictionaryFactoryMock.Object); + + // ACT + pickleIdStore.GetPickleIdForScenario(scenarioInfo); + + // ASSERT + dictionary.Should().Contain(scenarioInfo, existingGuid) + .And.NotContain(scenarioInfo, guidToCreate); + } + + public Mock GetPickleIdStoreDictionaryFactoryMock(Dictionary dictionary) + { + var pickleIdStoreDictionaryFactoryMock = new Mock(); + pickleIdStoreDictionaryFactoryMock.Setup(m => m.BuildDictionary()) + .Returns(dictionary); + return pickleIdStoreDictionaryFactoryMock; + } + + public Mock GetPickleIdGeneratorMock(Guid guidToCreate) + { + var mock = new Mock(); + mock.Setup(m => m.GeneratePickleId()) + .Returns(guidToCreate); + return mock; + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/Sinks/ProtobufFileSinkOutputTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/Sinks/ProtobufFileSinkOutputTests.cs new file mode 100644 index 000000000..ec8a4f441 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/Sinks/ProtobufFileSinkOutputTests.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using FluentAssertions; +using Google.Protobuf.WellKnownTypes; +using Io.Cucumber.Messages; +using Moq; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.CucumberMessages.Sinks; +using TechTalk.SpecFlow.FileAccess; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.CucumberMessages.Sinks +{ + public class ProtobufFileSinkOutputTests + { + [Fact(DisplayName = @"WriteMessage should return success if the ProtobufFileSinkOutput is initialized")] + public void WriteMessage_Message_ShouldReturnSuccessIfInitialized() + { + // ARRANGE + var message = new Wrapper { TestRunStarted = new TestRunStarted()}; + var protobufFileSinkConfiguration = GetProtobufFileSinkConfiguration(); + var binaryFileAccessorMock = GetBinaryFileAccessorMock(); + var protobufFileNameResolverMock = GetProtobufFileNameResolverMock(); + + var protobufFileSinkOutput = new ProtobufFileSinkOutput(binaryFileAccessorMock.Object, protobufFileSinkConfiguration, protobufFileNameResolverMock.Object); + + // ACT + var actualResult = protobufFileSinkOutput.WriteMessage(message); + + // ASSERT + actualResult.Should().BeAssignableTo(); + } + + [Fact(DisplayName = @"WriteMessage should write the specified message to OutputStream")] + public void WriteMessage_Message_ShouldWriteTheSpecifiedMessageToOutputStream() + { + // ARRANGE + var message = new Wrapper + { + TestRunStarted = new TestRunStarted + { + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + CucumberImplementation = "SpecFlow" + } + }; + + var protobufFileSinkConfiguration = GetProtobufFileSinkConfiguration(); + var writableStream = GetWritableStream(); + var binaryFileAccessorMock = GetBinaryFileAccessorMock(Result.Success(writableStream)); + var protobufFileNameResolverMock = GetProtobufFileNameResolverMock(); + + var protobufFileSinkOutput = new ProtobufFileSinkOutput(binaryFileAccessorMock.Object, protobufFileSinkConfiguration, protobufFileNameResolverMock.Object); + + // ACT + protobufFileSinkOutput.WriteMessage(message); + + // ASSERT + writableStream.ToArray().Length.Should().BeGreaterThan(0); + } + + public Mock GetBinaryFileAccessorMock(IResult openOrAppendStream = null) + { + var binaryFileAccessorMock = new Mock(); + binaryFileAccessorMock.Setup(m => m.OpenAppendOrCreateFile(It.IsAny())) + .Returns(openOrAppendStream ?? Result.Success(GetWritableStream())); + return binaryFileAccessorMock; + } + + public Mock GetProtobufFileNameResolverMock() + { + var protobufFileNameResolverMock = new Mock(); + protobufFileNameResolverMock.Setup(m => m.Resolve(It.IsAny())) + .Returns(Result.Success); + return protobufFileNameResolverMock; + } + + public Stream GetNotWritableStream() + { + return new MemoryStream(new byte[0], false); + } + + public MemoryStream GetWritableStream() + { + return new MemoryStream(); + } + + public ProtobufFileSinkConfiguration GetProtobufFileSinkConfiguration(string targetFilePath = "CucumberMessageQueue") + { + return new ProtobufFileSinkConfiguration(targetFilePath); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/TestResultFactoryTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/TestResultFactoryTests.cs new file mode 100644 index 000000000..eb24ed9bb --- /dev/null +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/CucumberMessages/TestResultFactoryTests.cs @@ -0,0 +1,279 @@ +using System; +using FluentAssertions; +using Io.Cucumber.Messages; +using Moq; +using TechTalk.SpecFlow.CommonModels; +using TechTalk.SpecFlow.CucumberMessages; +using TechTalk.SpecFlow.ErrorHandling; +using Xunit; + +namespace TechTalk.SpecFlow.RuntimeTests.CucumberMessages +{ + public class TestResultFactoryTests + { + [Fact(DisplayName = @"BuildFromScenarioContext should return a failure with an ArgumentNullException when null is passed")] + public void BuildFromScenarioContext_Null_ShouldReturnFailureWithArgumentNullException() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + + // ACT + var actualTestResult = testResultFactory.BuildFromContext(null, null); + + // ASSERT + actualTestResult.Should().BeAssignableTo().Which + .Exception.Should().BeOfType(); + } + + [Fact(DisplayName = @"BuildPassedResult should return a TestResult with status Passed")] + public void BuildPassedResult_ValidParameters_ShouldReturnTestResultWithStatusPassed() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const Status expectedStatus = Status.Passed; + + // ACT + var actualTestResult = testResultFactory.BuildPassedResult(10Lu); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Status.Should().Be(expectedStatus); + } + + [Fact(DisplayName = @"BuildPassedResult should return a TestResult with the passed nanoseconds duration")] + public void BuildPassedResult_Nanoseconds_ShouldReturnTestResultWithCorrectNanoseconds() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const ulong expectedNanoseconds = 15Lu; + + // ACT + var actualTestResult = testResultFactory.BuildPassedResult(expectedNanoseconds); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.DurationNanoseconds.Should().Be(expectedNanoseconds); + } + + [Fact(DisplayName = @"BuildPassedResult should return a TestResult with empty message")] + public void BuildPassedResult_ValidParameters_ShouldReturnTestResultWithEmptyMessage() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const string expectedMessage = ""; + + // ACT + var actualTestResult = testResultFactory.BuildPassedResult(10Lu); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Message.Should().Be(expectedMessage); + } + + [Fact(DisplayName = @"BuildFailedResult should return a TestResult with status Failed")] + public void BuildFailedResult_ValidParameters_ShouldReturnTestResultWithStatusFailed() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const Status expectedStatus = Status.Failed; + + // ACT + var actualTestResult = testResultFactory.BuildFailedResult(10Lu, "Test Message"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Status.Should().Be(expectedStatus); + } + + [Fact(DisplayName = @"BuildFailedResult should return a TestResult with the passed nanoseconds duration")] + public void BuildFailedResult_Nanoseconds_ShouldReturnTestResultWithNanoseconds() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const ulong expectedNanoseconds = 15Lu; + + // ACT + var actualTestResult = testResultFactory.BuildFailedResult(expectedNanoseconds, "Test Message"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.DurationNanoseconds.Should().Be(expectedNanoseconds); + } + + [Fact(DisplayName = @"BuildFailedResult should return a TestResult with the passed message")] + public void BuildFailedResult_Message_ShouldReturnTestResultWithMessage() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const string expectedMessage = "This is a test message"; + + // ACT + var actualTestResult = testResultFactory.BuildFailedResult(10Lu, expectedMessage); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Message.Should().Be(expectedMessage); + } + + [Fact(DisplayName = @"BuildPendingResult should return a TestResult with status Pending")] + public void BuildPendingMessage_ValidParameters_ShouldReturnTestResultWithStatusPending() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const Status expectedStatus = Status.Pending; + + // ACT + var actualTestResult = testResultFactory.BuildPendingResult(10Lu, "Pending test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Status.Should().Be(expectedStatus); + } + + [Fact(DisplayName = @"BuildPendingResult should return a TestResult with the passed nanoseconds duration")] + public void BuildPendingResult_Nanoseconds_ShouldReturnTestResultWithNanoseconds() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const ulong expectedNanoseconds = 15Lu; + + // ACT + var actualTestResult = testResultFactory.BuildPendingResult(expectedNanoseconds, "Pending test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.DurationNanoseconds.Should().Be(expectedNanoseconds); + } + + [Fact(DisplayName = @"BuildPendingResult should return a TestResult with the passed message")] + public void BuildPendingResult_Message_ShouldReturnTestResultWithMessage() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const string expectedMessage = "This is a test message"; + + // ACT + var actualTestResult = testResultFactory.BuildPendingResult(10Lu, expectedMessage); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Message.Should().Be(expectedMessage); + } + + [Fact(DisplayName = @"BuildAmbiguousResult should return a TestResult with status Ambiguous")] + public void BuildAmbiguousResult_ValidParameters_ShouldReturnTestResultWithStatusAmbiguous() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const Status expectedStatus = Status.Ambiguous; + + // ACT + var actualTestResult = testResultFactory.BuildAmbiguousResult(10Lu, "Ambiguous test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Status.Should().Be(expectedStatus); + } + + [Fact(DisplayName = @"BuildAmbiguousResult should return a TestResult with the passed nanoseconds duration")] + public void BuildAmbiguousResult_Nanoseconds_ShouldReturnTestResultWithNanoseconds() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const ulong expectedNanoseconds = 15Lu; + + // ACT + var actualTestResult = testResultFactory.BuildAmbiguousResult(expectedNanoseconds, "Ambiguous test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.DurationNanoseconds.Should().Be(expectedNanoseconds); + } + + [Fact(DisplayName = @"BuildAmbiguousResult should return a TestResult with the passed message")] + public void BuildAmbiguousResult_Message_ShouldReturnTestResultWithMessage() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const string expectedMessage = "This is a test message"; + + // ACT + var actualTestResult = testResultFactory.BuildAmbiguousResult(10Lu, expectedMessage); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Message.Should().Be(expectedMessage); + } + + [Fact(DisplayName = @"BuildUndefinedResult should return a TestResult with status Undefined")] + public void BuildUndefinedResult_ValidParameters_ShouldReturnTestResultWithStatusUndefined() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const Status expectedStatus = Status.Undefined; + + // ACT + var actualTestResult = testResultFactory.BuildUndefinedResult(10Lu, "Undefined test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Status.Should().Be(expectedStatus); + } + + [Fact(DisplayName = @"BuildUndefinedResult should return a TestResult with the passed nanoseconds duration")] + public void BuildUndefinedResult_Nanoseconds_ShouldReturnTestResultWithNanoseconds() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const ulong expectedNanoseconds = 15Lu; + + // ACT + var actualTestResult = testResultFactory.BuildUndefinedResult(expectedNanoseconds, "Undefined test"); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.DurationNanoseconds.Should().Be(expectedNanoseconds); + } + + [Fact(DisplayName = @"BuildUndefinedResult should return a TestResult with the passed message")] + public void BuildUndefinedResult_Message_ShouldReturnTestResultWithMessage() + { + // ARRANGE + var testResultFactory = GetTestResultFactory(); + const string expectedMessage = "This is a test message"; + + // ACT + var actualTestResult = testResultFactory.BuildUndefinedResult(10Lu, expectedMessage); + + // ASSERT + actualTestResult.Should().BeAssignableTo>().Which + .Result.Message.Should().Be(expectedMessage); + } + + public TestResultFactory GetTestResultFactory(Mock errorProviderMock = null, Mock testUndefinedMessageFactoryMock = null) + { + var testResultFactory = new TestResultFactory( + new TestErrorMessageFactory(), + new TestPendingMessageFactory(errorProviderMock?.Object ?? GetErrorProviderMock().Object), + new TestAmbiguousMessageFactory(), + testUndefinedMessageFactoryMock?.Object ?? GetTestUndefinedMessageFactoryMock().Object); + return testResultFactory; + } + + public Mock GetErrorProviderMock() + { + var errorProviderMock = new Mock(); + errorProviderMock.Setup(m => m.GetPendingStepDefinitionError()) + .Returns(new PendingStepException()); + return errorProviderMock; + } + + public Mock GetTestUndefinedMessageFactoryMock() + { + var testUndefinedMessageFactoryMock = new Mock(); + testUndefinedMessageFactoryMock.Setup(m => m.BuildFromContext(It.IsAny(), It.IsAny())) + .Returns("Step definition not defined"); + return testUndefinedMessageFactoryMock; + } + } +} diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs index 02ddcd55f..2c52c0280 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/Infrastructure/TestExecutionEngineTests.cs @@ -14,6 +14,7 @@ using TechTalk.SpecFlow.Tracing; using TechTalk.SpecFlow.UnitTestProvider; using FluentAssertions; +using TechTalk.SpecFlow.CucumberMessages; namespace TechTalk.SpecFlow.RuntimeTests.Infrastructure { @@ -32,12 +33,15 @@ public class TestExecutionEngineTests private Mock stepDefinitionSkeletonProviderMock; private Mock testObjectResolverMock; private Mock obsoleteTestHandlerMock; + private Mock cucumberMessageSenderMock; private FeatureInfo featureInfo; private ScenarioInfo scenarioInfo; private ObjectContainer testThreadContainer; private ObjectContainer featureContainer; private ObjectContainer scenarioContainer; private TestObjectResolver defaultTestObjectResolver = new TestObjectResolver(); + private ITestPendingMessageFactory _testPendingMessageFactory; + private ITestUndefinedMessageFactory _testUndefinedMessageFactory; private List beforeScenarioEvents; private List afterScenarioEvents; @@ -120,6 +124,13 @@ public TestExecutionEngineTests() stepErrorHandlers = new Dictionary(); obsoleteTestHandlerMock = new Mock(); + + cucumberMessageSenderMock = new Mock(); + cucumberMessageSenderMock.Setup(m => m.SendTestRunStarted()) + .Callback(() => { }); + + _testPendingMessageFactory = new TestPendingMessageFactory(errorProviderStub.Object); + _testUndefinedMessageFactory = new TestUndefinedMessageFactory(stepDefinitionSkeletonProviderMock.Object, errorProviderStub.Object, specFlowConfiguration); } private TestExecutionEngine CreateTestExecutionEngine() @@ -131,17 +142,39 @@ private TestExecutionEngine CreateTestExecutionEngine() new Mock().Object, specFlowConfiguration, bindingRegistryStub.Object, - new Mock().Object, - stepDefinitionSkeletonProviderMock.Object, + new Mock().Object, contextManagerStub.Object, stepDefinitionMatcherStub.Object, stepErrorHandlers, methodBindingInvokerMock.Object, obsoleteTestHandlerMock.Object, + cucumberMessageSenderMock.Object, + new TestResultFactory(new TestErrorMessageFactory(), _testPendingMessageFactory, new TestAmbiguousMessageFactory(), _testUndefinedMessageFactory), + _testPendingMessageFactory, + _testUndefinedMessageFactory, testObjectResolverMock.Object, testThreadContainer); } + private Mock GetPickleIdStoreMock() + { + var dictionary = new Dictionary(); + var pickleIdStoreMock = new Mock(); + pickleIdStoreMock.Setup(m => m.GetPickleIdForScenario(It.IsAny())) + .Returns(info => + { + if (dictionary.ContainsKey(info)) + { + return dictionary[info]; + } + + var newGuid = Guid.NewGuid(); + dictionary.Add(info, newGuid); + return newGuid; + }); + return pickleIdStoreMock; + } + private Mock RegisterStepDefinition() { var methodStub = new Mock(); diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs index 493a6e034..6c54f9698 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerRunnerCreationTests.cs @@ -57,7 +57,7 @@ public void Should_initialize_test_runner_with_the_provided_assembly() var factory = CreateTestRunnerFactory(); factory.CreateTestRunner(0); - testRunnerFake.Verify(tr => tr.OnTestRunStart()); + factory.IsTestRunInitialized.Should().BeTrue(); } [Fact] @@ -68,7 +68,7 @@ public void Should_initialize_test_runner_with_additional_step_assemblies() factory.CreateTestRunner(0); - testRunnerFake.Verify(tr => tr.OnTestRunStart()); + factory.IsTestRunInitialized.Should().BeTrue(); } [Fact] @@ -79,8 +79,7 @@ public void Should_initialize_test_runner_with_the_provided_assembly_even_if_the factory.CreateTestRunner(0); - testRunnerFake.Verify(tr => tr.OnTestRunStart()); - + factory.IsTestRunInitialized.Should().BeTrue(); } diff --git a/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerStaticApiTest.cs b/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerStaticApiTest.cs index de54f61d1..599b19556 100644 --- a/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerStaticApiTest.cs +++ b/Tests/TechTalk.SpecFlow.RuntimeTests/TestRunnerManagerStaticApiTest.cs @@ -69,6 +69,18 @@ public static void AfterTestRun() } } + [Binding] + public class BeforeTestRunTestBinding + { + public static int BeforeTestRunCallCount = 0; + + [BeforeTestRun] + public static void BeforeTestRun() + { + BeforeTestRunCallCount++; + } + } + [Fact] public void OnTestRunEnd_should_fire_AfterTestRun_events() { @@ -107,19 +119,47 @@ public void OnTestRunEnd_should_not_fire_AfterTestRun_events_multiple_times() } [Fact] - public void DomainUnload_event_should_not_fire_AfterTestRun_events_multiple_times_after_OnTestRunEnd() + public void OnTestRunStart_should_fire_BeforeTestRun_events() { - // make sure a test runner is initialized - TestRunnerManager.GetTestRunner(thisAssembly); + BeforeTestRunTestBinding.BeforeTestRunCallCount = 0; //reset + TestRunnerManager.OnTestRunStart(thisAssembly); - AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset - TestRunnerManager.OnTestRunEnd(thisAssembly); + BeforeTestRunTestBinding.BeforeTestRunCallCount.Should().Be(1); + } - // simulating DomainUnload event - var trm = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(thisAssembly); - trm.OnDomainUnload(); + [Fact] + public void OnTestRunStart_without_arguments_should_fire_BeforeTestRun_events_for_calling_assembly() + { + BeforeTestRunTestBinding.BeforeTestRunCallCount = 0; //reset + TestRunnerManager.OnTestRunStart(); - AfterTestRunTestBinding.AfterTestRunCallCount.Should().Be(1); + BeforeTestRunTestBinding.BeforeTestRunCallCount.Should().Be(1); + } + + [Fact] + public void OnTestRunStart_should_not_fire_BeforeTestRun_events_multiple_times() + { + BeforeTestRunTestBinding.BeforeTestRunCallCount = 0; //reset + TestRunnerManager.OnTestRunStart(thisAssembly); + TestRunnerManager.OnTestRunStart(thisAssembly); + + BeforeTestRunTestBinding.BeforeTestRunCallCount.Should().Be(1); } + + //[Fact] + //public void DomainUnload_event_should_not_fire_AfterTestRun_events_multiple_times_after_OnTestRunEnd() + //{ + // // make sure a test runner is initialized + // TestRunnerManager.GetTestRunner(thisAssembly); + + // AfterTestRunTestBinding.AfterTestRunCallCount = 0; //reset + // TestRunnerManager.OnTestRunEnd(thisAssembly); + + // // simulating DomainUnload event + // var trm = (TestRunnerManager)TestRunnerManager.GetTestRunnerManager(thisAssembly); + // trm.OnDomainUnload(); + + // AfterTestRunTestBinding.AfterTestRunCallCount.Should().Be(1); + //} } } diff --git a/Tests/TechTalk.SpecFlow.Specs/Drivers/CucumberMessages/MessageValidationDriver.cs b/Tests/TechTalk.SpecFlow.Specs/Drivers/CucumberMessages/MessageValidationDriver.cs new file mode 100644 index 000000000..d63dcceda --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/Drivers/CucumberMessages/MessageValidationDriver.cs @@ -0,0 +1,44 @@ +using TechTalk.SpecFlow.Assist; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages.RowObjects; + +namespace TechTalk.SpecFlow.Specs.Drivers.CucumberMessages +{ + public class MessageValidationDriver + { + private readonly TestCaseFinishedDriver _testCaseFinishedDriver; + private readonly TestRunStartedDriver _testRunStartedDriver; + private readonly TestCaseStartedDriver _testCaseStartedDriver; + + public MessageValidationDriver(TestCaseFinishedDriver testCaseFinishedDriver, TestRunStartedDriver testRunStartedDriver, TestCaseStartedDriver testCaseStartedDriver) + { + _testCaseFinishedDriver = testCaseFinishedDriver; + _testRunStartedDriver = testRunStartedDriver; + _testCaseStartedDriver = testCaseStartedDriver; + } + + public void TestCaseFinishedMessageShouldHaveBeenSent(Table values) + { + var testCaseFinishedRow = values.CreateInstance(); + _testCaseFinishedDriver.TestCaseFinishedMessageShouldHaveBeenSent(testCaseFinishedRow); + } + + public void TestCaseFinishedMessageShouldHaveBeenSentWithTestResult(Table values) + { + var testResultRow = values.CreateInstance(); + _testCaseFinishedDriver.TestCaseFinishedMessageShouldHaveBeenSentWithTestResult(testResultRow); + } + + public void TestRunStartedMessageShouldHaveBeenSent(Table values) + { + var testRunStartedRow = values.CreateInstance(); + _testRunStartedDriver.TestRunStartedMessageShouldHaveBeenSent(testRunStartedRow); + } + + public void TestCaseStartedMessageShouldHaveBeenSent(Table values) + { + var testCaseStartedRow = values.CreateInstance(); + _testCaseStartedDriver.TestCaseStartedMessageShouldHaveBeenSent(testCaseStartedRow); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/ScenarioSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/ScenarioSteps.cs new file mode 100644 index 000000000..ac562665a --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/ScenarioSteps.cs @@ -0,0 +1,59 @@ +using System; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; + +namespace TechTalk.SpecFlow.Specs.StepDefinitions.CucumberMessages +{ + [Binding] + public class ScenarioSteps + { + private readonly TestSuiteSetupDriver _testSuiteSetupDriver; + + public ScenarioSteps(TestSuiteSetupDriver testSuiteSetupDriver) + { + _testSuiteSetupDriver = testSuiteSetupDriver; + } + + [Given(@"there are '(\d+)' scenarios")] + [Given(@"there are (\d+) scenarios")] + public void GivenThereAreScenarios(int scenarios) + { + _testSuiteSetupDriver.AddScenarios(scenarios); + } + + [Given(@"there is a scenario")] + public void GivenThereIsAScenario() + { + _testSuiteSetupDriver.AddScenarios(1); + } + + [Given(@"there is a scenario with PickleId '(.*)'")] + public void GivenThereIsAScenarioWithPickleId(Guid pickleId) + { + _testSuiteSetupDriver.AddScenario(pickleId); + } + + [Given(@"there are two step definitions with identical bindings")] + public void GivenThereAreTwoStepDefinitionsWithIdenticalRegex() + { + _testSuiteSetupDriver.AddDuplicateStepDefinition(); + } + + [Given(@"there are no matching step definitions")] + public void GivenThereAreNoMatchingStepDefinitions() + { + _testSuiteSetupDriver.AddNotMatchingStepDefinition(); + } + + [Given(@"there is a scenario with the following steps: '(.*)'")] + public void GivenThereIsAScenarioWithTheFollowingSteps(string step) + { + _testSuiteSetupDriver.AddScenarioWithGivenStep(step); + } + + [Given(@"with step definitions in the following order: '(.*)'")] + public void GivenWithStepDefinitionsInTheFollowingOrder(string stepDefinitionOrder) + { + _testSuiteSetupDriver.AddStepDefinitionsFromStringList(stepDefinitionOrder); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseFinishedSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseFinishedSteps.cs new file mode 100644 index 000000000..0e9096a37 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseFinishedSteps.cs @@ -0,0 +1,57 @@ +using System; +using TechTalk.SpecFlow.Specs.Drivers.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator.Driver; + +namespace TechTalk.SpecFlow.Specs.StepDefinitions.CucumberMessages +{ + [Binding] + public class TestCaseFinishedSteps + { + private readonly TestSuiteInitializationDriver _testSuiteInitializationDriver; + private readonly TestSuiteSetupDriver _testSuiteSetupDriver; + private readonly SolutionDriver _solutionDriver; + private readonly VSTestExecutionDriver _vsTestExecutionDriver; + private readonly TestCaseFinishedDriver _testCaseFinishedDriver; + private readonly MessageValidationDriver _messageValidationDriver; + + public TestCaseFinishedSteps(TestSuiteInitializationDriver testSuiteInitializationDriver, TestSuiteSetupDriver testSuiteSetupDriver, SolutionDriver solutionDriver, VSTestExecutionDriver vsTestExecutionDriver, TestCaseFinishedDriver testCaseFinishedDriver, MessageValidationDriver messageValidationDriver) + { + _testSuiteInitializationDriver = testSuiteInitializationDriver; + _testSuiteSetupDriver = testSuiteSetupDriver; + _solutionDriver = solutionDriver; + _vsTestExecutionDriver = vsTestExecutionDriver; + _testCaseFinishedDriver = testCaseFinishedDriver; + _messageValidationDriver = messageValidationDriver; + } + + [When(@"the scenario is finished at '(.*)'")] + public void WhenTheScenarioIsFinishedAt(DateTime finishTime) + { + _testSuiteInitializationDriver.OverrideTestCaseFinishedTime = finishTime; + _testSuiteSetupDriver.EnsureAProjectIsCreated(); + _solutionDriver.CompileSolution(BuildTool.MSBuild); + _solutionDriver.CheckSolutionShouldHaveCompiled(); + _vsTestExecutionDriver.ExecuteTests(); + } + + [Then(@"(.*) TestCaseFinished messages have been sent")] + public void ThenTestCaseFinishedMessagesHaveBeenSent(int numberOfMessages) + { + _testCaseFinishedDriver.TestCaseFinishedMessagesShouldHaveBeenSent(numberOfMessages); + } + + [Then(@"a TestCaseFinished message has been sent with the following attributes")] + public void ThenATestCaseFinishedMessageHasBeenSentWithTheFollowingAttributes(Table table) + { + _messageValidationDriver.TestCaseFinishedMessageShouldHaveBeenSent(table); + } + + [Then(@"a TestCaseFinished message has been sent with the following TestResult")] + public void ThenATestCaseFinishedMessageHasBeenSentWithTheFollowingTestResult(Table table) + { + _messageValidationDriver.TestCaseFinishedMessageShouldHaveBeenSentWithTestResult(table); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseStartedSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseStartedSteps.cs new file mode 100644 index 000000000..5277b46a1 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestCaseStartedSteps.cs @@ -0,0 +1,60 @@ +using System; +using TechTalk.SpecFlow.Specs.Drivers.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator.Driver; + +namespace TechTalk.SpecFlow.Specs.StepDefinitions.CucumberMessages +{ + [Binding] + public class TestCaseStartedSteps + { + private readonly VSTestExecutionDriver _vsTestExecutionDriver; + private readonly TestCaseStartedDriver _testCaseStartedDriver; + private readonly SolutionDriver _solutionDriver; + private readonly TestSuiteInitializationDriver _testSuiteInitializationDriver; + private readonly TestSuiteSetupDriver _testSuiteSetupDriver; + private readonly MessageValidationDriver _messageValidationDriver; + + public TestCaseStartedSteps(VSTestExecutionDriver vsTestExecutionDriver, TestCaseStartedDriver testCaseStartedDriver, SolutionDriver solutionDriver, TestSuiteInitializationDriver testSuiteInitializationDriver, TestSuiteSetupDriver testSuiteSetupDriver, MessageValidationDriver messageValidationDriver) + { + _vsTestExecutionDriver = vsTestExecutionDriver; + _testCaseStartedDriver = testCaseStartedDriver; + _solutionDriver = solutionDriver; + _testSuiteInitializationDriver = testSuiteInitializationDriver; + _testSuiteSetupDriver = testSuiteSetupDriver; + _messageValidationDriver = messageValidationDriver; + } + + [When(@"the scenario is executed")] + public void WhenTheScenarioIsExecuted() + { + _testSuiteSetupDriver.EnsureAProjectIsCreated(); + _solutionDriver.CompileSolution(BuildTool.MSBuild); + _solutionDriver.CheckSolutionShouldHaveCompiled(); + _vsTestExecutionDriver.ExecuteTests(); + } + + [When(@"the scenario is started at '(.*)'")] + public void WhenTheScenarioIsStartedAt(DateTime startTime) + { + _testSuiteInitializationDriver.OverrideTestCaseStartedTime = startTime; + _testSuiteSetupDriver.EnsureAProjectIsCreated(); + _solutionDriver.CompileSolution(BuildTool.MSBuild); + _solutionDriver.CheckSolutionShouldHaveCompiled(); + _vsTestExecutionDriver.ExecuteTests(); + } + + [Then(@"'(\d+)' TestCaseStarted messages have been sent")] + public void ThenTestCaseStartedMessagesHaveBeenSent(int numberOfTestCaseStartedMessages) + { + _testCaseStartedDriver.TestCaseStartedMessagesShouldHaveBeenSent(numberOfTestCaseStartedMessages); + } + + [Then(@"a TestCaseStarted message has been sent with the following attributes")] + public void ThenATestCaseStartedMessageHasBeenSentWithTheFollowingAttributes(Table table) + { + _messageValidationDriver.TestCaseStartedMessageShouldHaveBeenSent(table); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestRunStartedSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestRunStartedSteps.cs new file mode 100644 index 000000000..7e2cefc99 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestRunStartedSteps.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using TechTalk.SpecFlow.Specs.Drivers.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; +using TechTalk.SpecFlow.TestProjectGenerator.Driver; + +namespace TechTalk.SpecFlow.Specs.StepDefinitions.CucumberMessages +{ + [Binding] + public class TestRunStartedSteps + { + private readonly VSTestExecutionDriver _vsTestExecutionDriver; + private readonly TestRunStartedDriver _testRunStartedDriver; + private readonly SolutionDriver _solutionDriver; + private readonly TestSuiteSetupDriver _testSuiteSetupDriver; + private readonly TestSuiteInitializationDriver _testSuiteInitializationDriver; + private readonly MessageValidationDriver _messageValidationDriver; + private readonly ScenarioContext _scenarioContext; + + public TestRunStartedSteps(VSTestExecutionDriver vsTestExecutionDriver, TestRunStartedDriver testRunStartedDriver, SolutionDriver solutionDriver, TestSuiteSetupDriver testSuiteSetupDriver, TestSuiteInitializationDriver testSuiteInitializationDriver, MessageValidationDriver messageValidationDriver, + ScenarioContext scenarioContext) + { + _vsTestExecutionDriver = vsTestExecutionDriver; + _testRunStartedDriver = testRunStartedDriver; + _solutionDriver = solutionDriver; + _testSuiteSetupDriver = testSuiteSetupDriver; + _testSuiteInitializationDriver = testSuiteInitializationDriver; + _messageValidationDriver = messageValidationDriver; + this._scenarioContext = scenarioContext; + } + + [When(@"the test suite is executed")] + [When(@"the test suite was executed")] + public void WhenTheTestSuiteIsExecuted() + { + _testSuiteSetupDriver.EnsureAProjectIsCreated(); + _solutionDriver.CompileSolution(BuildTool.MSBuild); + _solutionDriver.CheckSolutionShouldHaveCompiled(); + _vsTestExecutionDriver.ExecuteTests(); + } + + [When(@"the test suite is started at '(.*)'")] + public void WhenTheTestSuiteIsStartedAt(DateTime startTime) + { + _testSuiteSetupDriver.EnsureAProjectIsCreated(); + _testSuiteInitializationDriver.OverrideTestSuiteStartupTime = startTime; + } + + [Then(@"'(\d+)' TestRunStarted messages have been sent")] + public void ThenATestRunStartedMessageHasBeenSent(int numberOfMessages) + { + if (_scenarioContext.ScenarioInfo.Tags.Contains("SpecFlow")) + { + return; + } + + _testRunStartedDriver.TestRunStartedMessageShouldHaveBeenSent(numberOfMessages); + } + + [Then(@"a TestRunStarted message has been sent with the following attributes")] + public void ThenATestRunStartedMessageHasBeenSentWithTheFollowingAttributes(Table attributesTable) + { + _messageValidationDriver.TestRunStartedMessageShouldHaveBeenSent(attributesTable); + } + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestSuiteSteps.cs b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestSuiteSteps.cs new file mode 100644 index 000000000..bd1dc3d33 --- /dev/null +++ b/Tests/TechTalk.SpecFlow.Specs/StepDefinitions/CucumberMessages/TestSuiteSteps.cs @@ -0,0 +1,60 @@ +using System.Linq; +using TechTalk.SpecFlow.TestProjectGenerator.CucumberMessages; + +namespace TechTalk.SpecFlow.Specs.StepDefinitions.CucumberMessages +{ + [Binding] + public class TestSuiteSteps + { + private readonly TestSuiteSetupDriver _testSuiteSetupDriver; + private readonly ScenarioContext _scenarioContext; + + public TestSuiteSteps(TestSuiteSetupDriver testSuiteSetupDriver, ScenarioContext scenarioContext) + { + _testSuiteSetupDriver = testSuiteSetupDriver; + _scenarioContext = scenarioContext; + } + + [Given(@"there are '(\d+)' feature files")] + [Given(@"there are (\d+) feature files")] + public void GivenThereAreFeatureFiles(int featureFilesCount) + { + if (_scenarioContext.ScenarioInfo.Tags.Contains("SpecFlow")) + { + return; + } + + _testSuiteSetupDriver.AddFeatureFiles(featureFilesCount); + } + + [Given(@"the cucumber implementation is (.*)")] + public void GivenTheCucumberImplementationIs(string cucumberImplementation) + { + if (_scenarioContext.ScenarioInfo.Tags.Contains("SpecFlow")) + { + return; + } + + _testSuiteSetupDriver.AddFeatureFiles(1); + _testSuiteSetupDriver.AddGenericWhenStepBinding(); + } + + + + [Given(@"the test runner is '(.*)'")] + [Scope(Tag = "SpecFlow")] + public void GivenTheTestRunnerIs(string testRunner) + { + + } + + [When(@"the test suite was executed with a testThreadCount of '(.*)'")] + [Scope(Tag = "SpecFlow")] + public void WhenTheTestSuiteWasExecutedWithATestThreadCountOf(int p0) + { + + } + + + } +} diff --git a/Tests/TechTalk.SpecFlow.Specs/TechTalk.SpecFlow.Specs.csproj b/Tests/TechTalk.SpecFlow.Specs/TechTalk.SpecFlow.Specs.csproj index a8fcf9f2d..468344db3 100644 --- a/Tests/TechTalk.SpecFlow.Specs/TechTalk.SpecFlow.Specs.csproj +++ b/Tests/TechTalk.SpecFlow.Specs/TechTalk.SpecFlow.Specs.csproj @@ -37,6 +37,10 @@ + + + + @@ -62,7 +66,6 @@ - <_SpecFlow_PluginTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">netcoreapp2.0 <_SpecFlow_PluginTFM Condition=" '$(MSBuildRuntimeType)' != 'Core'">net471 @@ -77,6 +80,13 @@ + + + + + + + @@ -93,8 +103,9 @@ <_SpecFlow_TaskAssembly>..\..\SpecFlow.Tools.MsBuild.Generation\bin\$(Configuration)\$(_SpecFlow_TaskFolder)\SpecFlow.Tools.MsBuild.Generation.dll - - + + + PreBuild; $(BuildDependsOn) diff --git a/nuget.config b/nuget.config index 5b498c0f5..56cc63a2b 100644 --- a/nuget.config +++ b/nuget.config @@ -4,5 +4,6 @@ + \ No newline at end of file