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