From d9e3dc27029c99567c9c676d8576d7dfd0a115c6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:35:21 -0500 Subject: [PATCH 001/218] Add TimeStamp to all ExecutionEvents. These will be used when serializing events as Cucumber Messages. --- Reqnroll/Events/ExecutionEvent.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index afa94a42d..cc6ab3742 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -5,6 +5,9 @@ namespace Reqnroll.Events { public class ExecutionEvent : IExecutionEvent { + public DateTime Timestamp { get; } + + public ExecutionEvent() => Timestamp = DateTime.Now; } public class TestRunStartedEvent : ExecutionEvent From 73e48e29d2b68e942ec4b4f083c13b9604f2d415 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:56:02 -0500 Subject: [PATCH 002/218] Experiment: Threading.Channel used to pump Messages from TestThreadExecutionEvents to one or more Message Sinks. Default implementation of one Sink that serializes Messages to an ndjson file per feature. Not finished: hooking up the TestThreadExecution event handlers. --- .../CucumberMessagEnumConverter.cs | 40 +++++++++++++ .../FileSinkPlugin.cs | 57 +++++++++++++++++++ .../NdjsonSerializer.cs | 56 ++++++++++++++++++ ...mberMessage.FileSink.ReqnrollPlugin.csproj | 23 ++++++++ Reqnroll.sln | 10 ++++ .../CucumberMessageBroker.cs | 51 +++++++++++++++++ .../CucumberMessageExtensions.cs | 27 +++++++++ .../CucumberMessagePublisher.cs | 39 +++++++++++++ .../CucumberMessageSinkBase.cs | 36 ++++++++++++ .../CucumberMesssages/ICucumberMessageSink.cs | 10 ++++ .../ReqnrollCucumberMessage.cs | 11 ++++ Reqnroll/Reqnroll.csproj | 7 +++ 12 files changed, 367 insertions(+) create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageBroker.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessageSink.cs create mode 100644 Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs new file mode 100644 index 000000000..c5fb0457e --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Cucumber.Messages +{ + internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum + { + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + protected internal CucumberMessageEnumConverter() + { + var type = typeof(T); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var value = (T)field.GetValue(null)!; + var attribute = field.GetCustomAttribute(); + var name = attribute?.Description ?? field.Name; + _enumToString[value] = name; + _stringToEnum[name] = value; + } + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString(); + return _stringToEnum.TryGetValue(stringValue!, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : value.ToString()); + } + } + +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs new file mode 100644 index 000000000..826f9041a --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -0,0 +1,57 @@ +using Reqnroll.CucumberMesssages; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using Io.Cucumber.Messages; +using Cucumber.Messages; +using Io.Cucumber.Messages.Types; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class FileSinkPlugin : CucumberMessageSinkBase + { + new public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + base.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + + Task.Run(() => ConsumeAndWriteToFiles()); + } + + private async Task ConsumeAndWriteToFiles() + { + await foreach (var message in Consume()) + { + var featureName = message.CucumberMessageSource; + if (message.Envelope != null) + { + Write(featureName, Serialize(message.Envelope)); + } + else + { + CloseFeature(featureName); + } + } + } + + private Dictionary fileStreams = new(); + private string baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "CucumberMessages"); + + private string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + private void Write(string featureName, string cucumberMessage) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + fileStreams[featureName].WriteLine(cucumberMessage); + } + + private void CloseFeature(string featureName) + { + fileStreams[featureName].Close(); + fileStreams.Remove(featureName); + } + } +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs new file mode 100644 index 000000000..4ea75d028 --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs @@ -0,0 +1,56 @@ +using Io.Cucumber.Messages.Types; +using System; +using System.Text.Json; + +namespace Cucumber.Messages +{ + /// + /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. + /// Consumers of Cucumber.Messages should use these options, or their serialization library's equivalent options. + /// These options should work with System.Text.Json v6 or above. + /// + public class NdjsonSerializer + { + private static readonly Lazy _jsonOptions = new(() => + { + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + + return options; + }); + + private static JsonSerializerOptions JsonOptions { get + { + return _jsonOptions.Value; + } + } + + public static string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + + internal static string Serialize(T message) + { + return JsonSerializer.Serialize(message, JsonOptions); + } + + public static Envelope Deserialize(string json) + { + return NdjsonSerializer.Deserialize(json); + } + + internal static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions)!; + } + } +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj new file mode 100644 index 000000000..0b4956b03 --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + enable + enable + + + + + + + + + + + + + ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + + + + diff --git a/Reqnroll.sln b/Reqnroll.sln index 521df415a..60a1c844f 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,6 +116,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessageConsumerTest", "CucumberMessageConsumerTest", "{01874043-F824-4C72-A7F5-CA1B16031714}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -242,6 +246,10 @@ Global {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -280,6 +288,8 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} + {01874043-F824-4C72-A7F5-CA1B16031714} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs new file mode 100644 index 000000000..c771724cb --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -0,0 +1,51 @@ +using Reqnroll.BoDi; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Security.Authentication.ExtendedProtection; +using System.Text; +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + + public interface ICucumberMessageBroker + { + Task CompleteAsync(string cucumberMessageSource); + Task PublishAsync(ReqnrollCucumberMessage message); + } + + public class CucumberMessageBroker : ICucumberMessageBroker + { + private List registeredSinks; + + + public CucumberMessageBroker(IObjectContainer objectContainer) + { + var sinks = objectContainer.ResolveAll(); + registeredSinks = new List(sinks); + } + public async Task PublishAsync(ReqnrollCucumberMessage message) + { + foreach (var sink in registeredSinks) + { + await sink.Publish(message); + } + } + + // using an empty CucumberMessage to indicate completion + public async Task CompleteAsync(string cucumberMessageSource) + { + var completionMessage = new ReqnrollCucumberMessage + { + CucumberMessageSource = cucumberMessageSource + }; + + foreach (var sink in registeredSinks) + { + await sink.Publish(completionMessage); + } + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs new file mode 100644 index 000000000..de2ae191c --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; + + +namespace Reqnroll.CucumberMesssages +{ +#if NETSTANDARD2_0 + public static class CucumberMessageExtensions + { + public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (reader.TryRead(out T? item)) + { + yield return item; + } + } + } + } +#endif +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs new file mode 100644 index 000000000..1e16854eb --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -0,0 +1,39 @@ +using Reqnroll.BoDi; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Io.Cucumber.Messages; +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMesssages +{ + public class CucumberMessagePublisher + { + private ICucumberMessageBroker broker; + + public CucumberMessagePublisher(ITestThreadExecutionEventPublisher eventSource, ICucumberMessageBroker CucumberMessageBroker) + { + broker = CucumberMessageBroker; + eventSource.AddHandler(FeatureStartedEventHandler); + eventSource.AddHandler(FeatureFinishedEventHandler); + } + + private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) + { + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.FolderPath; + Task.Run(() => broker.CompleteAsync(featureName)); + } + + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + { + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.FolderPath; + Task.Run(() => broker.PublishAsync(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) + })); + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs new file mode 100644 index 000000000..99be48a47 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -0,0 +1,36 @@ +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using System.Collections.Generic; +using System.Threading.Channels; +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + public class CucumberMessageSinkBase : ICucumberMessageSink, IRuntimePlugin + { + private Channel _channel = Channel.CreateUnbounded(); + public async Task Publish(ReqnrollCucumberMessage message) + { + await _channel.Writer.WriteAsync(message); + if (message.Envelope == null) + { + _channel.Writer.Complete(); + } + } + + public async IAsyncEnumerable Consume() + { + await foreach (var message in _channel.Reader.ReadAllAsync()) + { + yield return message; + } + } + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + } + + } +} diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs new file mode 100644 index 000000000..a796bdb24 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + public interface ICucumberMessageSink + { + Task Publish(ReqnrollCucumberMessage message); + } +} diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs new file mode 100644 index 000000000..d91af932d --- /dev/null +++ b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs @@ -0,0 +1,11 @@ +using Io.Cucumber.Messages.Types; + + +namespace Reqnroll.CucumberMesssages +{ + public class ReqnrollCucumberMessage + { + public string CucumberMessageSource { get; set; } + public Envelope Envelope { get; set; } + } +} diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 85bca01b9..ed47c9ca2 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -29,6 +29,7 @@ + @@ -41,6 +42,12 @@ + + + ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + + + From 63e95fc2421fd19a166ab11f2d50957a4fd71077 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:33:18 -0500 Subject: [PATCH 003/218] Experimental: plumbed the instantiation and intialization of the Cucumber Messages Publisher and Broker to the initialization sequence provided by the Plugin architecture. --- .../FileSinkPlugin.cs | 4 ++-- Reqnroll.sln | 8 ++++---- .../CucumberMesssages/CucumberMessagePublisher.cs | 12 ++++++++---- .../CucumberMesssages/CucumberMessageSinkBase.cs | 9 ++++++++- .../CucumberMesssages/ICucumberMessagePublisher.cs | 9 +++++++++ 5 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 826f9041a..501106f87 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -27,7 +27,7 @@ private async Task ConsumeAndWriteToFiles() } else { - CloseFeature(featureName); + CloseFeatureStream(featureName); } } } @@ -48,7 +48,7 @@ private void Write(string featureName, string cucumberMessage) fileStreams[featureName].WriteLine(cucumberMessage); } - private void CloseFeature(string featureName) + private void CloseFeatureStream(string featureName) { fileStreams[featureName].Close(); fileStreams.Remove(featureName); diff --git a/Reqnroll.sln b/Reqnroll.sln index 60a1c844f..b17411911 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,10 +116,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessageConsumerTest", "CucumberMessageConsumerTest", "{01874043-F824-4C72-A7F5-CA1B16031714}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -288,8 +288,8 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {01874043-F824-4C72-A7F5-CA1B16031714} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} + {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1e16854eb..063eb906b 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -9,15 +9,19 @@ namespace Reqnroll.CucumberMesssages { - public class CucumberMessagePublisher + public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; - public CucumberMessagePublisher(ITestThreadExecutionEventPublisher eventSource, ICucumberMessageBroker CucumberMessageBroker) + public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker) { broker = CucumberMessageBroker; - eventSource.AddHandler(FeatureStartedEventHandler); - eventSource.AddHandler(FeatureFinishedEventHandler); + } + + public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) + { + testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); + testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index 99be48a47..199664475 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -1,4 +1,5 @@ -using Reqnroll.Plugins; +using Reqnroll.Events; +using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; using System.Collections.Generic; using System.Threading.Channels; @@ -30,6 +31,12 @@ public async IAsyncEnumerable Consume() public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var publisher = args.ObjectContainer.Resolve(); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; } } diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs new file mode 100644 index 000000000..458747f4f --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs @@ -0,0 +1,9 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + public interface ICucumberMessagePublisher + { + public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher); + } +} \ No newline at end of file From 57b21d0e170d11dd8144dab6b151281f6678f50e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:30:16 -0500 Subject: [PATCH 004/218] Exploratory: Extended System Tests to create a CucumberCompatibilityTest project. The File Sink is hooked in as a nuget package. Next: exploratory debugging to confirm the event listeners are wired up correctly at Feature start time. --- .../CucumberMessagEnumConverter.cs | 0 .../FileSinkPlugin.cs | 6 ++++- .../NdjsonSerializer.cs | 0 ...mberMessage.FileSink.ReqnrollPlugin.csproj | 7 +++-- Reqnroll.sln | 21 ++++++++++----- .../CucumberCompatibilityTestBase.cs | 18 +++++++++++++ .../CucumberCompatibilityTests.cs | 24 +++++++++++++++++ ...CucumberMessages.CompatibilityTests.csproj | 27 +++++++++++++++++++ nuget.config | 3 ++- 9 files changed, 95 insertions(+), 11 deletions(-) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/CucumberMessagEnumConverter.cs (100%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/FileSinkPlugin.cs (92%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/NdjsonSerializer.cs (100%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj (55%) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs similarity index 100% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs similarity index 92% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 501106f87..ac9359a90 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -4,6 +4,10 @@ using Io.Cucumber.Messages; using Cucumber.Messages; using Io.Cucumber.Messages.Types; +using System.Reflection; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; + +[assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { @@ -25,7 +29,7 @@ private async Task ConsumeAndWriteToFiles() { Write(featureName, Serialize(message.Envelope)); } - else + else { CloseFeatureStream(featureName); } diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs similarity index 100% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj similarity index 55% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 0b4956b03..afea6d0f2 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -4,6 +4,9 @@ netstandard2.0 enable enable + True + Reqnroll Plugin which publishes test results as Cucumber Messages + @@ -11,12 +14,12 @@ - + - ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + ..\..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll diff --git a/Reqnroll.sln b/Reqnroll.sln index b17411911..e0e020419 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,10 +116,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.CompatibilityTests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.CompatibilityTests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Plugins\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{58B078D5-37A2-416B-91DA-E5E6BD510062}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -246,10 +248,14 @@ Global {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.Build.0 = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.Build.0 = Release|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -288,8 +294,9 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} + {5072F73C-8CDD-4B44-B3F8-4212F65C3708} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} + {58B078D5-37A2-416B-91DA-E5E6BD510062} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs new file mode 100644 index 000000000..38463ae2d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -0,0 +1,18 @@ +using Reqnroll.SystemTests; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + public class CucumberCompatibilityTestBase : SystemTestBase + { + + protected void AddCucumberMessagePlugIn() + { + _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs new file mode 100644 index 000000000..d45c31fa5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -0,0 +1,24 @@ +namespace CucumberMessages.CompatibilityTests +{ + [TestClass] + public class CucumberCompatibilityTests : CucumberCompatibilityTestBase + { + [TestMethod] + public void SmokeTest() + { + AddCucumberMessagePlugIn(); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Smoke Test + Given I have a passing step + """); + + AddPassingStepBinding(); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + } +} \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj new file mode 100644 index 000000000..ab72b2ffb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/nuget.config b/nuget.config index 27a5cefad..c8c559d96 100644 --- a/nuget.config +++ b/nuget.config @@ -1,8 +1,9 @@ - + + \ No newline at end of file From 80af263a8b194ff0e57cadee9762ee5c1d993db5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:30:06 -0500 Subject: [PATCH 005/218] Plugin stucture complete; test case runs, but no output. Need to figure out why trace logging also not working. --- .../FileSinkPlugin.cs | 75 +++++++++++++++++-- ...mberMessage.FileSink.ReqnrollPlugin.csproj | 7 +- .../CucumberMessageBroker.cs | 20 ++++- .../CucumberMessageExtensions.cs | 2 - .../CucumberMessagePublisher.cs | 31 ++++++-- .../CucumberMessageSinkBase.cs | 40 ++++++---- .../DefaultDependencyProvider.cs | 5 ++ Reqnroll/Reqnroll.csproj | 7 +- 8 files changed, 141 insertions(+), 46 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index ac9359a90..24c61aff4 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -6,28 +6,58 @@ using Io.Cucumber.Messages.Types; using System.Reflection; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using System.Diagnostics; +using Reqnroll.Events; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - public class FileSinkPlugin : CucumberMessageSinkBase + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - new public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + private Task? fileWritingTask; + + private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - base.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - Task.Run(() => ConsumeAndWriteToFiles()); + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); + testThreadExecutionEventPublisher.AddHandler(CloseFileSink); + }; + } + + private void CloseFileSink(TestRunFinishedEvent @event) + { + fileWritingTask?.Wait(); + fileWritingTask = null; + } + + private void LaunchFileSink(TestRunStartedEvent testRunStarted) + { + Console.WriteLine( "LaunchFileSink called" ); + fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } private async Task ConsumeAndWriteToFiles() { - await foreach (var message in Consume()) + Console.WriteLine( "ConsumeAndWriteToFiles called" ); + + await foreach (var message in sinkBase.Consume()) { var featureName = message.CucumberMessageSource; + + Console.WriteLine( "ConsumeAndWriteToFiles: " + featureName ); if (message.Envelope != null) { - Write(featureName, Serialize(message.Envelope)); + var cm = Serialize(message.Envelope); + Console.WriteLine("ConsumeAndWriteToFiles: " + cm); + Write(featureName, cm); } else { @@ -37,7 +67,8 @@ private async Task ConsumeAndWriteToFiles() } private Dictionary fileStreams = new(); - private string baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "CucumberMessages"); + private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); + private bool disposedValue; private string Serialize(Envelope message) { @@ -45,9 +76,12 @@ private string Serialize(Envelope message) } private void Write(string featureName, string cucumberMessage) { + string appName = Process.GetCurrentProcess().ProcessName; + string appDirectory = Path.Combine(baseDirectory, appName); + if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(appDirectory, $"{featureName}.ndjson")); } fileStreams[featureName].WriteLine(cucumberMessage); } @@ -57,5 +91,30 @@ private void CloseFeatureStream(string featureName) fileStreams[featureName].Close(); fileStreams.Remove(featureName); } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CloseFileSink(new TestRunFinishedEvent()); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public Task Publish(ReqnrollCucumberMessage message) + { + return sinkBase.Publish(message); + } } } diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index afea6d0f2..7dbb46029 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,6 +10,7 @@ + @@ -17,10 +18,4 @@ - - - ..\..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll - - - diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index c771724cb..f98343066 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -1,4 +1,5 @@ using Reqnroll.BoDi; +using Reqnroll.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -19,17 +20,25 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { private List registeredSinks; - + private IObjectContainer _objectContainer; + //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) - { + { + _objectContainer = objectContainer; var sinks = objectContainer.ResolveAll(); registeredSinks = new List(sinks); } public async Task PublishAsync(ReqnrollCucumberMessage message) { + var _traceListener = _objectContainer.Resolve(); + _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); + + foreach (var sink in registeredSinks) - { + { + _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); + await sink.Publish(message); } } @@ -37,6 +46,9 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public async Task CompleteAsync(string cucumberMessageSource) { + var _traceListener = _objectContainer.Resolve(); + _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); + var completionMessage = new ReqnrollCucumberMessage { CucumberMessageSource = cucumberMessageSource @@ -44,6 +56,8 @@ public async Task CompleteAsync(string cucumberMessageSource) foreach (var sink in registeredSinks) { + _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); + await sink.Publish(completionMessage); } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs index de2ae191c..638102fa9 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -8,7 +8,6 @@ namespace Reqnroll.CucumberMesssages { -#if NETSTANDARD2_0 public static class CucumberMessageExtensions { public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, @@ -23,5 +22,4 @@ public static async IAsyncEnumerable ReadAllAsync(this ChannelReader re } } } -#endif } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 063eb906b..833093695 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -6,38 +6,55 @@ using System.Threading.Tasks; using Io.Cucumber.Messages; using Io.Cucumber.Messages.Types; +using Reqnroll.Tracing; namespace Reqnroll.CucumberMesssages { public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; + private IObjectContainer objectContainer; + private bool initialized = false; - public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker) - { + public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) + { + this.objectContainer = objectContainer; broker = CucumberMessageBroker; } public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { + if (initialized) + { + return; + } + initialized = true; + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); + testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { - var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.FolderPath; - Task.Run(() => broker.CompleteAsync(featureName)); + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + broker.CompleteAsync(featureName); } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { - var featureName = featureStartedEvent.FeatureContext.FeatureInfo.FolderPath; - Task.Run(() => broker.PublishAsync(new ReqnrollCucumberMessage + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + + broker.PublishAsync(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) - })); + }); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index 199664475..ea085a694 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -1,5 +1,7 @@ -using Reqnroll.Events; +using Reqnroll.BoDi; +using Reqnroll.Events; using Reqnroll.Plugins; +using Reqnroll.Tracing; using Reqnroll.UnitTestProvider; using System.Collections.Generic; using System.Threading.Channels; @@ -8,11 +10,29 @@ namespace Reqnroll.CucumberMesssages { - public class CucumberMessageSinkBase : ICucumberMessageSink, IRuntimePlugin + public class CucumberMessageSinkBase { + protected IObjectContainer _testThreadContainer; + + private ICucumberMessagePublisher _publisher; private Channel _channel = Channel.CreateUnbounded(); + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + _testThreadContainer = args.ObjectContainer; + _publisher = args.ObjectContainer.Resolve(); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + _publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; + } + public async Task Publish(ReqnrollCucumberMessage message) { + var traceListener = _testThreadContainer.Resolve(); + traceListener.WriteTestOutput($"Cucumber Message Sink publishing message."); + await _channel.Writer.WriteAsync(message); if (message.Envelope == null) { @@ -22,22 +42,14 @@ public async Task Publish(ReqnrollCucumberMessage message) public async IAsyncEnumerable Consume() { + var traceListener = _testThreadContainer.Resolve(); + traceListener.WriteTestOutput($"Cucumber Message Sink Consume() called."); + await foreach (var message in _channel.Reader.ReadAllAsync()) { + // _traceListener.WriteTestOutput($"Cucumber Message Sink consuming message."); yield return message; } } - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - var publisher = args.ObjectContainer.Resolve(); - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); - }; - } - } } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index eb489540e..a4bb84221 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,6 +16,7 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; +using Reqnroll.CucumberMesssages; namespace Reqnroll.Infrastructure { @@ -98,6 +99,10 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + + //Support for publishing Cucumber Messages + container.RegisterTypeAs(); + container.RegisterTypeAs(); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index ed47c9ca2..c9afb5bbe 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,6 +16,7 @@ + @@ -42,12 +43,6 @@ - - - ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll - - - From a686657e78b7987cd696977ebde10e6468325c14 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:15:32 -0500 Subject: [PATCH 006/218] Made pulling of registered sinks to be dynamic; eliminated Publisher check of whether it had been initialized (temp). (Temp) - added Debugger.Launch to FileSinkPlugin to force launch of debugger. --- CHANGELOG.md | 2 +- .../FileSinkPlugin.cs | 6 ++++++ Reqnroll/CucumberMesssages/CucumberMessageBroker.cs | 8 ++++---- .../CucumberMesssages/CucumberMessagePublisher.cs | 12 ++++++------ 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8ef325c..3a3fa0878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# [vNext] +# v2.1.0-local - 2024-08-09 ## Bug fixes: * Modified VersionInfo class to force it to pull version information from the Reqnroll assembly diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 24c61aff4..6946b10a0 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -19,8 +19,14 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + public FileSinkPlugin() + { + Debugger.Launch(); + } + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index f98343066..ab33ec4a1 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Security.Authentication.ExtendedProtection; using System.Text; using System.Threading.Tasks; @@ -19,22 +20,19 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { - private List registeredSinks; private IObjectContainer _objectContainer; //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; - var sinks = objectContainer.ResolveAll(); - registeredSinks = new List(sinks); } public async Task PublishAsync(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); + var registeredSinks = _objectContainer.ResolveAll().ToList(); _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); - foreach (var sink in registeredSinks) { _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); @@ -46,6 +44,8 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public async Task CompleteAsync(string cucumberMessageSource) { + var registeredSinks = _objectContainer.ResolveAll().ToList(); + var _traceListener = _objectContainer.Resolve(); _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 833093695..fa29eab8d 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -14,7 +14,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private bool initialized = false; + //private bool initialized = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -24,11 +24,11 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - if (initialized) - { - return; - } - initialized = true; + //if (initialized) + //{ + // return; + //} + //initialized = true; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); From d85764a66885ef1e9774d49427c391836fc0264c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:38:54 -0500 Subject: [PATCH 007/218] Refactored Publisher into an internally loaded PlugIn. Separated the Publisher from the FileSinkPlugIn completely. --- .../CucumberMessagePublisher.cs | 19 +++++++++++++++---- .../CucumberMessageSinkBase.cs | 4 ---- .../DefaultDependencyProvider.cs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index fa29eab8d..7c3c47f2c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -7,20 +7,30 @@ using Io.Cucumber.Messages; using Io.Cucumber.Messages.Types; using Reqnroll.Tracing; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; namespace Reqnroll.CucumberMesssages { - public class CucumberMessagePublisher : ICucumberMessagePublisher + public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; //private bool initialized = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) - { - this.objectContainer = objectContainer; + { broker = CucumberMessageBroker; } + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => + { + objectContainer = args.ObjectContainer; + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; + } public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { @@ -37,6 +47,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } + private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; @@ -50,7 +61,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - broker.PublishAsync(new ReqnrollCucumberMessage + broker.PublishAsync(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index ea085a694..3e3057a80 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -14,7 +14,6 @@ public class CucumberMessageSinkBase { protected IObjectContainer _testThreadContainer; - private ICucumberMessagePublisher _publisher; private Channel _channel = Channel.CreateUnbounded(); public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -22,9 +21,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { _testThreadContainer = args.ObjectContainer; - _publisher = args.ObjectContainer.Resolve(); - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - _publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); }; } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index a4bb84221..d4f0e2d98 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -102,7 +102,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); - container.RegisterTypeAs(); + container.RegisterTypeAs("CucumberMessagePublisher"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) From 63d23df2bebdac09bfa27bf817c56ee5b2c2acbc Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:33:47 -0500 Subject: [PATCH 008/218] Simplified by eliminating use of Threading.Channels; substituting in the use of a BlockingCollection. Eliminated base class for Sinks. Eliminated use of Async for MessageSinks. Sinks have to ensure their own thread-safety and non-blocking behavior. May want to revisit that. --- .../FileSinkPlugin.cs | 58 ++++++++++++------- .../CucumberMessageBroker.cs | 14 +++-- .../CucumberMessagePublisher.cs | 4 +- .../CucumberMessageSinkBase.cs | 51 ---------------- .../CucumberMesssages/ICucumberMessageSink.cs | 2 +- 5 files changed, 49 insertions(+), 80 deletions(-) delete mode 100644 Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 6946b10a0..19364b8a8 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -8,16 +8,24 @@ using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; using System.Diagnostics; using Reqnroll.Events; +using System.Collections.Concurrent; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { + //TODO: Add support for Reqnroll Configuration to initialize the FileSinkPlugin by specifying the path to the base directory. + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private Task? fileWritingTask; - private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) + + private readonly BlockingCollection postedMessages = new(); + private readonly ConcurrentDictionary fileStreams = new(); public FileSinkPlugin() { @@ -27,7 +35,6 @@ public FileSinkPlugin() public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => @@ -40,30 +47,42 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar private void CloseFileSink(TestRunFinishedEvent @event) { + postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; } private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - Console.WriteLine( "LaunchFileSink called" ); - fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + Console.WriteLine("LaunchFileSink called"); + + if (!Directory.Exists(baseDirectory)) + { + Directory.CreateDirectory(baseDirectory); + } + + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + } + + public void Publish(ReqnrollCucumberMessage message) + { + postedMessages.Add(message); } private async Task ConsumeAndWriteToFiles() { - Console.WriteLine( "ConsumeAndWriteToFiles called" ); + Console.WriteLine("ConsumeAndWriteToFiles called"); - await foreach (var message in sinkBase.Consume()) + foreach (var message in postedMessages.GetConsumingEnumerable()) { var featureName = message.CucumberMessageSource; - Console.WriteLine( "ConsumeAndWriteToFiles: " + featureName ); + Console.WriteLine("ConsumeAndWriteToFiles: " + featureName); if (message.Envelope != null) { var cm = Serialize(message.Envelope); Console.WriteLine("ConsumeAndWriteToFiles: " + cm); - Write(featureName, cm); + await Write(featureName, cm); } else { @@ -72,7 +91,6 @@ private async Task ConsumeAndWriteToFiles() } } - private Dictionary fileStreams = new(); private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); private bool disposedValue; @@ -80,22 +98,20 @@ private string Serialize(Envelope message) { return NdjsonSerializer.Serialize(message); } - private void Write(string featureName, string cucumberMessage) + private async Task Write(string featureName, string cucumberMessage) { - string appName = Process.GetCurrentProcess().ProcessName; - string appDirectory = Path.Combine(baseDirectory, appName); if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(appDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); } - fileStreams[featureName].WriteLine(cucumberMessage); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); } private void CloseFeatureStream(string featureName) { fileStreams[featureName].Close(); - fileStreams.Remove(featureName); + fileStreams.TryRemove(featureName, out var _); } protected virtual void Dispose(bool disposing) @@ -105,6 +121,13 @@ protected virtual void Dispose(bool disposing) if (disposing) { CloseFileSink(new TestRunFinishedEvent()); + postedMessages.Dispose(); + foreach (var stream in fileStreams.Values) + { + stream.Close(); + stream.Dispose(); + }; + fileStreams.Clear(); } disposedValue = true; @@ -117,10 +140,5 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - public Task Publish(ReqnrollCucumberMessage message) - { - return sinkBase.Publish(message); - } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index ab33ec4a1..54411bd83 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -14,8 +14,8 @@ namespace Reqnroll.CucumberMesssages public interface ICucumberMessageBroker { - Task CompleteAsync(string cucumberMessageSource); - Task PublishAsync(ReqnrollCucumberMessage message); + void Complete(string cucumberMessageSource); + void Publish(ReqnrollCucumberMessage message); } public class CucumberMessageBroker : ICucumberMessageBroker @@ -27,9 +27,11 @@ public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; } - public async Task PublishAsync(ReqnrollCucumberMessage message) + public void Publish(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); + + //TODO: find a way to populate this list a single time var registeredSinks = _objectContainer.ResolveAll().ToList(); _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); @@ -37,12 +39,12 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) { _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); - await sink.Publish(message); + sink.Publish(message); } } // using an empty CucumberMessage to indicate completion - public async Task CompleteAsync(string cucumberMessageSource) + public void Complete(string cucumberMessageSource) { var registeredSinks = _objectContainer.ResolveAll().ToList(); @@ -58,7 +60,7 @@ public async Task CompleteAsync(string cucumberMessageSource) { _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); - await sink.Publish(completionMessage); + sink.Publish(completionMessage); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 7c3c47f2c..a70a48afb 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -51,7 +51,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - broker.CompleteAsync(featureName); + broker.Complete(featureName); } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) @@ -61,7 +61,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - broker.PublishAsync(new ReqnrollCucumberMessage + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs deleted file mode 100644 index 3e3057a80..000000000 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Reqnroll.BoDi; -using Reqnroll.Events; -using Reqnroll.Plugins; -using Reqnroll.Tracing; -using Reqnroll.UnitTestProvider; -using System.Collections.Generic; -using System.Threading.Channels; -using System.Threading.Tasks; - - -namespace Reqnroll.CucumberMesssages -{ - public class CucumberMessageSinkBase - { - protected IObjectContainer _testThreadContainer; - - private Channel _channel = Channel.CreateUnbounded(); - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - _testThreadContainer = args.ObjectContainer; - }; - } - - public async Task Publish(ReqnrollCucumberMessage message) - { - var traceListener = _testThreadContainer.Resolve(); - traceListener.WriteTestOutput($"Cucumber Message Sink publishing message."); - - await _channel.Writer.WriteAsync(message); - if (message.Envelope == null) - { - _channel.Writer.Complete(); - } - } - - public async IAsyncEnumerable Consume() - { - var traceListener = _testThreadContainer.Resolve(); - traceListener.WriteTestOutput($"Cucumber Message Sink Consume() called."); - - await foreach (var message in _channel.Reader.ReadAllAsync()) - { - // _traceListener.WriteTestOutput($"Cucumber Message Sink consuming message."); - yield return message; - } - } - } -} diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs index a796bdb24..926e2cf1c 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -5,6 +5,6 @@ namespace Reqnroll.CucumberMesssages { public interface ICucumberMessageSink { - Task Publish(ReqnrollCucumberMessage message); + void Publish(ReqnrollCucumberMessage message); } } From 18923fefc71b44ca33a535799a71125fb83f5dd6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 10 Aug 2024 15:28:32 -0500 Subject: [PATCH 009/218] TestRunStarted and TestRunFinished Cucumber Messages supported. --- .../FileSinkPlugin.cs | 2 +- .../CucumberMessagePublisher.cs | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 19364b8a8..7a6165a15 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -29,7 +29,7 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin public FileSinkPlugin() { - Debugger.Launch(); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index a70a48afb..5ff54ceae 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -9,6 +9,8 @@ using Reqnroll.Tracing; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; +using Reqnroll.Time; +using Cucumber.Messages; namespace Reqnroll.CucumberMesssages { @@ -34,11 +36,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - //if (initialized) - //{ - // return; - //} - //initialized = true; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); @@ -51,6 +48,15 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + + var ts = objectContainer.Resolve().GetNowDateAndTime(); + + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) + }); + broker.Complete(featureName); } @@ -61,10 +67,12 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + var ts = objectContainer.Resolve().GetNowDateAndTime(); + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) + Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); } } From 0842f1c76b59d4cc925d296434e1594adff6abf5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:03:44 -0500 Subject: [PATCH 010/218] Proof of Concept working for the generation and emit of the static Cuke Messages (meta, Source, GherkinDocument, and Pickles). --- .../Generation/UnitTestFeatureGenerator.cs | 46 ++- Reqnroll.Parser/CucumberMessagesConverter.cs | 41 +++ Reqnroll.Parser/ICucumberMessageConverter.cs | 14 + .../CucumberMessageExtensions.cs | 25 -- .../CucumberMessagePublisher.cs | 41 +++ .../CucumberMessageTransformer.cs | 342 ++++++++++++++++++ .../FeatureLevelCucumberMessages.cs | 20 + Reqnroll/FeatureInfo.cs | 6 + Reqnroll/Reqnroll.csproj | 1 - 9 files changed, 509 insertions(+), 27 deletions(-) create mode 100644 Reqnroll.Parser/CucumberMessagesConverter.cs create mode 100644 Reqnroll.Parser/ICucumberMessageConverter.cs delete mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs create mode 100644 Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 404d22d75..9741738ea 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -3,7 +3,9 @@ using System.Globalization; using System.Linq; using System.Reflection; +using Gherkin.CucumberMessages; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; @@ -171,7 +173,7 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Name = GeneratorConstants.TESTCLASS_INITIALIZE_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testClassInitializeMethod); - + _testGeneratorProvider.SetTestClassInitializeMethod(generationContext); //testRunner = TestRunnerManager.GetTestRunnerForAssembly(null, [test_worker_id]); @@ -200,6 +202,8 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio _codeDomHelper.TargetLanguage.ToString()), new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)))); + PersistStaticCucumberMessagesToFeatureInfo(generationContext, testClassInitializeMethod); + //await testRunner.OnFeatureStartAsync(featureInfo); var onFeatureStartExpression = new CodeMethodInvokeExpression( testRunnerField, @@ -211,6 +215,46 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Statements.Add(onFeatureStartExpression); } + private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) + { + var CucumberMessagesInitializeMethod = new CodeMemberMethod(); + CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + CucumberMessagesInitializeMethod.Name = "CucumberMessagesInitializeMethod"; + CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); + generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); + + //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); + var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); + var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(generationContext.Document); + + // Serialize the Cucumber Messages to strings + var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); + var featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); + var featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); + + // Create a FeatureLevelCucumberMessages object and add it to featureInfo + var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), + new CodePrimitiveExpression(featureSourceMessageString), + new CodePrimitiveExpression(featureGherkinDocumentMessageString), + new CodePrimitiveExpression(featurePickleMessagesString)); + CucumberMessagesInitializeMethod.Statements.Add( + new CodeAssignStatement( + new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), + featureLevelCucumberMessagesExpression)); + + // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod + var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( + null, + "CucumberMessagesInitializeMethod", + new CodeVariableReferenceExpression("featureInfo")); + + // Add the CodeMethodInvokeExpression to the testClassInitializeMethod statements + testClassInitializeMethod.Statements.Add(invokeCucumberMessagesInitializeMethod); + + } + private void SetupTestClassCleanupMethod(TestClassGenerationContext generationContext) { var testClassCleanupMethod = generationContext.TestClassCleanupMethod; diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs new file mode 100644 index 000000000..9ccf304c1 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser +{ + public class CucumberMessagesConverter : ICucumberMessagesConverters + { + private IIdGenerator _idGenerator; + + public CucumberMessagesConverter(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) + { + var converter = new AstMessagesConverter(_idGenerator); + var location = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)); + return converter.ConvertGherkinDocumentToEventArgs(gherkinDocument, location); + } + + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) + { + var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + return new Source + { + Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), + Data = sourceText, + MediaType = "text/x.cucumber.gherkin+plain" + }; + } + + public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument) + { + var pickleCompiler = new Gherkin.CucumberMessages.Pickles.PickleCompiler(_idGenerator); + var gd = ConvertToCucumberMessagesGherkinDocument(gherkinDocument); + return pickleCompiler.Compile(gd); + } + } +} diff --git a/Reqnroll.Parser/ICucumberMessageConverter.cs b/Reqnroll.Parser/ICucumberMessageConverter.cs new file mode 100644 index 000000000..3198c0843 --- /dev/null +++ b/Reqnroll.Parser/ICucumberMessageConverter.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser +{ + public interface ICucumberMessagesConverters + { + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument); + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument); + public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument); + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs deleted file mode 100644 index 638102fa9..000000000 --- a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable - -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Channels; - - -namespace Reqnroll.CucumberMesssages -{ - public static class CucumberMessageExtensions - { - public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) - { - while (reader.TryRead(out T? item)) - { - yield return item; - } - } - } - } -} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5ff54ceae..94fc5ab96 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -69,6 +69,47 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var ts = objectContainer.Resolve().GetNowDateAndTime(); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new Meta( + Cucumber.Messages.ProtocolVersion.Version, + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + null)) + }); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(messageSource) + }); + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(gherkinDocument) + }); + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + foreach (var pickle in pickles) + { + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(pickle) + }); + } + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs new file mode 100644 index 000000000..4b95b9be1 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -0,0 +1,342 @@ +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reqnroll.CucumberMesssages +{ + internal class CucumberMessageTransformer + { + internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessages.Types.Source gherkinSource) + { + var result = new Io.Cucumber.Messages.Types.Source + ( + gherkinSource.Uri, + gherkinSource.Data, + gherkinSource.MediaType == "text/x.cucumber.gherkin+plain" ? Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN : Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN + ); + return result; + } + + internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) + { + var result = new Io.Cucumber.Messages.Types.GherkinDocument + ( + gherkinDoc.Uri, + CucumberMessageTransformer.ToFeature(gherkinDoc.Feature), + CucumberMessageTransformer.ToComments(gherkinDoc.Comments) + ); + return result; + } + + private static Io.Cucumber.Messages.Types.Feature ToFeature(Gherkin.CucumberMessages.Types.Feature feature) + { + if (feature == null) + { + return null; + } + + var children = feature.Children.Select(ToFeatureChild).ToList(); + var tags = feature.Tags.Select(ToTag).ToList(); + + return new Io.Cucumber.Messages.Types.Feature( + ToLocation(feature.Location), + tags, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + children) + ; + } + + private static Io.Cucumber.Messages.Types.Location ToLocation(Gherkin.CucumberMessages.Types.Location location) + { + if (location == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Location(location.Line, location.Column); + } + + + private static Io.Cucumber.Messages.Types.Tag ToTag(Gherkin.CucumberMessages.Types.Tag tag) + { + if (tag == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Tag(ToLocation(tag.Location), tag.Name, tag.Id); + } + + private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(Gherkin.CucumberMessages.Types.FeatureChild child) + { + if (child == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.FeatureChild + ( + ToRule(child.Rule), + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static Io.Cucumber.Messages.Types.Scenario ToScenario(Gherkin.CucumberMessages.Types.Scenario scenario) + { + if (scenario == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Scenario + ( + ToLocation(scenario.Location), + scenario.Tags.Select(ToTag).ToList(), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.Select(ToStep).ToList(), + scenario.Examples.Select(ToExamples).ToList(), + scenario.Id + ); + } + + private static Io.Cucumber.Messages.Types.Examples ToExamples(Gherkin.CucumberMessages.Types.Examples examples) + { + if (examples == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Examples( + ToLocation(examples.Location), + examples.Tags.Select(ToTag).ToList(), + examples.Keyword, + examples.Name, + examples.Description, + ToTableRow(examples.TableHeader), + examples.TableBody.Select(ToTableRow).ToList(), + examples.Id + ); + } + private static Io.Cucumber.Messages.Types.TableCell ToTableCell(Gherkin.CucumberMessages.Types.TableCell cell) + { + return new Io.Cucumber.Messages.Types.TableCell( + ToLocation(cell.Location), + cell.Value + ); + } + + private static Io.Cucumber.Messages.Types.TableRow ToTableRow(Gherkin.CucumberMessages.Types.TableRow row) + { + return new Io.Cucumber.Messages.Types.TableRow( + ToLocation(row.Location), + row.Cells.Select(ToTableCell).ToList(), + row.Id + ); + } + private static Io.Cucumber.Messages.Types.Step ToStep(Gherkin.CucumberMessages.Types.Step step) + { + if (step == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Step( + ToLocation(step.Location), + step.Keyword, + ToKeyWordType(step.KeywordType), + step.Text, + step.DocString == null ? null : ToDocString(step.DocString), + step.DataTable == null ? null : ToDataTable(step.DataTable), + step.Id + ); + } + + private static Io.Cucumber.Messages.Types.Background ToBackground(Gherkin.CucumberMessages.Types.Background background) + { + if (background == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Background( + ToLocation(background.Location), + background.Keyword, + background.Name, + background.Description, + background.Steps.Select(ToStep).ToList(), + background.Id + ); + } + + private static Io.Cucumber.Messages.Types.Rule ToRule(Gherkin.CucumberMessages.Types.Rule rule) + { + if (rule == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Rule( + ToLocation(rule.Location), + rule.Tags.Select(ToTag).ToList(), + rule.Keyword, + rule.Name, + rule.Description, + rule.Children.Select(ToRuleChild).ToList(), + rule.Id + ); + } + + private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(Gherkin.CucumberMessages.Types.RuleChild child) + { + return new Io.Cucumber.Messages.Types.RuleChild( + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static List ToComments(IReadOnlyCollection comments) + { + return comments.Select(ToComment).ToList(); + } + + private static Io.Cucumber.Messages.Types.Comment ToComment(Gherkin.CucumberMessages.Types.Comment comment) + { + return new Io.Cucumber.Messages.Types.Comment( + ToLocation(comment.Location), + comment.Text + ); + } + private static Io.Cucumber.Messages.Types.StepKeywordType ToKeyWordType(Gherkin.StepKeywordType keywordType) + { + return keywordType switch + { + //case Gherkin.StepKeywordType.Unspecified: + // return Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN; + Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.StepKeywordType.CONTEXT, + Gherkin.StepKeywordType.Conjunction => Io.Cucumber.Messages.Types.StepKeywordType.CONJUNCTION, + Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.StepKeywordType.ACTION, + Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.StepKeywordType.OUTCOME, + Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN, + _ => throw new ArgumentException($"Invalid keyword type: {keywordType}"), + }; + } + + private static Io.Cucumber.Messages.Types.DocString ToDocString(Gherkin.CucumberMessages.Types.DocString docString) + { + return new Io.Cucumber.Messages.Types.DocString( + ToLocation(docString.Location), + docString.MediaType, + docString.Content, + docString.Delimiter + ); + } + + private static Io.Cucumber.Messages.Types.DataTable ToDataTable(Gherkin.CucumberMessages.Types.DataTable dataTable) + { + return new Io.Cucumber.Messages.Types.DataTable( + ToLocation(dataTable.Location), + dataTable.Rows.Select(ToTableRow).ToList() + ); + } + + internal static List ToPickles(IReadOnlyCollection pickles) + { + return pickles.Select(ToPickle).ToList(); + } + + private static Io.Cucumber.Messages.Types.Pickle ToPickle(Gherkin.CucumberMessages.Types.Pickle pickle) + { + return new Io.Cucumber.Messages.Types.Pickle( + pickle.Id, + pickle.Uri, + pickle.Name, + pickle.Language, + pickle.Steps.Select(ToPickleStep).ToList(), + pickle.Tags.Select(ToPickleTag).ToList(), + pickle.AstNodeIds.ToList() + ); + } + private static Io.Cucumber.Messages.Types.PickleTag ToPickleTag(Gherkin.CucumberMessages.Types.PickleTag pickleTag) + { + return new Io.Cucumber.Messages.Types.PickleTag( + pickleTag.Name, + pickleTag.AstNodeId + ); + } + private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(Gherkin.CucumberMessages.Types.PickleStep pickleStep) + { + return new Io.Cucumber.Messages.Types.PickleStep( + ToPickleStepArgument(pickleStep.Argument), + pickleStep.AstNodeIds.ToList(), + pickleStep.Id, + ToPickleStepType(pickleStep.Type), + pickleStep.Text + ); + } + private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgument(Gherkin.CucumberMessages.Types.PickleStepArgument pickleStepArgument) + { + if (pickleStepArgument == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleStepArgument( + ToPickleDocString(pickleStepArgument.DocString), + ToPickleTable(pickleStepArgument.DataTable) + ); + } + + private static Io.Cucumber.Messages.Types.PickleStepType ToPickleStepType(Gherkin.StepKeywordType pickleStepType) + { + return pickleStepType switch + { + Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.PickleStepType.UNKNOWN, + Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.PickleStepType.ACTION, + Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.PickleStepType.OUTCOME, + Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.PickleStepType.CONTEXT, + _ => throw new ArgumentException($"Invalid pickle step type: {pickleStepType}") + }; + } + private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gherkin.CucumberMessages.Types.PickleDocString pickleDocString) + { + if (pickleDocString == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleDocString( + pickleDocString.Content, + pickleDocString.MediaType + ); + } + + private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(Gherkin.CucumberMessages.Types.PickleTable pickleTable) + { + if (pickleTable == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleTable( + pickleTable.Rows.Select(ToPickleTableRow).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableRow ToPickleTableRow(Gherkin.CucumberMessages.Types.PickleTableRow pickleTableRow) + { + return new Io.Cucumber.Messages.Types.PickleTableRow( + pickleTableRow.Cells.Select(ToPickleTableCell).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableCell ToPickleTableCell(Gherkin.CucumberMessages.Types.PickleTableCell pickleTableCell) + { + return new Io.Cucumber.Messages.Types.PickleTableCell( + pickleTableCell.Value + ); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs new file mode 100644 index 000000000..bdf39518d --- /dev/null +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public class FeatureLevelCucumberMessages + { + public FeatureLevelCucumberMessages(string source, string gkerkinDocument, string pickles) + { + Source = source; + GherkinDocument = gkerkinDocument; + Pickles = pickles; + } + + public string Source { get; set; } + public string GherkinDocument { get; set; } + public string Pickles { get; set; } + } +} diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index cdb62f7b7..dcdbc9cea 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Reqnroll.CucumberMesssages; using Reqnroll.Tracing; namespace Reqnroll @@ -15,6 +16,11 @@ public class FeatureInfo public string Description { get; private set; } public CultureInfo Language { get; private set; } + + // TODO: Add this to the constructor and update all the related tests + // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired + public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } + public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) { diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index c9afb5bbe..583dd298c 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -30,7 +30,6 @@ - From 4fa4441f36068bd6ed681a138f7c5d097a835079 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:57:20 -0500 Subject: [PATCH 011/218] Added CucumberMessageFactory as a draft method of converting Reqnroll information into Messages. First example is transforming StepBinding classes into messages of type StepDefinition. --- .../CucumberMessageFactory.cs | 31 +++++++++++++++++++ .../CucumberMessagePublisher.cs | 17 ++++++++++ .../CucumberCompatibilityTests.cs | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageFactory.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs new file mode 100644 index 000000000..1405a7b32 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -0,0 +1,31 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + internal class CucumberMessageFactory + { + internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) + { + var bindingSourceText = binding.SourceExpression; + var expressionType = binding.ExpressionType; + var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; + var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + + var methodName = binding.Method.Name; + var className = binding.Method.Type.Name; + var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); + var methodDescription = new JavaMethod(className, className, paramTypes); + var sourceRef = SourceReference.Create(methodDescription); + + var result = new StepDefinition + ( + "XX", // TODO: Generate an ID for this + stepDefinitionPattern, + sourceRef + ); + return result; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 94fc5ab96..ba823d572 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -11,6 +11,7 @@ using Reqnroll.UnitTestProvider; using Reqnroll.Time; using Cucumber.Messages; +using Reqnroll.Bindings; namespace Reqnroll.CucumberMesssages { @@ -110,11 +111,27 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) }); } + var bindingRegistry = objectContainer.Resolve(); + if (bindingRegistry.IsValid) + { + foreach (var binding in bindingRegistry.GetStepDefinitions()) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(stepDefinition) + }); + } + } + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); + + // throw new ApplicationException(); } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d45c31fa5..1a362738c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -14,7 +14,7 @@ public void SmokeTest() Given I have a passing step """); - AddPassingStepBinding(); + AddPassingStepBinding("Given"); ExecuteTests(); From 76b6bcae49becd69d43fb05a47966dc85cda867b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:10:35 -0500 Subject: [PATCH 012/218] Fixed a bug in how the cucumber messages were created by the UnitTestGenerator. --- Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs | 6 +++--- Reqnroll.Parser/CucumberMessagesConverter.cs | 5 ++--- Reqnroll.Parser/ICucumberMessageConverter.cs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 9741738ea..52d41c8e9 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -219,7 +219,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte { var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; - CucumberMessagesInitializeMethod.Name = "CucumberMessagesInitializeMethod"; + CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); @@ -227,7 +227,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); - var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); // Serialize the Cucumber Messages to strings var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); @@ -247,7 +247,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( null, - "CucumberMessagesInitializeMethod", + CucumberMessagesInitializeMethod.Name, new CodeVariableReferenceExpression("featureInfo")); // Add the CodeMethodInvokeExpression to the testClassInitializeMethod statements diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 9ccf304c1..ec9e26af7 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -31,11 +31,10 @@ public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) }; } - public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument) + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument) { var pickleCompiler = new Gherkin.CucumberMessages.Pickles.PickleCompiler(_idGenerator); - var gd = ConvertToCucumberMessagesGherkinDocument(gherkinDocument); - return pickleCompiler.Compile(gd); + return pickleCompiler.Compile(gherkinDocument); } } } diff --git a/Reqnroll.Parser/ICucumberMessageConverter.cs b/Reqnroll.Parser/ICucumberMessageConverter.cs index 3198c0843..b08a3773c 100644 --- a/Reqnroll.Parser/ICucumberMessageConverter.cs +++ b/Reqnroll.Parser/ICucumberMessageConverter.cs @@ -9,6 +9,6 @@ public interface ICucumberMessagesConverters { public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument); public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument); - public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument); + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument); } } From e679f048e2122bf1eeacb4910e5ac43bb945f993 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:07:05 -0500 Subject: [PATCH 013/218] Fixed bug that was causing a namespace conflict with the Specs project. --- Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 52d41c8e9..458926d61 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -220,7 +220,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; - CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); + CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo From 8eb0df9001277f7e9c34b58090cbd730de673f87 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:51:53 -0500 Subject: [PATCH 014/218] When Pickles can't be created (such as with ExternalData), suspend support for CucumberMessages for that feature. --- .../Generation/UnitTestFeatureGenerator.cs | 32 +++++++++++++------ .../CucumberMessagePublisher.cs | 17 +++++++++- Reqnroll/CucumberMesssages/FeatureState.cs | 8 +++++ 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/FeatureState.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 458926d61..1e17b09b3 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -217,22 +217,34 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) { + string featureSourceMessageString = null; + string featureGherkinDocumentMessageString = null; + string featurePickleMessagesString = null; + try + { + //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); + var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); + var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); + + // Serialize the Cucumber Messages to strings + featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); + featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); + featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); + } + catch + { + // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. + return; + } + // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); - //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo - var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); - var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); - var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); - var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); - - // Serialize the Cucumber Messages to strings - var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); - var featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); - var featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); // Create a FeatureLevelCucumberMessages object and add it to featureInfo var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index ba823d572..1c971e473 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -19,7 +19,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - //private bool initialized = false; + private Dictionary featureState = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -49,6 +49,9 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + var featureState = this.featureState[featureName]; + if (!featureState.Enabled) + return; var ts = objectContainer.Resolve().GetNowDateAndTime(); @@ -64,10 +67,22 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + var featureState = new FeatureState + { + Name = featureName, + Enabled = enabled + }; + + this.featureState[featureName] = featureState; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + if (!enabled) + return; + var ts = objectContainer.Resolve().GetNowDateAndTime(); broker.Publish(new ReqnrollCucumberMessage diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs new file mode 100644 index 000000000..ebb6d46f5 --- /dev/null +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.CucumberMesssages +{ + internal class FeatureState + { + public string Name { get; set; } + public bool Enabled { get; set; } //This will be false if the feature could not be pickled + } +} \ No newline at end of file From 4321f34d3f1141c4deaf9aa73a2feb568410f949 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:42:54 -0500 Subject: [PATCH 015/218] Messages are cached until the last thread is finished with the Feature, then all are published. --- .../CucumberMessagePublisher.cs | 52 +++++++++++++++---- .../CucumberMessageTransformer.cs | 5 ++ Reqnroll/CucumberMesssages/FeatureState.cs | 17 +++++- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1c971e473..663c63c69 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -12,6 +12,11 @@ using Reqnroll.Time; using Cucumber.Messages; using Reqnroll.Bindings; +using System.Reflection; +using ScenarioNameIDMap = System.Collections.Generic.Dictionary; +using StepPatternIDMap = System.Collections.Generic.Dictionary; +using TestCaseToPickleMap = System.Collections.Generic.Dictionary; +using System.Collections.Concurrent; namespace Reqnroll.CucumberMesssages { @@ -19,7 +24,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private Dictionary featureState = new(); + private ConcurrentDictionary featureStatesByFeatureName = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -49,18 +54,36 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = this.featureState[featureName]; + var featureState = featureStatesByFeatureName[featureName]; + featureState.workerThreadMarkers.TryPop(out int result); + + lock (featureState) + { + if (featureState.workerThreadMarkers.TryPeek(out result)) + { + // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet + return; + } + } + + if (!featureState.Enabled) return; var ts = objectContainer.Resolve().GetNowDateAndTime(); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, + //TODO: add feature status Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) }); + foreach (var message in featureState.Messages) + { + broker.Publish(message); + } + broker.Complete(featureName); } @@ -75,7 +98,15 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Enabled = enabled }; - this.featureState[featureName] = featureState; + if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + { + // This feature has already been started by another thread (executing a different scenario) + var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well + + // None of the rest of this method should be executed + return; + } var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); @@ -85,7 +116,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var ts = objectContainer.Resolve().GetNowDateAndTime(); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new Meta( @@ -99,7 +130,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(messageSource) @@ -108,7 +139,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(gherkinDocument) @@ -119,7 +150,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) foreach (var pickle in pickles) { - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(pickle) @@ -132,7 +163,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(stepDefinition) @@ -140,13 +171,12 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) } } - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); - // throw new ApplicationException(); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index 4b95b9be1..17ccc9d7e 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -9,6 +9,11 @@ namespace Reqnroll.CucumberMesssages { + /// + /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace + /// + /// once the Gherkin project is updated to directly consume and produce Cucumber messages, this class can be removed + /// internal class CucumberMessageTransformer { internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessages.Types.Source gherkinSource) diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index ebb6d46f5..8ae47e19e 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,8 +1,23 @@ -namespace Reqnroll.CucumberMesssages +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMesssages { internal class FeatureState { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled + + // These two flags are used to avoid duplicate messages being sent when Scenarios within Features are run concurrently + // and multiple FeatureStartedEvent and FeatureFinishedEvent events are fired + public bool Started { get; set; } + public bool Finished { get; set; } + + public Dictionary ScenarioNameIDMap = new Dictionary(); // + public Dictionary StepPatternIDMap = new Dictionary(); // + + public ConcurrentQueue Messages = new ConcurrentQueue(); + public ConcurrentStack workerThreadMarkers = new ConcurrentStack(); } } \ No newline at end of file From 514b558eb5cf5e2266b6424bd4d2d610b5854f2b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 18 Aug 2024 10:31:29 -0500 Subject: [PATCH 016/218] Slight change to handling of FeatureFiinished event for thread safety. --- Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 663c63c69..419530f1f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -55,10 +55,13 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - featureState.workerThreadMarkers.TryPop(out int result); lock (featureState) { + // Remove the worker thread marker for this thread + featureState.workerThreadMarkers.TryPop(out int result); + + // Check if there are other threads still working on this feature if (featureState.workerThreadMarkers.TryPeek(out result)) { // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet From 3180353b1669b4988ebf67ea612afe7748a2f17e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:08:53 -0500 Subject: [PATCH 017/218] Initial setup of ScenarioStart and Finish. --- .../CucumberMessageFactory.cs | 7 ++- .../CucumberMessagePublisher.cs | 63 ++++++++++++++++--- Reqnroll/CucumberMesssages/FeatureState.cs | 20 ++++-- .../CucumberMesssages/IdGeneratorFactory.cs | 43 +++++++++++++ Reqnroll/CucumberMesssages/ScenarioState.cs | 43 +++++++++++++ 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/IdGeneratorFactory.cs create mode 100644 Reqnroll/CucumberMesssages/ScenarioState.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 1405a7b32..4886df10c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,4 +1,5 @@ -using Io.Cucumber.Messages.Types; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; using System.Linq; @@ -6,7 +7,7 @@ namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { - internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) + internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { var bindingSourceText = binding.SourceExpression; var expressionType = binding.ExpressionType; @@ -21,7 +22,7 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) var result = new StepDefinition ( - "XX", // TODO: Generate an ID for this + idGenerator.GetNewId(), stepDefinitionPattern, sourceRef ); diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 419530f1f..4fdce34e2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -13,10 +13,8 @@ using Cucumber.Messages; using Reqnroll.Bindings; using System.Reflection; -using ScenarioNameIDMap = System.Collections.Generic.Dictionary; -using StepPatternIDMap = System.Collections.Generic.Dictionary; -using TestCaseToPickleMap = System.Collections.Generic.Dictionary; using System.Collections.Concurrent; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -48,9 +46,10 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); + testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); + testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); } - private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; @@ -117,8 +116,6 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - var ts = objectContainer.Resolve().GetNowDateAndTime(); - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -151,8 +148,13 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + string lastID = ExtractID(pickles); + featureState.IDGenerator = IdGeneratorFactory.Create(lastID); + foreach (var pickle in pickles) { + featureState.PicklesByScenarioName.Add(pickle.Name, pickle); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -165,7 +167,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { foreach (var binding in bindingRegistry.GetStepDefinitions()) { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, featureState.IDGenerator); + var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + featureState.StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -177,9 +182,51 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) + Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp))) }); } + + private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + { + var sr = stepDefinition.SourceReference; + var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + return $"{stepDefinition.Pattern}({signature})"; + } + + private string ExtractID(List pickles) + { + return pickles.Last().Id; + } + + private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + { + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; + var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; + + var featureState = featureStatesByFeatureName[featureName]; + + var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, featureState); + featureState.ScenarioName2StateMap.Add(scenarioName, scenarioState); + + foreach (Envelope e in scenarioState.ProcessEvent(scenarioStartedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } + } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + { + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; + var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; + var scenarioState = featureState.ScenarioName2StateMap[scenarioName]; + foreach (Envelope e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } + } + + } } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 8ae47e19e..b0ab44439 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,4 +1,6 @@ -using System.Collections; +using Gherkin.CucumberMessages; +using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -14,10 +16,18 @@ internal class FeatureState public bool Started { get; set; } public bool Finished { get; set; } - public Dictionary ScenarioNameIDMap = new Dictionary(); // - public Dictionary StepPatternIDMap = new Dictionary(); // + //ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator + public IIdGenerator IDGenerator { get; set; } - public ConcurrentQueue Messages = new ConcurrentQueue(); - public ConcurrentStack workerThreadMarkers = new ConcurrentStack(); + //Lookup tables + public Dictionary StepDefinitionsByPattern = new(); + public Dictionary PicklesByScenarioName = new(); + + public Dictionary ScenarioName2StateMap = new(); + + public ConcurrentQueue Messages = new(); + public ConcurrentStack workerThreadMarkers = new(); } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs new file mode 100644 index 000000000..8ff9bc652 --- /dev/null +++ b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs @@ -0,0 +1,43 @@ +using Gherkin.CucumberMessages; +using System; +using System.Threading; +namespace Reqnroll.CucumberMesssages +{ + public class IdGeneratorFactory + { + public static IIdGenerator Create(string previousId) + { + if (Guid.TryParse(previousId, out var _)) + { + return new GuidIdGenerator(); + } + else + { + return new SeedableIncrementingIdGenerator(int.Parse(previousId)); + } + } + } + + public class SeedableIncrementingIdGenerator : IIdGenerator + { + public SeedableIncrementingIdGenerator(int seed) + { + _counter = seed; + } + + private int _counter = 0; + + public string GetNewId() + { + // Using thread-safe incrementing in case scenarios are running in parallel + var nextId = Interlocked.Increment(ref _counter); + return nextId.ToString(); + } + + + public void Reset() + { + _counter = 0; + } + } +} diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs new file mode 100644 index 000000000..f097d3905 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -0,0 +1,43 @@ +using Cucumber.Messages; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMesssages +{ + internal class ScenarioState + { + private readonly IIdGenerator _idGenerator; + private string TestCaseStartedID; + + public string Name { get; set; } + public string TestCaseID { get; set; } + public string PickleID { get; set; } + + public ScenarioState(IScenarioContext context, FeatureState featureState) + { + _idGenerator = featureState.IDGenerator; + + Name = context.ScenarioInfo.Title; + TestCaseID = _idGenerator.GetNewId(); + PickleID = featureState.PicklesByScenarioName[Name].Id; + } + + internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + TestCaseStartedID = _idGenerator.GetNewId(); + + //TODO: move Message creation to the CucumberMessageFactory + yield return Envelope.Create(new TestCase(TestCaseID, PickleID, new List())); + yield return Envelope.Create(new TestCaseStarted(0, TestCaseStartedID, TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp))); + } + + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + //TODO: move Message creation to the CucumberMessageFactory + + yield return Envelope.Create(new TestCaseFinished(TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false)); + } + } +} \ No newline at end of file From 351efa1312f380a9000260687d519a99469092fb Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:17:57 -0500 Subject: [PATCH 018/218] Refactored Message creation to Factory class. FeatureState and ScenarioState emit sequences of Envelopes for consumption by the Publisher. --- .../CucumberMessageFactory.cs | 28 ++++++- .../CucumberMessagePublisher.cs | 82 +------------------ Reqnroll/CucumberMesssages/FeatureState.cs | 72 +++++++++++++++- Reqnroll/CucumberMesssages/ScenarioState.cs | 11 +-- 4 files changed, 104 insertions(+), 89 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 4886df10c..e19e1646c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,12 +1,38 @@ -using Gherkin.CucumberMessages; +using Cucumber.Messages; +using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Generic; using System.Linq; namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { + public static TestRunStarted ToTestRunStarted(FeatureState featureState, FeatureStartedEvent featureStartedEvent) + { + return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); + } + internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + { + var testCase = new TestCase + ( + scenarioState.TestCaseID, + scenarioState.PickleID, + new List() + ); + return testCase; + } + internal static TestCaseStarted ToTestCaseStarted(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + { + return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); + } + internal static TestCaseFinished ToTestCaseFinished(ScenarioState scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + { + return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); + } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { var bindingSourceText = binding.SourceExpression; diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 4fdce34e2..5b0c0bd8b 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -116,88 +116,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(new Meta( - Cucumber.Messages.ProtocolVersion.Version, - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - null)) - }); - - Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(messageSource) - }); - - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(gherkinDocument) - }); - - var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); - var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); - - string lastID = ExtractID(pickles); - featureState.IDGenerator = IdGeneratorFactory.Create(lastID); - - foreach (var pickle in pickles) + foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) { - featureState.PicklesByScenarioName.Add(pickle.Name, pickle); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(pickle) - }); - } - - var bindingRegistry = objectContainer.Resolve(); - if (bindingRegistry.IsValid) - { - foreach (var binding in bindingRegistry.GetStepDefinitions()) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, featureState.IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); - featureState.StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(stepDefinition) - }); - } + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp))) - }); - - } - - private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) - { - var sr = stepDefinition.SourceReference; - var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; - - return $"{stepDefinition.Pattern}({signature})"; - } - - private string ExtractID(List pickles) - { - return pickles.Last().Id; } private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index b0ab44439..643d1db30 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,8 +1,14 @@ -using Gherkin.CucumberMessages; +using Cucumber.Messages; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.BoDi; +using Reqnroll.Events; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -29,5 +35,69 @@ internal class FeatureState public ConcurrentQueue Messages = new(); public ConcurrentStack workerThreadMarkers = new(); + + internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) + { + yield return Envelope.Create(new Meta( + Cucumber.Messages.ProtocolVersion.Version, + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + null)); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + yield return Envelope.Create(messageSource); + + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + yield return Envelope.Create(gherkinDocument); + + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + string lastID = ExtractLastID(pickles); + IDGenerator = IdGeneratorFactory.Create(lastID); + + foreach (var pickle in pickles) + { + PicklesByScenarioName.Add(pickle.Name, pickle); + yield return Envelope.Create(pickle); + } + + var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + if (bindingRegistry.IsValid) + { + foreach (var binding in bindingRegistry.GetStepDefinitions()) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + + yield return Envelope.Create(stepDefinition); + } + } + + yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); + + } + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + + private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + { + var sr = stepDefinition.SourceReference; + var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + return $"{stepDefinition.Pattern}({signature})"; + } + + + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index f097d3905..cd8ffd9da 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -9,8 +9,8 @@ namespace Reqnroll.CucumberMesssages internal class ScenarioState { private readonly IIdGenerator _idGenerator; - private string TestCaseStartedID; + public string TestCaseStartedID; public string Name { get; set; } public string TestCaseID { get; set; } public string PickleID { get; set; } @@ -28,16 +28,13 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted { TestCaseStartedID = _idGenerator.GetNewId(); - //TODO: move Message creation to the CucumberMessageFactory - yield return Envelope.Create(new TestCase(TestCaseID, PickleID, new List())); - yield return Envelope.Create(new TestCaseStarted(0, TestCaseStartedID, TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp))); + yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { - //TODO: move Message creation to the CucumberMessageFactory - - yield return Envelope.Create(new TestCaseFinished(TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); } } } \ No newline at end of file From e7f7e089902a9468c8edcffa58925c2522917ada Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:15:15 -0500 Subject: [PATCH 019/218] Refactor: deferred processing of all Scenario and step events until after scenario completion so that we have full binding Match info available to create the TestCase message. --- .../CucumberMessageFactory.cs | 2 +- Reqnroll/CucumberMesssages/FeatureState.cs | 18 ++++--- Reqnroll/CucumberMesssages/ScenarioState.cs | 50 +++++++++++++++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index e19e1646c..28d61ebac 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -21,7 +21,7 @@ internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStarted ( scenarioState.TestCaseID, scenarioState.PickleID, - new List() + new List () ); return testCase; } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 643d1db30..9c82f3d91 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -74,7 +74,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + var pattern = CanonicalizeStepDefinitionPattern(binding); StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); @@ -89,15 +89,21 @@ private string ExtractLastID(List pickles) return pickles.Last().Id; } - private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + //private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + //{ + // var sr = stepDefinition.SourceReference; + // var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + // return $"{stepDefinition.Pattern}({signature})"; + //} + public string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { - var sr = stepDefinition.SourceReference; - var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; - return $"{stepDefinition.Pattern}({signature})"; + return $"{stepDefinition.SourceExpression}({signature})"; } - } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index cd8ffd9da..72bb781c3 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -1,8 +1,11 @@ using Cucumber.Messages; using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; using Reqnroll.Events; +using System; using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -14,6 +17,13 @@ internal class ScenarioState public string Name { get; set; } public string TestCaseID { get; set; } public string PickleID { get; set; } + public Pickle Pickle { get; set; } + + // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages + private Queue _events = new(); + + // this holds the step definitions bindings that were executed by each step in this scenario. this will be used to find the Cucumber stepDefinitions that were used + private Queue _stepBindingsAsUsed = new(); public ScenarioState(IScenarioContext context, FeatureState featureState) { @@ -21,20 +31,54 @@ public ScenarioState(IScenarioContext context, FeatureState featureState) Name = context.ScenarioInfo.Title; TestCaseID = _idGenerator.GetNewId(); + Pickle = featureState.PicklesByScenarioName[Name]; PickleID = featureState.PicklesByScenarioName[Name].Id; } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { TestCaseStartedID = _idGenerator.GetNewId(); + _events.Enqueue(scenarioStartedEvent); + return Enumerable.Empty(); + } - yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + { + _events.Enqueue(stepStartedEvent); + return Enumerable.Empty(); + } + + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + _events.Enqueue(stepFinishedEvent); + _stepBindingsAsUsed.Enqueue(stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding); + + return Enumerable.Empty(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); + _events.Enqueue(scenarioFinishedEvent); + + while (_events.Count > 0) + { + var executionEvent = _events.Dequeue(); + + switch (executionEvent) + { + case ScenarioStartedEvent scenarioStartedEvent: + yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + break; + case ScenarioFinishedEvent scenarioFinished: + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); + break; + // add more cases for other event types + default: + throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); + } + } + } } } \ No newline at end of file From ff358d0d7f3511e4ae7c5295b74fd18dc283a96a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:32:23 -0500 Subject: [PATCH 020/218] Completed structure of event processors and state tracker objects for simple scenarios. --- .../CucumberMessageFactory.cs | 95 +++++++++++++++- .../CucumberMessagePublisher.cs | 103 ++++++++++-------- Reqnroll/CucumberMesssages/FeatureState.cs | 69 +++++++++--- Reqnroll/CucumberMesssages/ScenarioState.cs | 41 +++++-- Reqnroll/CucumberMesssages/StepState.cs | 84 ++++++++++++++ .../RuntimeBindingRegistryBuilderTests.cs | 2 +- 6 files changed, 325 insertions(+), 69 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/StepState.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 28d61ebac..c66e44155 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -15,13 +15,25 @@ public static TestRunStarted ToTestRunStarted(FeatureState featureState, Feature { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } + + public static TestRunFinished ToTestRunFinished(FeatureState featureState, FeatureFinishedEvent featureFinishedEvent) + { + return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); + } internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) { + var testSteps = new List(); + + foreach (var stepState in scenarioState.Steps) + { + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState); + testSteps.Add(testStep); + } var testCase = new TestCase ( scenarioState.TestCaseID, scenarioState.PickleID, - new List () + testSteps ); return testCase; } @@ -54,5 +66,86 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, ); return result; } + + internal static TestStep ToTestStep(ScenarioState scenarioState, StepState stepState) + { + //TODO: This only works if the step is properly bound. Need to determine what to do otherwise + + var args = stepState.StepArguments + .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) + .ToList(); + + var result = new TestStep( + null, + stepState.TestStepID, + stepState.PickleStepID, + new List { stepState.StepDefinitionId }, + new List { new StepMatchArgumentsList(args) } + ); + + return result; + } + + internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) + { + return new StepMatchArgument( + new Group( + null, + null, + argument.Value + ), + argument.Type); + } + internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStartedEvent stepStartedEvent) + { + return new TestStepStarted( + stepState.TestCaseStartedID, + stepState.TestStepID, + Converters.ToTimestamp(stepStartedEvent.Timestamp)); + } + + internal static TestStepFinished ToTestStepFinished(StepState stepState, StepFinishedEvent stepFinishedEvent) + { + return new TestStepFinished( + stepState.TestCaseStartedID, + stepState.TestStepID, + ToTestStepResult(stepState, stepFinishedEvent), + Converters.ToTimestamp(stepFinishedEvent.Timestamp)); + } + + private static TestStepResult ToTestStepResult(StepState stepState, StepFinishedEvent stepFinishedEvent) + { + return new TestStepResult( + Converters.ToDuration(stepState.Duration), + "", + ToTestStepResultStatus(stepState.Status), + null); + + } + + private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStatus status) + { + return status switch + { + ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, + ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, + ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, + ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, + ScenarioExecutionStatus.StepDefinitionPending => TestStepResultStatus.PENDING, + _ => TestStepResultStatus.UNKNOWN + }; + } + + + // utility methods + public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + { + + var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + + return $"{stepDefinition.SourceExpression}({signature})"; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5b0c0bd8b..5f415cabf 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -48,13 +48,47 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); + testThreadEventPublisher.AddHandler(StepStartedEventHandler); + testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + { + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + var featureState = new FeatureState + { + Name = featureName, + Enabled = enabled + }; + + if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + { + // This feature has already been started by another thread (executing a different scenario) + var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well + + // None of the rest of this method should be executed + return; + } + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + + if (!enabled) + return; + + foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - + lock (featureState) { // Remove the worker thread marker for this thread @@ -66,20 +100,17 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet return; } + featureState.Finished = true; } if (!featureState.Enabled) return; - var ts = objectContainer.Resolve().GetNowDateAndTime(); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage + foreach (Envelope e in featureState.ProcessEvent(featureFinishedEvent)) { - CucumberMessageSource = featureName, - //TODO: add feature status - Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) - }); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } foreach (var message in featureState.Messages) { @@ -89,61 +120,41 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve broker.Complete(featureName); } - private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { - var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - - var featureState = new FeatureState - { - Name = featureName, - Enabled = enabled - }; + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; - if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + foreach (Envelope e in featureState.ProcessEvent(scenarioStartedEvent)) { - // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; - featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well - - // None of the rest of this method should be executed - return; + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } - - var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - - if (!enabled) - return; - - foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) + } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + { + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; + foreach (Envelope e in featureState.ProcessEvent(scenarioFinishedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } } - private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { - var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - + var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - - var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, featureState); - featureState.ScenarioName2StateMap.Add(scenarioName, scenarioState); - - foreach (Envelope e in scenarioState.ProcessEvent(scenarioStartedEvent)) + foreach (Envelope e in featureState.ProcessEvent(stepStartedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } } - private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + + private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { - var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - var scenarioState = featureState.ScenarioName2StateMap[scenarioName]; - foreach (Envelope e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + foreach (Envelope e in featureState.ProcessEvent(stepFinishedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 9c82f3d91..7357d780a 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -12,7 +12,7 @@ namespace Reqnroll.CucumberMesssages { - internal class FeatureState + public class FeatureState { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled @@ -22,6 +22,14 @@ internal class FeatureState public bool Started { get; set; } public bool Finished { get; set; } + public bool Success + { + get + { + return Enabled && Finished && ScenarioName2StateMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK) ; + } + } + //ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator @@ -31,7 +39,7 @@ internal class FeatureState public Dictionary StepDefinitionsByPattern = new(); public Dictionary PicklesByScenarioName = new(); - public Dictionary ScenarioName2StateMap = new(); + public Dictionary ScenarioName2StateMap = new(); public ConcurrentQueue Messages = new(); public ConcurrentStack workerThreadMarkers = new(); @@ -74,7 +82,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(binding); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); @@ -84,26 +92,61 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); } + + + internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); + } + + internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, this); + ScenarioName2StateMap.Add(scenarioName, scenarioState); + + foreach (var e in scenarioState.ProcessEvent(scenarioStartedEvent)) + { + yield return e; + } + } + private string ExtractLastID(List pickles) { return pickles.Last().Id; } - //private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) - //{ - // var sr = stepDefinition.SourceReference; - // var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; + + foreach (var e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + { + yield return e; + } + } - // return $"{stepDefinition.Pattern}({signature})"; - //} - public string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { - - var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; - return $"{stepDefinition.SourceExpression}({signature})"; + foreach (var e in scenarioState.ProcessEvent(stepStartedEvent)) + { + yield return e; + } } + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; + foreach (var e in scenarioState.ProcessEvent(stepFinishedEvent)) + { + yield return e; + } + } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index 72bb781c3..bd0189f26 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -9,9 +9,10 @@ namespace Reqnroll.CucumberMesssages { - internal class ScenarioState + public class ScenarioState { - private readonly IIdGenerator _idGenerator; + internal readonly IIdGenerator IdGenerator; + internal readonly FeatureState FeatureState; public string TestCaseStartedID; public string Name { get; set; } @@ -22,22 +23,30 @@ internal class ScenarioState // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); - // this holds the step definitions bindings that were executed by each step in this scenario. this will be used to find the Cucumber stepDefinitions that were used - private Queue _stepBindingsAsUsed = new(); + public Dictionary StepsByEvent { get; set; } = new(); + public List Steps + { + get + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Select(kvp => kvp.Value).ToList(); + } + } + public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } public ScenarioState(IScenarioContext context, FeatureState featureState) { - _idGenerator = featureState.IDGenerator; + IdGenerator = featureState.IDGenerator; + FeatureState = featureState; Name = context.ScenarioInfo.Title; - TestCaseID = _idGenerator.GetNewId(); + TestCaseID = IdGenerator.GetNewId(); Pickle = featureState.PicklesByScenarioName[Name]; PickleID = featureState.PicklesByScenarioName[Name].Id; } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - TestCaseStartedID = _idGenerator.GetNewId(); + TestCaseStartedID = IdGenerator.GetNewId(); _events.Enqueue(scenarioStartedEvent); return Enumerable.Empty(); } @@ -45,13 +54,20 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); + + var stepState = new StepState(this, stepStartedEvent); + StepsByEvent.Add(stepStartedEvent, stepState); + stepState.ProcessEvent(stepStartedEvent); + return Enumerable.Empty(); } internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { _events.Enqueue(stepFinishedEvent); - _stepBindingsAsUsed.Enqueue(stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding); + var stepState = StepsByEvent.Values.Last(); + stepState.ProcessEvent(stepFinishedEvent); + StepsByEvent.Add(stepFinishedEvent, stepState); return Enumerable.Empty(); } @@ -71,8 +87,17 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); break; case ScenarioFinishedEvent scenarioFinished: + ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); break; + case StepStartedEvent stepStartedEvent: + var stepState = StepsByEvent[stepStartedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState, stepStartedEvent)); + break; + case StepFinishedEvent stepFinishedEvent: + var stepFinishedState = StepsByEvent[stepFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState, stepFinishedEvent)); + break; // add more cases for other event types default: throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); diff --git a/Reqnroll/CucumberMesssages/StepState.cs b/Reqnroll/CucumberMesssages/StepState.cs new file mode 100644 index 000000000..96a5360da --- /dev/null +++ b/Reqnroll/CucumberMesssages/StepState.cs @@ -0,0 +1,84 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Assist; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + public class StepArgument + { + public string Value; + public string Type; + } + + public class StepState + { + private ScenarioState scenarioState; + private StepStartedEvent stepStartedEvent; + + public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStartedEvent) + { + scenarioState = parentScenarioState; + this.stepStartedEvent = stepStartedEvent; + + } + + public string TestStepID { get; set; } + public string TestCaseStartedID => scenarioState.TestCaseStartedID; + public string PickleStepID { get; set; } + public bool Bound { get; set; } + public string CanonicalizedStepPattern { get; set; } + public string StepDefinitionId { get; private set; } + public IStepDefinitionBinding StepDefinition { get; set; } + + public StepArgument[] StepArguments { get; set; } + public TimeSpan Duration { get; set; } + public ScenarioExecutionStatus Status { get; set; } + + + + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + { + TestStepID = scenarioState.IdGenerator.GetNewId(); + return Enumerable.Empty(); + } + + private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) + { + return scenarioState.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + } + + private string FindPickleStepIDByStepText(string stepText) + { + return scenarioState.FeatureState.PicklesByScenarioName[scenarioState.Name].Steps.Where(st => st.Text == stepText).First().Id; + } + + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + StepDefinition = stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding; + Bound = !(StepDefinition == null || StepDefinition == BindingMatch.NonMatching); + + if (Bound) + { + CanonicalizedStepPattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition); + StepDefinitionId = FindStepDefIDByStepPattern(CanonicalizedStepPattern); + + PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); + + Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; + Status = stepFinishedEvent.StepContext.Status; + + StepArguments = stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument + { + Value = arg.ToString(), + Type = arg.GetType().Name + }).ToArray(); + } + return Enumerable.Empty(); + } + + } +} \ No newline at end of file diff --git a/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 0135b7038..33492bece 100644 --- a/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -42,7 +42,7 @@ public int TransformWithOrderAndWithoutRegex(string val) private BindingSourceProcessorStub bindingSourceProcessorStub; - /* Steps that are feature scoped */ + /* StepsByEvent that are feature scoped */ [Binding] public class ScopedStepTransformationExample From 380bff5fdbb2a08436572076243a4d5780bfda95 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:33:56 -0500 Subject: [PATCH 021/218] Basic structure in place. minimal and pending scenarios added. --- .../CucumberMessageFactory.cs | 2 +- .../CCK/minimal/minimal.cs | 19 +++++++++++ .../CCK/minimal/minimal.feature | 10 ++++++ .../CCK/minimal/minimal.feature.ndjson | 12 +++++++ .../CCK/pending/pending.cs | 31 +++++++++++++++++ .../CCK/pending/pending.feature | 18 ++++++++++ .../CCK/pending/pending.feature.ndjson | 30 ++++++++++++++++ .../CucumberCompatibilityTestBase.cs | 16 ++++++++- .../CucumberCompatibilityTests.cs | 34 ++++++++++++++++++- ...CucumberMessages.CompatibilityTests.csproj | 26 ++++++++++++++ .../Drivers/TestFileManager.cs | 10 +++--- Tests/Reqnroll.SystemTests/SystemTestBase.cs | 5 +-- 12 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index c66e44155..8974ba766 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -90,7 +90,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) { return new StepMatchArgument( new Group( - null, + new List(), null, argument.Value ), diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs new file mode 100644 index 000000000..2207c6ede --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.Minimal +{ + [Binding] + public class Minimal + { + [Given("I have {int} cukes in my belly")] + public void GivenIHaveCukesInMyBelly(int p0) + { + // pass + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature new file mode 100644 index 000000000..5f1e5f688 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature @@ -0,0 +1,10 @@ +Feature: minimal + + Cucumber doesn't execute this markdown, but @cucumber/react renders it + + * This is + * a bullet + * list + + Scenario: cukes + Given I have 42 cukes in my belly diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson new file mode 100644 index 000000000..d10bc4bb3 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/minimal/minimal.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":9},"name":"cukes","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"I have 42 cukes in my belly"}],"tags":[]}}],"description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"minimal","tags":[]},"uri":"samples/minimal/minimal.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cukes","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 cukes in my belly","type":"Context"}],"tags":[],"uri":"samples/minimal/minimal.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"I have {int} cukes in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/minimal/minimal.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs new file mode 100644 index 000000000..6d2fb8755 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs @@ -0,0 +1,31 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.pending +{ + [Binding] + internal class Pending + { + [Given("an unimplemented pending step")] + public void GivenAnUnimplementedPendingStep() + { + throw new PendingStepException(); + } + + [Given("an implemented non-pending step")] + public void GivenAnImplementedNonPendingStep() + { + //nop + } + + [Given("an implemented step that is skipped")] + public void GivenAnImplementedStepThatIsSkipped() + { + throw new ApplicationException("This step should not have been executed"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature new file mode 100644 index 000000000..767ece531 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature @@ -0,0 +1,18 @@ +Feature: Pending steps + During development, step definitions can signal at runtime that they are + not yet implemented (or "pending") by returning or throwing a particular + value. + + This causes subsequent steps in the scenario to be skipped, and the overall + result to be treated as a failure. + + Scenario: Unimplemented step signals pending status + Given an unimplemented pending step + + Scenario: Steps before unimplemented steps are executed + Given an implemented non-pending step + And an unimplemented pending step + + Scenario: Steps after unimplemented steps are skipped + Given an unimplemented pending step + And an implemented step that is skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson new file mode 100644 index 000000000..b8a2c03b4 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson @@ -0,0 +1,30 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Pending steps\n During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.\n\n Scenario: Unimplemented step signals pending status\n Given an unimplemented pending step\n\n Scenario: Steps before unimplemented steps are executed\n Given an implemented non-pending step\n And an unimplemented pending step\n\n Scenario: Steps after unimplemented steps are skipped\n Given an unimplemented pending step\n And an implemented step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/pending/pending.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"4","keyword":"Scenario","location":{"column":3,"line":9},"name":"Unimplemented step signals pending status","steps":[{"id":"3","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"7","keyword":"Scenario","location":{"column":3,"line":12},"name":"Steps before unimplemented steps are executed","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":13},"text":"an implemented non-pending step"},{"id":"6","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":14},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":16},"name":"Steps after unimplemented steps are skipped","steps":[{"id":"8","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":17},"text":"an unimplemented pending step"},{"id":"9","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":18},"text":"an implemented step that is skipped"}],"tags":[]}}],"description":" During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Pending steps","tags":[]},"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["4"],"id":"12","language":"en","name":"Unimplemented step signals pending status","steps":[{"astNodeIds":["3"],"id":"11","text":"an unimplemented pending step","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["7"],"id":"15","language":"en","name":"Steps before unimplemented steps are executed","steps":[{"astNodeIds":["5"],"id":"13","text":"an implemented non-pending step","type":"Context"},{"astNodeIds":["6"],"id":"14","text":"an unimplemented pending step","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"18","language":"en","name":"Steps after unimplemented steps are skipped","steps":[{"astNodeIds":["8"],"id":"16","text":"an unimplemented pending step","type":"Context"},{"astNodeIds":["9"],"id":"17","text":"an implemented step that is skipped","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"an implemented non-pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/pending/pending.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"an implemented step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/pending/pending.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"an unimplemented pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/pending/pending.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"20","pickleId":"12","testSteps":[{"id":"19","pickleStepId":"11","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"23","pickleId":"15","testSteps":[{"id":"21","pickleStepId":"13","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"22","pickleStepId":"14","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"26","pickleId":"18","testSteps":[{"id":"24","pickleStepId":"16","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"17","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"20","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"19","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"19","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"23","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"22","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"22","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"29","testCaseId":"26","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"25","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"25","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 38463ae2d..419805f39 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,7 +1,9 @@ -using Reqnroll.SystemTests; +using FluentAssertions; +using Reqnroll.SystemTests; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -14,5 +16,17 @@ protected void AddCucumberMessagePlugIn() { _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); } + + protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) + { + var bindingCLassFileContent = _testFileManager.GetTestFileContent(fileName, prefix, assemblyToLoadFrom); + AddBindingClass(bindingCLassFileContent); + } + + protected void ShouldAllScenariosPend(int? expectedNrOfTestsSpec = null) + { + int expectedNrOfTests = ConfirmAllTestsRan(expectedNrOfTestsSpec); + _vsTestExecutionDriver.LastTestExecutionResult.Pending.Should().Be(expectedNrOfTests, "all tests should pend"); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 1a362738c..7bc6963ae 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,7 +1,10 @@ + +using System.Reflection; + namespace CucumberMessages.CompatibilityTests { [TestClass] - public class CucumberCompatibilityTests : CucumberCompatibilityTestBase + public class CucumberCompatibilitySmokeTest : CucumberCompatibilityTestBase { [TestMethod] public void SmokeTest() @@ -20,5 +23,34 @@ Given I have a passing step ShouldAllScenariosPass(); } + + [TestMethod] + [DataRow("minimal")] + public void CCKPassingScenarios(string scenarioName) + { + AddCucumberMessagePlugIn(); + + AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] + [DataRow("pending")] + public void CCKPendingScenarios(string scenarioName) + { + AddCucumberMessagePlugIn(); + + AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + + ExecuteTests(); + + ShouldAllScenariosPend(); + } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index ab72b2ffb..4464aac06 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -9,6 +9,27 @@ true + + + + + + + + + + + + + + + + + + + + + @@ -17,6 +38,7 @@ + @@ -24,4 +46,8 @@ + + + + diff --git a/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs b/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs index 95cb6149b..9a0bd6f1c 100644 --- a/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs +++ b/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs @@ -14,12 +14,14 @@ public class TestFileManager private const string TestFileFolder = "Resources"; private readonly string _prefix = $"{RootNamespace}.{TestFileFolder}"; - public string GetTestFileContent(string testFileName) + public string GetTestFileContent(string testFileName, string? prefixOverride = null, Assembly? assemblyToLoadFrom = null) { + + var prefix = prefixOverride ?? _prefix; + var assembly = assemblyToLoadFrom ?? Assembly.GetExecutingAssembly(); var testFileResourceName = testFileName.Replace('/', '.'); - var resourceName = $"{_prefix}.{testFileResourceName}"; - var projectTemplateStream = Assembly - .GetExecutingAssembly() + var resourceName = $"{prefix}.{testFileResourceName}"; + var projectTemplateStream = assembly .GetManifestResourceStream(resourceName); projectTemplateStream.Should().NotBeNull($"Resource with name '{resourceName}' should be an embedded resource"); Debug.Assert(projectTemplateStream != null); diff --git a/Tests/Reqnroll.SystemTests/SystemTestBase.cs b/Tests/Reqnroll.SystemTests/SystemTestBase.cs index 53092dc25..201a269d9 100644 --- a/Tests/Reqnroll.SystemTests/SystemTestBase.cs +++ b/Tests/Reqnroll.SystemTests/SystemTestBase.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -101,9 +102,9 @@ protected virtual void TestCleanup() _folderCleaner.CleanSolutionFolder(); } - protected void AddFeatureFileFromResource(string fileName, int? preparedTests = null) + protected void AddFeatureFileFromResource(string fileName, string? prefixOverride = null, Assembly? assembly = null, int? preparedTests = null) { - var featureFileContent = _testFileManager.GetTestFileContent(fileName); + var featureFileContent = _testFileManager.GetTestFileContent(fileName, prefixOverride, assembly); AddFeatureFile(featureFileContent, preparedTests); } From 0220a7752074d9c98fb98b5d775b9ea652abbc8e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:38:13 -0500 Subject: [PATCH 022/218] All CCK examples loaded, except for markdown, retry, and stack-traces which I don't think apply to the .NET implementation. Need to add support for images and documents as embedded resources to be added to generated test projects before attachments and hooks can be implemented and tested. Not yet added infrastructure to compare output ndjon with expected. --- .../CCK/attachments/attachments.feature | 51 ++++++++ .../attachments/attachments.feature.ndjson | 91 +++++++++++++++ .../CCK/attachments/cucumber.jpeg | Bin 0 -> 1444 bytes .../CCK/attachments/cucumber.png | Bin 0 -> 1739 bytes .../CCK/attachments/document.pdf | Bin 0 -> 10061 bytes .../CCK/cdata/cdata.cs | 18 +++ .../CCK/cdata/cdata.feature | 5 + .../CCK/cdata/cdata.feature.ndjson | 12 ++ .../CCK/data-tables/data_tables.cs | 110 ++++++++++++++++++ .../CCK/data-tables/data_tables.feature | 13 +++ .../data-tables/data_tables.feature.ndjson | 15 +++ .../CCK/examples-tables/examples_tables.cs | 54 +++++++++ .../examples-tables/examples_tables.feature | 43 +++++++ .../examples_tables.feature.ndjson | 100 ++++++++++++++++ .../CCK/hooks/cucumber.svg | 7 ++ .../CCK/hooks/hooks.feature | 21 ++++ .../CCK/hooks/hooks.feature.ndjson | 77 ++++++++++++ .../CCK/parameter-types/parameter_types.cs | 36 ++++++ .../parameter-types/parameter_types.feature | 11 ++ .../parameter_types.feature.ndjson | 13 +++ .../CCK/rules/rules.cs | 61 ++++++++++ .../CCK/rules/rules.feature | 29 +++++ .../CCK/rules/rules.feature.ndjson | 47 ++++++++ .../CCK/skipped/skipped.cs | 37 ++++++ .../CCK/skipped/skipped.feature | 19 +++ .../CCK/skipped/skipped.feature.ndjson | 33 ++++++ .../CCK/undefined/undefined.cs | 23 ++++ .../CCK/undefined/undefined.feature | 16 +++ .../CCK/undefined/undefined.feature.ndjson | 29 +++++ .../unknown_parameter_type.cs | 23 ++++ .../unknown_parameter_type.feature | 6 + .../unknown_parameter_type.feature.ndjson | 12 ++ .../CucumberCompatibilityTests.cs | 26 ++--- ...CucumberMessages.CompatibilityTests.csproj | 64 ++++++++++ 34 files changed, 1088 insertions(+), 14 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature new file mode 100644 index 000000000..063960e9c --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature @@ -0,0 +1,51 @@ +Feature: Attachments + It is sometimes useful to take a screenshot while a scenario runs. + Or capture some logs. + + Cucumber lets you `attach` arbitrary files during execution, and you can + specify a content type for the contents. + + Formatters can then render these attachments in reports. + + Attachments must have a body and a content type + + Scenario: Strings can be attached with a media type + Beware that some formatters such as @cucumber/react use the media type + to determine how to display an attachment. + + When the string "hello" is attached as "application/octet-stream" + + Scenario: Log text + When the string "hello" is logged + + Scenario: Log ANSI coloured text + When text with ANSI escapes is logged + + Scenario: Log JSON + When the following string is attached as "application/json": + ``` + {"message": "The big question", "foo": "bar"} + ``` + + Scenario: Byte arrays are base64-encoded regardless of media type + When an array with 10 bytes is attached as "text/plain" + + Scenario: Attaching JPEG images + When a JPEG image is attached + + Scenario: Attaching PNG images + When a PNG image is attached + + Scenario Outline: Attaching images in an examples table + When a image is attached + + Examples: + | type | + | JPEG | + | PNG | + + Scenario: Attaching PDFs with a different filename + When a PDF document is attached and renamed + + Scenario: Attaching URIs + When a link to "https://cucumber.io" is attached diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson new file mode 100644 index 000000000..1e42d17af --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson @@ -0,0 +1,91 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching JPEG images\n When a JPEG image is attached\n\n Scenario: Attaching PNG images\n When a PNG image is attached\n\n Scenario Outline: Attaching images in an examples table\n When a image is attached\n\n Examples:\n | type |\n | JPEG |\n | PNG |\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/attachments/attachments.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":12},"name":"Strings can be attached with a media type","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":16},"text":"the string \"hello\" is attached as \"application/octet-stream\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":18},"name":"Log text","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":19},"text":"the string \"hello\" is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"14","keyword":"Scenario","location":{"column":3,"line":21},"name":"Log ANSI coloured text","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":22},"text":"text with ANSI escapes is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"16","keyword":"Scenario","location":{"column":3,"line":24},"name":"Log JSON","steps":[{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```","location":{"column":8,"line":26}},"id":"15","keyword":"When ","keywordType":"Action","location":{"column":6,"line":25},"text":"the following string is attached as \"application/json\":"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":30},"name":"Byte arrays are base64-encoded regardless of media type","steps":[{"id":"17","keyword":"When ","keywordType":"Action","location":{"column":5,"line":31},"text":"an array with 10 bytes is attached as \"text/plain\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"20","keyword":"Scenario","location":{"column":3,"line":33},"name":"Attaching JPEG images","steps":[{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":5,"line":34},"text":"a JPEG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"22","keyword":"Scenario","location":{"column":3,"line":36},"name":"Attaching PNG images","steps":[{"id":"21","keyword":"When ","keywordType":"Action","location":{"column":5,"line":37},"text":"a PNG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"27","keyword":"Examples","location":{"column":5,"line":42},"name":"","tableBody":[{"cells":[{"location":{"column":9,"line":44},"value":"JPEG"}],"id":"25","location":{"column":7,"line":44}},{"cells":[{"location":{"column":9,"line":45},"value":"PNG"}],"id":"26","location":{"column":7,"line":45}}],"tableHeader":{"cells":[{"location":{"column":9,"line":43},"value":"type"}],"id":"24","location":{"column":7,"line":43}},"tags":[]}],"id":"28","keyword":"Scenario Outline","location":{"column":3,"line":39},"name":"Attaching images in an examples table","steps":[{"id":"23","keyword":"When ","keywordType":"Action","location":{"column":5,"line":40},"text":"a image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"30","keyword":"Scenario","location":{"column":3,"line":47},"name":"Attaching PDFs with a different filename","steps":[{"id":"29","keyword":"When ","keywordType":"Action","location":{"column":5,"line":48},"text":"a PDF document is attached and renamed"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"32","keyword":"Scenario","location":{"column":3,"line":50},"name":"Attaching URIs","steps":[{"id":"31","keyword":"When ","keywordType":"Action","location":{"column":5,"line":51},"text":"a link to \"https://cucumber.io\" is attached"}],"tags":[]}}],"description":" It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Attachments","tags":[]},"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"34","language":"en","name":"Strings can be attached with a media type","steps":[{"astNodeIds":["9"],"id":"33","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"36","language":"en","name":"Log text","steps":[{"astNodeIds":["11"],"id":"35","text":"the string \"hello\" is logged","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["14"],"id":"38","language":"en","name":"Log ANSI coloured text","steps":[{"astNodeIds":["13"],"id":"37","text":"text with ANSI escapes is logged","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["16"],"id":"40","language":"en","name":"Log JSON","steps":[{"argument":{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}"}},"astNodeIds":["15"],"id":"39","text":"the following string is attached as \"application/json\":","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["18"],"id":"42","language":"en","name":"Byte arrays are base64-encoded regardless of media type","steps":[{"astNodeIds":["17"],"id":"41","text":"an array with 10 bytes is attached as \"text/plain\"","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["20"],"id":"44","language":"en","name":"Attaching JPEG images","steps":[{"astNodeIds":["19"],"id":"43","text":"a JPEG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["22"],"id":"46","language":"en","name":"Attaching PNG images","steps":[{"astNodeIds":["21"],"id":"45","text":"a PNG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["28","25"],"id":"48","language":"en","name":"Attaching images in an examples table","steps":[{"astNodeIds":["23","25"],"id":"47","text":"a JPEG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["28","26"],"id":"50","language":"en","name":"Attaching images in an examples table","steps":[{"astNodeIds":["23","26"],"id":"49","text":"a PNG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["30"],"id":"52","language":"en","name":"Attaching PDFs with a different filename","steps":[{"astNodeIds":["29"],"id":"51","text":"a PDF document is attached and renamed","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["32"],"id":"54","language":"en","name":"Attaching URIs","steps":[{"astNodeIds":["31"],"id":"53","text":"a link to \"https://cucumber.io\" is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the string {string} is attached as {string}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"the string {string} is logged","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"text with ANSI escapes is logged","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"the following string is attached as {string}:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":18},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"an array with {int} bytes is attached as {string}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"5","pattern":{"source":"a JPEG image is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":31},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"6","pattern":{"source":"a PNG image is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":35},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"7","pattern":{"source":"a PDF document is attached and renamed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":39},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"8","pattern":{"source":"a link to {string} is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":43},"uri":"samples/attachments/attachments.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"56","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"},{"group":{"children":[{"children":[{"children":[]}],"start":35,"value":"application/octet-stream"},{"children":[{"children":[]}]}],"start":34,"value":"\"application/octet-stream\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"58","pickleId":"36","testSteps":[{"id":"57","pickleStepId":"35","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"60","pickleId":"38","testSteps":[{"id":"59","pickleStepId":"37","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"62","pickleId":"40","testSteps":[{"id":"61","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":37,"value":"application/json"},{"children":[{"children":[]}]}],"start":36,"value":"\"application/json\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"64","pickleId":"42","testSteps":[{"id":"63","pickleStepId":"41","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"10"},"parameterTypeName":"int"},{"group":{"children":[{"children":[{"children":[]}],"start":39,"value":"text/plain"},{"children":[{"children":[]}]}],"start":38,"value":"\"text/plain\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"66","pickleId":"44","testSteps":[{"id":"65","pickleStepId":"43","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"68","pickleId":"46","testSteps":[{"id":"67","pickleStepId":"45","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"70","pickleId":"48","testSteps":[{"id":"69","pickleStepId":"47","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"72","pickleId":"50","testSteps":[{"id":"71","pickleStepId":"49","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"74","pickleId":"52","testSteps":[{"id":"73","pickleStepId":"51","stepDefinitionIds":["7"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"76","pickleId":"54","testSteps":[{"id":"75","pickleStepId":"53","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":11,"value":"https://cucumber.io"},{"children":[{"children":[]}]}],"start":10,"value":"\"https://cucumber.io\""},"parameterTypeName":"string"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"77","testCaseId":"56","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"77","testStepId":"55","timestamp":{"nanos":2000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","testCaseStartedId":"77","testStepId":"55"}} +{"testStepFinished":{"testCaseStartedId":"77","testStepId":"55","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"77","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"78","testCaseId":"58","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"78","testStepId":"57","timestamp":{"nanos":6000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"78","testStepId":"57"}} +{"testStepFinished":{"testCaseStartedId":"78","testStepId":"57","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"78","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"79","testCaseId":"60","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"79","testStepId":"59","timestamp":{"nanos":10000000,"seconds":0}}} +{"attachment":{"body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"79","testStepId":"59"}} +{"testStepFinished":{"testCaseStartedId":"79","testStepId":"59","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"79","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"80","testCaseId":"62","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"80","testStepId":"61","timestamp":{"nanos":14000000,"seconds":0}}} +{"attachment":{"body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","testCaseStartedId":"80","testStepId":"61"}} +{"testStepFinished":{"testCaseStartedId":"80","testStepId":"61","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"80","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"81","testCaseId":"64","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"81","testStepId":"63","timestamp":{"nanos":18000000,"seconds":0}}} +{"attachment":{"body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","testCaseStartedId":"81","testStepId":"63"}} +{"testStepFinished":{"testCaseStartedId":"81","testStepId":"63","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"81","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"82","testCaseId":"66","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"82","testStepId":"65","timestamp":{"nanos":22000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"82","testStepId":"65"}} +{"testStepFinished":{"testCaseStartedId":"82","testStepId":"65","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"82","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"83","testCaseId":"68","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"83","testStepId":"67","timestamp":{"nanos":26000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"83","testStepId":"67"}} +{"testStepFinished":{"testCaseStartedId":"83","testStepId":"67","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"83","timestamp":{"nanos":28000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"84","testCaseId":"70","timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"84","testStepId":"69","timestamp":{"nanos":30000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"84","testStepId":"69"}} +{"testStepFinished":{"testCaseStartedId":"84","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"84","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"85","testCaseId":"72","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"71","timestamp":{"nanos":34000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"85","testStepId":"71"}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"nanos":36000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"86","testCaseId":"74","timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"73","timestamp":{"nanos":38000000,"seconds":0}}} +{"attachment":{"body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","fileName":"renamed.pdf","mediaType":"application/pdf","testCaseStartedId":"86","testStepId":"73"}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"87","testCaseId":"76","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"75","timestamp":{"nanos":42000000,"seconds":0}}} +{"attachment":{"body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","testCaseStartedId":"87","testStepId":"75"}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"nanos":44000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":45000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e833d6c77d33e620ae49d01c6341ad7e5543bbd1 GIT binary patch literal 1444 zcmex=ma3|jiIItm zOAI4iKMQ#V{6EAX$ibk;pvlar#K0uT$SlbC{|JK&&=V}oKwrQC2Ma43I|Cy#)Bjrx z93W3JFj%5R(9qV{Ntcq}Ky~tP0uLAPSj53Js=3YaT3(saOkCY9;`5A|n$EGYdN> z$V3JPCPrqUBN+tQg%k~il>!AEoq|My6AOzQCy9zF8yTB~CKo{+&uGtZX0w)!P@n75 z+pl=GZ!o)2TKe4kXH-t@v#4EXEL!JE?zOu6r)a^it+7!xlS1t8{b#8CcjV%O8K0Lw zw!5=@{{4E%lYXmb|9XFR)y|pcq!QHyPd&4_T>Dx#CekoQenQBu?LV{I?)a=Z|5`qv zFCb50m1$zkt9Lo_J0{jC=okGqK3#m~$Ha_v^0sbzj}ElmKM-8K@73}%+E1_Sj1$h= za&Sv<=wdZVW0znRzq!_L_bo^lOYMz6H&?{%)XoFh(vNfajh)BRw*tXt#N5h?h}`dZpTv+M2|iZeX+YndxfsS7m0HMKkjMk z3R-;5(6ii>*OK{q>6sTyS60rQly}eE#LW&5BG%-(C1!D;fUT^{(mfa_yh< z{#u>M^5=OJ`#1Z*!uSb$%D$efQTh0vq2T`C;?gU-q$)Q@ZJHi;`qC^B2V>vgrM{Op zz2Y^CHRFrdI~bc)vyq`s=08J-S2f$|3$=&W)J@FMUY+(-GI%Z`A@L?@)(B$FN=Q(t=#l1^;xf6 ztKqtSx0-4T?$s+Kr!hS|dgkT)$(;KdOXF_sl{kL&qTE`s1rg3$Iy;OF^4Vh9&`X%c- z?@Ir1`muNQ?HGfD#&eeSnRs0PY8_Wf0x zYr%isu|McfyL=m%q*VpMX&9LEAPE|n37MD~SOi&xgcSot42=?%9Sb43h7p*8_w1>- z6Mg&7uRHgQ{q+{+?($T>Bl_*Z+yh@{H3b$5c{NME=w}I8|8lFOYSF`(6w%|A@+BXl zmusIkS2)Mu_D0Bf{>CWJM_W~opFJsmFzB$zE1{4Rf8~=jG8xtK!v7@pU6p$h?!dst zAi;cKJ=ASXzzoOCisUvHRt7dfAw$K$LSa!QBS)~?m_cs48@=pNOiq~YFVP2{3ZGA2 z+`D6=>s#ATRX)mE@f(tl=gZwXkQQv**LRUIg?&x;o5PWIH+oN(d8?&{>zqn{JdOYJ zpDR~h9MUx8cipWf_R8znWtJ&uK2N+Xmx{{Kw?qP#Uw literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png new file mode 100644 index 0000000000000000000000000000000000000000..2760899aa0ce16aeaaea93d3118a32e7ac5bec8d GIT binary patch literal 1739 zcmV;+1~mDJP)G+r@kQ0VOJz1t=x5=pe3owlM@Ei<#*ZtpGI zgKd}Iv=WU6L@O5L5Qs#)gFvk*7OQ`VCsjOx5<>9^RV-lQA0`qL#Hi6fjMC@#y`7oe zS+={g-EDP}m+f}vz4x2%_`Ec`caU@Gwv+u9&;ebh;EPxToe6TQSS7$RIEHqf|0i z0O+ z#n5`Lu?_+l5a?76FBT_ov)oVPu)W&2iyqQk6q$E2`kQlqo37e8-)c+O?QiP<=H+=W zgC{tLKe`UI8wSZ$`}IIZS7^VP!&SQ9uqqj{=4V~1HP0i=kNxUV+pBnmxV8-q#Bxu7a+-a2h-P!vK`Neu>T>`ydm{x2F*T=U zFja;RD&fUT?=ygOHKmEIjm9$XXg4i$m^A1Td}8TX_oouO^1 zftu}jxiw*Sqct@^-zMG^t)7q>c1_O=)^$NOakew|C1vwbb(rq-=>$Sb3W6-RZHa~m zCJ-n2XtkjN1C$hf07a9cuvH0K135T-J;#nfDUID~Sv*Yz!r1hE3|a|Q4_9AFEWLHK zWz~oq!$8|OVjZ(mf6Y*Z+@G&h%4HXA<`4i@t!XO}{GYwU7nxru-K+|Xtu%RHv$V5t zB%F}kxeBctB<7atq+W_2XaHsIuS|aaB@`qli21Zmg1ge~X)cg!Z$@+@T2Kf6wH3yz z&3Yvsd2vMF!t&A=G!b@&(o#XX>H~4DCJ(w*r-TNAhSk2Rta1dYR_3O9WK; zw#R~odmNn4Rrysbu1(x67Wr<38~#FiG1`n}wO&7`@#)Xk3f`-e^xfhbRWBNCp0aZ6 zG7j-w;#H4aR;4%|7`O;-+)Hl;uEM%I>mVnijiapH>2)nQ~8L|KJMiw<@G*t1yFW9;lFYc#uS`DwjogG)sqA zR1ZXz{)UVWQ#sGl0$Aj@M%qBOs8U>G?P+&{nu~!`wccEicA`NtKbHn#g?=au3r^~) z3?!GiqlV-fJj2A0p}mr}21v220F29iQ1fLV>-W%pUD-_S!;=aJIQ+*`W9=9^HPvL{ z0;-_op8_N*ciN&xk6wP@c=5J5d#{rWB-U?ag_zePQ!sYV&o8}(nae94B=$6zwdRg$ z{h^gdI-K$AkW4|MJIzg%@h!p>_RSE#rObbMsUUTO7bj4~!21~&^MANd_d&CC2wth| hfX@)Vpvv|h{{nxzjlgvW5(5AL002ovPDHLkV1gBrLk9o= literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4647f3c9d70173713ad061f405a1aa9146615677 GIT binary patch literal 10061 zcmaKy2Q*x3*T;z{AzBC$Z6bObgBb=PdM9dh#uzQSQKAdcd#}-3^e%cQdJTeTiRdMW z&NuG8?|a|!ecxGUtvP2u``ORg`}yy)X3hKH_{zc7#0idY05Ge)v48KJn093Mdu!dRv?F9bYNy@?siCP3G zWd%bbBoJ_069g_$0byf?GzWkne8R#2N2CJ+W{vBbrXM+u+sXA%a_Q*txv!JrB!nY~ z@ZFyFCT$@ljlEBSrri`7BDeeSAZ@T$_R!+ z0Q|h%s4(IvZ`58p0`9g$6&I+6bZ~+rU%?#yFmjlr3^0~YHhkX8-kKY9K_&VB zD2AVl3;M5;*>!v1sxJ9^zo@Z3v1IYVoWN$TZ9yzSE{5-Y9C-x+%FO5&MxFFAXwg0Z z`In+9DNkMnGe_VNYy@3qMoeh86L4~I3q0rK+G@bV30oZkY%rI7Z#C3#@d>(EAG{oO z&)83Gg|-PeZA}$@R4W*1;altuDkOZF=bJOgCN(a7%t?GC{?+B!|1j3m&&!>fSmfQs z)_80|T!08%C-8UuD_VXp2b*tIp!vuwPw>`>+F+liasy(I-2AIFspI_6IX>PKv%#@F zN%!-i!9?@ZuS-|h(e~tpTQTpef40>$o06!gQ*9*<+n5{3$dY1Rz8|X}`A~lq)ASLq zDQE{Ou`aGyy$*EG|Ni}v0>xT#JuetCBtUU0v}4b690(C5N~K+w@E(fhPFV+PleK-= zW#XO^*A%hs7z&V37FsXh&1$js>Z*Dl@SBq#C z)YXP|=Q^Rs*pm7bIAWmWc=(HLS&f`*q;hSXM1Un#6WPvdpU`m%y@$^Y?vuK$@S-JV z!R)&pPMC5U3Psk;g;D|;N{j4!jVIQ@?4QDFc;K-ebr#Xj^F15$4)7d9v_tj<)8aV> z$eF?xZic_8RhOEOdLcfSnM-eHQ5IV1BM%qXx=~$aB>EEr1SRQn(Um}uYJaAl7Cq4q-b)UPxod0ajNgQ zdi{!Q<(nkMG4ech`H*8Vr7=8*YkN`JZ0zmd1>fhih4LLN){HkkglB!ic@YuMEgMam z1}~|3<&yw%V^zc??zuH=fn4o#I{%C`98lY$+cY9kBx(A%sxYDV>%A+MtpUv{R8mbX z=ye6Tr+BklLWFNj*5mKt%RzGHpy4d66lr*+yBaZhXwj1xOnN#8oNQ+z<3jXOu_~X0 zF$WzSj_{F!^Qh5)J;clK3%?PqN%KcFrjx1%_)))0(#PE?Qr=R)h&b_i%wy_CsY@T$ z_AFyUUectkph4H0$6Ot`#7MrTi0AMPPU{h*Nn(Wkq*}T47T$ur9l0H~;#EVGUl2b& zT8V?rib7sht+<$|EjGAE-4JgSGB>SnNJ};`pD&HG5Yc6G98;uXn*e^XfyOVsfLNm;!~-M?{47qg$2nB`8aFD}UJ%dkm1DW{Xu z9ANN1PnWaRP{c__~sKsj#MZ@*}QZZmmTb>l*6UJ|FU3v~pMc zZ+yJoS;hIO)jhg#eZQ;Ps8qVe8qoLfKvm>vL;~jV!|UMRAo5ELL!%(x?@yWo*iL4m ziLu8?hkm$bPexpalqJQgzD`h zvlaHzyruRFuXWh`Tl%Hqg>vDKUT+k31q!9%(hEqYy;5%@6|JJ)lsVuWbLzgI`fEUq*4C3wEy!1Quf#h|nJEbppeB9~qD zgQmQ_qGsx};gd4(f|1Zyjm49`vNoh=(a+7935o75)}b5cMoPBr+?HcV83r=wiPrtl z-aHUfYG+xe6Mj?lVqaZFpfv)HZx~c$UkeQo<-ICWAv4mPVc}c*n(vXxSnRGr?KQ$<#bpJt0ts2VezEJMu()8b zsRVku+!pPqT{abNUzFkNTkBI_VE5lZy5rKRTpuL|HBb92v^<2~igusBVSm&rZ#q{J zq>nSIVCFbSSNHIP-Y}#R`^N&UnR17`ZPi+mBWe90lyF(xm4!BP+xS~Q`C;lgN3&mB zN1KX*oWHTkYP=PqMzw&KRX*;=09H?Vb8d4vm8uf!(#piN-E>xdvCVW&gl0jp{B+ZF z1(WOucIn(-96N4a;O*rn0f^pc4|+;zCUriKYoVnf)UCKIH$6^GodUAl#6}Rqp#5 zktF5-?BcQhz*07xzi(!e+A2XH)h64c?w;~8;fne(kDloeq_>*qiDU7;%Iy;-X}^9Owm6~bU$1>8}6h?OdbcC=Ln~APTFSO^kdI#vKiUqU59^}B&*BcW?hh; zv~&1fjfbCeX3)jlB4WE@d-T~ueDwN#J&*RIYwDS`LedpznWSedqgrLF(k)wd6OemH zDn*ohjJV_%Nf?*ZoaDLwnrpGJoOaL&O=j@LJ`x+RfYFmCyr5u1G}ia20?kfkMoY+v zfn>NxOsj;&P6~!dNg#Ri!57u<8MWW4STq>Et!5wLN=Hx#ml*XgUp0$jIAR}<9-5qP zr~Vijvda?$OY3}m!7!H)mqbK6!vyKI42}p1!y)8J4a3q{33Fi8%h>|AHGL~@)Q9&T zlRLe}xQ}o4VC(v+M2_3mS*=@9{>jv_*ip>Bg_~c>x5<>DkB?Phw^XH_dFO_lE*9tz zYh&;k<)A(|VipWKb`or8La@Re)F zLa@+1tNZ-k*>7NM^6bVjE5VPS>m1&vdM|q(tN-J%S8B1|PbAl@F`P zFQ}xdtCGu}csayhaL^QqAsi^$Du--iy38HBEjO@A!Dg=q8-3zUr`+kxKAD22G%w== zo`R&pu-6hUNy>#{komZ4m5Aw?^u?6y8g=^Naf#Zz9|FVPqupE8y6N}cSQ=q?zFjZ8 zNKm10qfXO;YIm7?C@R)wly*d=Xa#F&*m;JR;XW(MC_eAf%PY%I_;fOx@|{dXQD@Ie zUg@{<7B}oTbeixucUzstrOp>j|d-Ier)1IPshzbkrVM*b3ZU&QHKkFLU(k*%J zuXlV($z94>`U6kwenZ4{bwTbkdXTA38=xbEA4+X$zQ2A=6DcC7>mO9~&^-i7_W2^} z2AYW7FjzbGG0W%D{GP}BD;_%XJr{u=Rz_|UX-%4PDguzNxY?=OjX}wE4vrkXuZ0O0 z2?0KPsVfsDWeG|*?A~<?xUP<$>>9_ z7;eY*#z3Uy4q%a#i{V*%j;I29__peL2mYPL1fK{J_i0|`mSTb{AB$K7QIyQiFd!Vvgmy=ZWc(LOWmlpjq;Kdot5h9s+-wm+7BH!g76Ai zMVXECU15%PrJ+*-_oD$*_LPe+_rPcdJda*!3#>rT3v z37!{VhlY(@7D&T7;~ieH?fwK z+_xe`4YvK1!TGEm-*Vx?`mL%BxUo4eL>!~%CF>tbYG3T!%Ma&9UuBEh=S9r87+`qH zr!>lYU~ostEW&2dTdoaKqot)1lt8k)DRgx0@+Rol+XNj%6t-c@VFCTjyz&x9_0;b~ zkLtfsbUbl=YEUlQY8r^?>e!Ue4Q`($kqST5d(X0556XvOp$j@3gRNCZ z^visv+6oGZ8FGoDz?{SkHZmG-_JqguxlKG5aeDt{RJ0&s%F~>qhD_Doex;~W6eIcP zF*X6kYM4v!fv|p&rTpuVXIqtht>@$)?y;TZSf=RsTcr%_{bm&Crx`6^b_!^$U?C14 z$P2)Pg%huz`FKeXP-FB8%Gwn<`)XI(Qnm&QCYKO|P{SxqfWg$Mdd3+Px^GfVY=hD> z7K{u{^~k(qG8C#*?^R1MFe=FuDr+TW($8}6>F!rL6l*9}2Bw;0d;2G~7K`)WyIJ@V zUZ)eTRuWb@e>poHB+<-=46>Fb-6N(%TQA;Kv2)7iW6f-1dV#r40+r|TPUH-Il;2rZ zg?*W$rCxWFKC>H&_qz;Vr)ZQfpd`^Hg06z1KT)aKyoO;{)LCpSs zl_bsRXGt;0ZwjA~$n=lm;)j*9E1#U+7mz)!N}65if(OD$wsAOU>dZm%yifHvN#S`{ zLLtFCP?AT#jsmy~1P6EJ_*lvJ`_FTa*2>n{_gdPj%zrPAAJl<)X42gPmBXkYPP-w2 zb5q@yf=PZX>@g?1Uzbj5HRc<0f)~B4wI{__*-5vh<25BZU*=8EGpV>elg(B%Uh=v5 zwBT1_)4Moq;>`18F|0UMcHcB@F$}A>N!8BMP46P80>Vb zt!6N;yn>P(l~(eRRQ9v?$}0;geVwtE!?m#5m0IH3=vo!WVaKlZfYIwg3h z+0TYpuUSu8>sq^8ON9=@bd3a!)TrxDAU5t#3n;aPC@#P8XW1YOdP}Wn*Sbr>LaWDU z61lWKJa~f~GU~;e${&&cQS{q2mNoyGfbV&QI2A>Q>!lX^@(ucf$ ze=>}V$LBUh>Y$~jMgbF+SrLtTAT(cXIn65YBDGPn#O5%&k>_64RKd&jchCH)7ql_X zqB_L!0tkjj$SN#NR(*K2<J;%_z4`d!Xsczs+$ z;car1_Sli+k%OA%elvV4?PvM7ONij8*XC;PuOmI#QtCa=`$r`(8uB9Ya6%SKO};zG8RIbG@pu%*4zb&h*Qcl-1a=PU+H#$(tRU z2{=NiJlf=!!Dji6w*IcyFW>KrAH*+ARau72NAsQYJEeZI@|am?dExaBv(A3(Uw@7a z#!d$q)z&F#&I3O_NPsh84gNavvwZ%}(hm!}2Sd&ep{Z06Hav6`%q_%f`}zTmSVjMR z1&?P@haxfWLCxP6a=K$_yC1`4RyB<0c^2NLy0Ef>-m`_7MvE`yRn!$GHOJVM9j%ed z6WlxyA+uWP`#{K=~bG@nIwOckE{aKAKUzr#=Sv zT0}8K4aV+ACGWrK;zDn&AJ*SumR&AC5Lqdm>o8pRPWT~z^;WyJKO^=B9`od@y_M1W zJZK^Py?2Ftt73EXpW*inTMI1nJ0kfOP?p_O`*oH@3LRv zz3yDLIgyBSqD3>teZ9aL{Q$J7`wh}**aQ{Y^E>MXrvc%GxOPiOJbv`O>FxFhc7~sD z)6hceTgFT}6et21Mutzc)k&>i>C)vbKro9oMq1 zJDce`Lxk!+Z*L~<9_$*m79`m|I$G@W2p@96KPokD6TV{1!kFxUGKpYdT+MN26^CNn=I&fi$Au5^TRgemgT;*o%K3^^0v~m&F&qDJ8;Uu6 z3^J5B1kA}dP|ALil^zjXo@kD~{)aHwP_mGMrmq2%@Wj*>SeX(iQ;uC#)|%{tX~p?J zO{QAb>c02_8gLV=&aFJOaC-^_oO{>LQaCF3NLt@=ZxsV`m`muxmbz!uSRt$HuRgWz zZmUcCT;~s8=jPCVSQNEj&5pol;v1BJhNU1paps1zsT7=VmqVr4WpypOOcI}pr66~Ial^YD)Aylan13uR}XTpw4a-qDv?LEMXG*g zV@j-&yDdvrhl(F%r0w8V90X5ZVZCNK>eEz3XcT!T6>u9(FrDKG#d}Tq8>-iHJFDU6 zdU<*JsAxF(Y>L$|N_t4o%ZBxod;&?vdCn|osfo)zNF--^+(*{)kXGXc;}vZuPh|Lq zTpEv$-r_<i!k|0^bOAY!rdxZ*+SS8`5`poU^l=V?9|zQnr-xf;=@rvinTJ6gSRJ}bsFk(E7pN3~}pCU0Ph zhxOC5_}T)EF9Xq}&Qg6eQ^5kWr6f*70w26jC$&O;uKbP$^StRHUp^@dk$Tl~8!(s} zbJhAJ<%>Wyvk3e67_-k4>v?hNpaP+@nJ_{L`2^fR?D5Gf zIBku_SquJ;Vfi4|>$zCA>mHHY)f0l%me-W)cx<2QiVcH9L`thdoUS@*Nny9-Mp;hUTSh6*kv9^ zaenPVi*dA;BOizM(^auntw*eWFUIpe%`+5;bZUQ?q|v?Sx0(~x+1;1P>^f`~XL4rN z&3Q3U5^9*ctxqZiyI{dBusfe0#4(~bC{j`(l|;{B&|!XR&Va{-bw*7i4)^h7CZmw{ z^f_#|W$|702__{}xaTDfk-MLv06AcZdsri{kVk6tbnc}5aD(ktj(R2uVV$9xfW=Gl z1GWJG6Ibknug!8|oFu1_VKcUu>~%l>P{KwQ`!)TKSB6I3!5GYxN%AzyFSiJ(?q9w8 zWsb&`;o$3Mk$D5fkLHiVLdRpKv_eMKCuFQQ$!C~X8o^A)ij>EmrCbKbYU@WT79!twWjQChNycDy1g{sgJbK8v*O zL*A$SzP)Hn6XzF9yt1n%jc8hu@fV;JF}SZ9gMq}F∨WEz|Va{bk-|&;_RAeM9+J zUnh0UX026ZO7^YamIZ5tFXz1xw3~EOReU=#MGVLU&?oU#=?!Q&VOGMa9g3{|}-(rTcEVg5D)DI{a z2GWDRGn4V{waEAz^x84pWqc%zrU@5vy=C+T^*opZy{sEeGZf(2bRQ7&>;3n3xehtByZm`RwV*79njVs~e!)Ed` z;YH~-tZ&NoXtbHSzJwFj7w*SIi)C%{Z8`Yu97yZ~Sa9T+txx2H)bjqO(lY!JZ9$Wd z?q>Zu#8Mn;b2b@_2~w}=Ym!RS<3n-cI9 zwjH50Myt%cFL0csJ&^^?GTKnrwi)*kO{uyhF(kR-koS?7m80R(c zIKR7oLwk9?hVqQyn)s}&`*Z`ke0eNnCvq*jB#li^lS_Wwa*V!tmKv21n> zIWRmXIB*O$9CN=2MaFzX^}(gDA^Pdq3=-`OpZdO+JS|#VtZN9tHt-cWHTu>u-?F#k zg|~2l_XW@B=IvR%<3*oH%%q4gQvvODblS~06nfp|{k(t{nm$Q>+P@j-Oauu{)-!d1p-7KZuoz;VzBBwu( zGi;S^D}Nr}uHT)sBb(kg=cqeOjYzwL|Arc%fB!xIC)99<9!MY@;SLsdNLzul+F$c0JmcRloYIBFCpvZ;0 zrxE)DdEkIRcsapfEub zk(E%lm6nxIgxR484--`O7Ra}MnAP4oqEeT&F}1xzI#dy67AUUdEr3}R#n~WGfR3^Q zioLP0F#|CFV;ah+=45AQg}6g_09<$M$Ll-jLzfTCr4Imu0lE+n7biats*w5sK5l?6 z7!2VAfw_55xBPr4WP^_nHHs>zKB^!RuK{4LyBPnd!@pod{~vUCCq~vXRzqlXO#k@lV>sE{OE+1y!pwuC+l-%Lw!84+cXP;}JYhW}G195Vmt850;vz9?0pz?(k=tBTq|bz2P^i#wnS0J__|f6=t71Hu#+ z)d1YMs6P1X06@TC9x%WZ@J|~E#Rc6R0Gof>c=%CR&wtoJT&RxucN-MKg~ECM-Npst z{qJ~SD5|^u!w>Sm_(7omwJtv&O0xek7s|zr%JkpkLAm+=WnH8L4AtZgckG**g&V3e ns1{VUwMD&4{_NjB+U+0Jazw%$kbl$)%Et@FWnhq0mcsoXXWVEI literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs new file mode 100644 index 000000000..036edadcf --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs @@ -0,0 +1,18 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.cdata +{ + [Binding] + internal class Cdata + { + [Given("I have {int} in my belly")] + public void GivenIHaveCukesInMyBelly(int p0) + { + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature new file mode 100644 index 000000000..9b6ebb155 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature @@ -0,0 +1,5 @@ +Feature: cdata + Cucumber xml formatters should be able to handle xml cdata elements + + Scenario: cdata + Given I have 42 in my belly diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson new file mode 100644 index 000000000..51aa3abcb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: cdata\n Cucumber xml formatters should be able to handle xml cdata elements\n\n Scenario: cdata\n Given I have 42 in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/cdata/cdata.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":4},"name":"cdata","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":5},"text":"I have 42 in my belly"}],"tags":[]}}],"description":" Cucumber xml formatters should be able to handle xml cdata elements","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"cdata","tags":[]},"uri":"samples/cdata/cdata.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cdata","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 in my belly","type":"Context"}],"tags":[],"uri":"samples/cdata/cdata.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"I have {int} in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/cdata/cdata.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs new file mode 100644 index 000000000..0e696ae16 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs @@ -0,0 +1,110 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.data_tables +{ + [Binding] + internal class data_tables + { + private Table? _transposedTable; + + [When("the following table is transposed:")] + public void WhenTheFollowingTableIsTransposed(Table table) + { + _transposedTable = Transpose(table); + } + + [Then("it should be:")] + public void ThenItShouldBe(Table expected) + { + TablesEqual(expected, _transposedTable!); + } + + private void TablesEqual(Table expected, Table transposedTable) + { + var ExpectednumRows = expected.Rows.Count; + var ExpectednumCols = expected.Rows[0].Count; + + if (ExpectednumRows != transposedTable.Rows.Count || ExpectednumCols != transposedTable.Rows[0].Count) + { + throw new Exception("Tables are not equal"); + } + + for (int i = 0; i < ExpectednumRows; i++) + { + for (int j = 0; j < ExpectednumCols; j++) + { + if (expected.Rows[i][j].ToString() != transposedTable.Rows[i][j].ToString()) + { + throw new Exception("Tables are not equal"); + } + } + } + } + + private Table Transpose(Table table) + { + + string[][] matrix = GetStringArray(table.Rows); + var t = TransposeMatrix(matrix); + return CreateTable(t); + + static string[][] GetStringArray(DataTableRows rows) + { + int numRows = rows.Count; + int numCols = rows.FirstOrDefault()?.Count ?? 0; + + string[][] result = new string[numRows][]; + for (int i = 0; i < numRows; i++) + { + result[i] = new string[numCols]; + for (int j = 0; j < numCols; j++) + { + result[i][j] = rows[i][j].ToString(); + } + } + + return result; + } + static string[][] TransposeMatrix(string[][] matrix) + { + int numRows = matrix.Length; + int numCols = matrix[0].Length; + + string[][] transposedMatrix = new string[numCols][]; + for (int i = 0; i < numCols; i++) + { + transposedMatrix[i] = new string[numRows]; + } + + for (int i = 0; i < numRows; i++) + { + for (int j = 0; j < numCols; j++) + { + transposedMatrix[j][i] = matrix[i][j]; + } + } + + return transposedMatrix; + } + static Table CreateTable(string[][] matrix) + { + var columnCount = matrix[0].Length; + var headers = Enumerable.Range(0, columnCount).Select(i => $"").ToArray(); + var table = new Table(); + + foreach (var row in matrix) + { + table.AddRow(row); + } + + return table; + } + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature new file mode 100644 index 000000000..2822419e5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature @@ -0,0 +1,13 @@ +Feature: Data Tables + Data Tables can be placed underneath a step and will be passed as the last + argument to the step definition. + + They can be used to represent richer data structures, and can be transformed to other data-types. + + Scenario: transposed table + When the following table is transposed: + | a | b | + | 1 | 2 | + Then it should be: + | a | 1 | + | b | 2 | diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson new file mode 100644 index 000000000..a8a9c2bd7 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson @@ -0,0 +1,15 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Data Tables\n Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.\n\n Scenario: transposed table\n When the following table is transposed:\n | a | b |\n | 1 | 2 |\n Then it should be:\n | a | 1 |\n | b | 2 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/data-tables/data-tables.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":7},"name":"transposed table","steps":[{"dataTable":{"location":{"column":7,"line":9},"rows":[{"cells":[{"location":{"column":9,"line":9},"value":"a"},{"location":{"column":13,"line":9},"value":"b"}],"id":"2","location":{"column":7,"line":9}},{"cells":[{"location":{"column":9,"line":10},"value":"1"},{"location":{"column":13,"line":10},"value":"2"}],"id":"3","location":{"column":7,"line":10}}]},"id":"4","keyword":"When ","keywordType":"Action","location":{"column":5,"line":8},"text":"the following table is transposed:"},{"dataTable":{"location":{"column":7,"line":12},"rows":[{"cells":[{"location":{"column":9,"line":12},"value":"a"},{"location":{"column":13,"line":12},"value":"1"}],"id":"5","location":{"column":7,"line":12}},{"cells":[{"location":{"column":9,"line":13},"value":"b"},{"location":{"column":13,"line":13},"value":"2"}],"id":"6","location":{"column":7,"line":13}}]},"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":11},"text":"it should be:"}],"tags":[]}}],"description":" Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Data Tables","tags":[]},"uri":"samples/data-tables/data-tables.feature"}} +{"pickle":{"astNodeIds":["8"],"id":"11","language":"en","name":"transposed table","steps":[{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"b"}]},{"cells":[{"value":"1"},{"value":"2"}]}]}},"astNodeIds":["4"],"id":"9","text":"the following table is transposed:","type":"Action"},{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"1"}]},{"cells":[{"value":"b"},{"value":"2"}]}]}},"astNodeIds":["7"],"id":"10","text":"it should be:","type":"Outcome"}],"tags":[],"uri":"samples/data-tables/data-tables.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the following table is transposed:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":5},"uri":"samples/data-tables/data-tables.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"it should be:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":9},"uri":"samples/data-tables/data-tables.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"14","pickleId":"11","testSteps":[{"id":"12","pickleStepId":"9","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"13","pickleStepId":"10","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"15","testCaseId":"14","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"12","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"12","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"13","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"13","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"15","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":7000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs new file mode 100644 index 000000000..a87217d3e --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs @@ -0,0 +1,54 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.examples_tables +{ + [Binding] + internal class example_tables + { + private int _count; + private int _friends; + + [Given("there are {int} cucumbers")] + public void GivenThereAreCucumbers(int p0) + { + _count = p0; + } + + [Given("there are {int} friends")] + public void GivenThereAreFriends(int p0) + { + _friends = p0; + } + + [When("I eat {int} cucumbers")] + public void WhenIEatCucumbers(int p0) + { + _count -= p0; + } + + [Then("I should have {int} cuc umbers")] + public void ThenIShouldHaveCucumbers(int p0) + { + if (_count != p0) + { + throw new Exception($"Cucumber count mismatch: Expected {p0}, got {_count}"); + } + } + + [Then("each person can eat {int} cucumbers")] + public void ThenEachPersonCanEatCucumbers(int p0) + { + var share = Math.Floor((double)_count / (1 + _friends)); + + if (share != p0) + { + throw new Exception($"Cucumber share mismatch: Expected {p0}, got {share}"); + } + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature new file mode 100644 index 000000000..6ce92b8c2 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature @@ -0,0 +1,43 @@ +Feature: Examples Tables + Sometimes it can be desirable to run the same scenario multiple times with + different data each time - this can be done by placing an Examples table + underneath a Scenario, and use in the Scenario which match the + table headers. + + The Scenario Outline name can also be parameterized. The name of the resulting + pickle will have the replaced with the value from the examples + table. + + Scenario Outline: Eating cucumbers + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + @passing + Examples: These are passing + | start | eat | left | + | 12 | 5 | 7 | + | 20 | 5 | 15 | + + @failing + Examples: These are failing + | start | eat | left | + | 12 | 20 | 0 | + | 0 | 1 | 0 | + + @undefined + Examples: These are undefined because the value is not an {int} + | start | eat | left | + | 12 | banana | 12 | + | 0 | 1 | apple | + + Scenario Outline: Eating cucumbers with friends + Given there are friends + And there are cucumbers + Then each person can eat cucumbers + + Examples: + | friends | start | share | + | 11 | 12 | 1 | + | 1 | 4 | 2 | + | 0 | 4 | 4 | diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson new file mode 100644 index 000000000..b169fd03d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson @@ -0,0 +1,100 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n @undefined\n Examples: These are undefined because the value is not an {int}\n | start | eat | left |\n | 12 | banana | 12 |\n | 0 | 1 | apple |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/examples-tables/examples-tables.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[{"description":"","id":"12","keyword":"Examples","location":{"column":5,"line":17},"name":"These are passing","tableBody":[{"cells":[{"location":{"column":12,"line":19},"value":"12"},{"location":{"column":19,"line":19},"value":"5"},{"location":{"column":26,"line":19},"value":"7"}],"id":"9","location":{"column":7,"line":19}},{"cells":[{"location":{"column":12,"line":20},"value":"20"},{"location":{"column":19,"line":20},"value":"5"},{"location":{"column":25,"line":20},"value":"15"}],"id":"10","location":{"column":7,"line":20}}],"tableHeader":{"cells":[{"location":{"column":9,"line":18},"value":"start"},{"location":{"column":17,"line":18},"value":"eat"},{"location":{"column":23,"line":18},"value":"left"}],"id":"8","location":{"column":7,"line":18}},"tags":[{"id":"11","location":{"column":5,"line":16},"name":"@passing"}]},{"description":"","id":"17","keyword":"Examples","location":{"column":5,"line":23},"name":"These are failing","tableBody":[{"cells":[{"location":{"column":12,"line":25},"value":"12"},{"location":{"column":18,"line":25},"value":"20"},{"location":{"column":26,"line":25},"value":"0"}],"id":"14","location":{"column":7,"line":25}},{"cells":[{"location":{"column":13,"line":26},"value":"0"},{"location":{"column":19,"line":26},"value":"1"},{"location":{"column":26,"line":26},"value":"0"}],"id":"15","location":{"column":7,"line":26}}],"tableHeader":{"cells":[{"location":{"column":9,"line":24},"value":"start"},{"location":{"column":17,"line":24},"value":"eat"},{"location":{"column":23,"line":24},"value":"left"}],"id":"13","location":{"column":7,"line":24}},"tags":[{"id":"16","location":{"column":5,"line":22},"name":"@failing"}]},{"description":"","id":"22","keyword":"Examples","location":{"column":5,"line":29},"name":"These are undefined because the value is not an {int}","tableBody":[{"cells":[{"location":{"column":12,"line":31},"value":"12"},{"location":{"column":17,"line":31},"value":"banana"},{"location":{"column":29,"line":31},"value":"12"}],"id":"19","location":{"column":7,"line":31}},{"cells":[{"location":{"column":13,"line":32},"value":"0"},{"location":{"column":22,"line":32},"value":"1"},{"location":{"column":26,"line":32},"value":"apple"}],"id":"20","location":{"column":7,"line":32}}],"tableHeader":{"cells":[{"location":{"column":9,"line":30},"value":"start"},{"location":{"column":17,"line":30},"value":"eat"},{"location":{"column":26,"line":30},"value":"left"}],"id":"18","location":{"column":7,"line":30}},"tags":[{"id":"21","location":{"column":5,"line":28},"name":"@undefined"}]}],"id":"23","keyword":"Scenario Outline","location":{"column":3,"line":11},"name":"Eating cucumbers","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":12},"text":"there are cucumbers"},{"id":"6","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"I eat cucumbers"},{"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":14},"text":"I should have cucumbers"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"31","keyword":"Examples","location":{"column":5,"line":39},"name":"","tableBody":[{"cells":[{"location":{"column":14,"line":41},"value":"11"},{"location":{"column":22,"line":41},"value":"12"},{"location":{"column":31,"line":41},"value":"1"}],"id":"28","location":{"column":7,"line":41}},{"cells":[{"location":{"column":15,"line":42},"value":"1"},{"location":{"column":23,"line":42},"value":"4"},{"location":{"column":31,"line":42},"value":"2"}],"id":"29","location":{"column":7,"line":42}},{"cells":[{"location":{"column":15,"line":43},"value":"0"},{"location":{"column":23,"line":43},"value":"4"},{"location":{"column":31,"line":43},"value":"4"}],"id":"30","location":{"column":7,"line":43}}],"tableHeader":{"cells":[{"location":{"column":9,"line":40},"value":"friends"},{"location":{"column":19,"line":40},"value":"start"},{"location":{"column":27,"line":40},"value":"share"}],"id":"27","location":{"column":7,"line":40}},"tags":[]}],"id":"32","keyword":"Scenario Outline","location":{"column":3,"line":34},"name":"Eating cucumbers with friends","steps":[{"id":"24","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":35},"text":"there are friends"},{"id":"25","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":36},"text":"there are cucumbers"},{"id":"26","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":37},"text":"each person can eat cucumbers"}],"tags":[]}}],"description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Examples Tables","tags":[]},"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","9"],"id":"36","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","9"],"id":"33","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","9"],"id":"34","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["7","9"],"id":"35","text":"I should have 7 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@passing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","10"],"id":"40","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","10"],"id":"37","text":"there are 20 cucumbers","type":"Context"},{"astNodeIds":["6","10"],"id":"38","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["7","10"],"id":"39","text":"I should have 15 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@passing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","14"],"id":"44","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","14"],"id":"41","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","14"],"id":"42","text":"I eat 20 cucumbers","type":"Action"},{"astNodeIds":["7","14"],"id":"43","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"16","name":"@failing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","15"],"id":"48","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","15"],"id":"45","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["6","15"],"id":"46","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["7","15"],"id":"47","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"16","name":"@failing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","19"],"id":"52","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","19"],"id":"49","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","19"],"id":"50","text":"I eat banana cucumbers","type":"Action"},{"astNodeIds":["7","19"],"id":"51","text":"I should have 12 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"21","name":"@undefined"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","20"],"id":"56","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","20"],"id":"53","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["6","20"],"id":"54","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["7","20"],"id":"55","text":"I should have apple cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"21","name":"@undefined"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","28"],"id":"60","language":"en","name":"Eating cucumbers with 11 friends","steps":[{"astNodeIds":["24","28"],"id":"57","text":"there are 11 friends","type":"Context"},{"astNodeIds":["25","28"],"id":"58","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["26","28"],"id":"59","text":"each person can eat 1 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","29"],"id":"64","language":"en","name":"Eating cucumbers with 1 friends","steps":[{"astNodeIds":["24","29"],"id":"61","text":"there are 1 friends","type":"Context"},{"astNodeIds":["25","29"],"id":"62","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["26","29"],"id":"63","text":"each person can eat 2 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","30"],"id":"68","language":"en","name":"Eating cucumbers with 0 friends","steps":[{"astNodeIds":["24","30"],"id":"65","text":"there are 0 friends","type":"Context"},{"astNodeIds":["25","30"],"id":"66","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["26","30"],"id":"67","text":"each person can eat 4 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"there are {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"there are {int} friends","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"I eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"I should have {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"each person can eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":20},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"72","pickleId":"36","testSteps":[{"id":"69","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"70","pickleStepId":"34","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"35","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"76","pickleId":"40","testSteps":[{"id":"73","pickleStepId":"37","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"74","pickleStepId":"38","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"80","pickleId":"44","testSteps":[{"id":"77","pickleStepId":"41","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"78","pickleStepId":"42","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"43","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"84","pickleId":"48","testSteps":[{"id":"81","pickleStepId":"45","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"82","pickleStepId":"46","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"47","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"88","pickleId":"52","testSteps":[{"id":"85","pickleStepId":"49","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"86","pickleStepId":"50","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"87","pickleStepId":"51","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"12"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"92","pickleId":"56","testSteps":[{"id":"89","pickleStepId":"53","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"90","pickleStepId":"54","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"91","pickleStepId":"55","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"96","pickleId":"60","testSteps":[{"id":"93","pickleStepId":"57","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"11"},"parameterTypeName":"int"}]}]},{"id":"94","pickleStepId":"58","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"95","pickleStepId":"59","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"1"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"100","pickleId":"64","testSteps":[{"id":"97","pickleStepId":"61","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"98","pickleStepId":"62","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"99","pickleStepId":"63","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"2"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"104","pickleId":"68","testSteps":[{"id":"101","pickleStepId":"65","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"102","pickleStepId":"66","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"103","pickleStepId":"67","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"4"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"105","testCaseId":"72","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"69","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"70","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"70","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"71","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"105","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"106","testCaseId":"76","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"73","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"74","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"74","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"75","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"106","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"107","testCaseId":"80","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"77","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"77","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"78","timestamp":{"nanos":20000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"78","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"79","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"79","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:25","status":"FAILED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"107","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"108","testCaseId":"84","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"81","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"81","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"82","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"82","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"83","timestamp":{"nanos":30000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"83","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:26","status":"FAILED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"108","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"109","testCaseId":"88","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"85","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"85","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"86","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"86","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"87","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"87","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"109","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"110","testCaseId":"92","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"89","timestamp":{"nanos":42000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"89","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"90","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"90","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"91","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"91","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"110","timestamp":{"nanos":48000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"111","testCaseId":"96","timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"93","timestamp":{"nanos":50000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"93","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"94","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"94","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"95","timestamp":{"nanos":54000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"95","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":55000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"111","timestamp":{"nanos":56000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"112","testCaseId":"100","timestamp":{"nanos":57000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"97","timestamp":{"nanos":58000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"97","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":59000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"98","timestamp":{"nanos":60000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"98","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":61000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"99","timestamp":{"nanos":62000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"99","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":63000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"112","timestamp":{"nanos":64000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"113","testCaseId":"104","timestamp":{"nanos":65000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"101","timestamp":{"nanos":66000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"101","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":67000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"102","timestamp":{"nanos":68000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"102","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":69000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"103","timestamp":{"nanos":70000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"103","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":71000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"113","timestamp":{"nanos":72000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":73000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg new file mode 100644 index 000000000..e76ff7faf --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature new file mode 100644 index 000000000..4136a8a51 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature @@ -0,0 +1,21 @@ +Feature: Hooks + Hooks are special steps that run before or after each scenario's steps. + + They can also conditionally target specific scenarios, using tag expressions + + Scenario: No tags and a passed step + When a step passes + + Scenario: No tags and a failed step + When a step fails + + Scenario: No tags and a undefined step + When a step does not exist + + @some-tag + Scenario: With a tag, a failure in the hook and a passed step + When a step passes + + @with-attachment + Scenario: With an tag, an valid attachment in the hook and a passed step + When a step passes diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson new file mode 100644 index 000000000..f3946e576 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson @@ -0,0 +1,77 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n\n Scenario: No tags and a undefined step\n When a step does not exist\n\n @some-tag\n Scenario: With a tag, a failure in the hook and a passed step\n When a step passes\n\n @with-attachment\n Scenario: With an tag, an valid attachment in the hook and a passed step\n When a step passes\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/hooks/hooks.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":6},"name":"No tags and a passed step","steps":[{"id":"7","keyword":"When ","keywordType":"Action","location":{"column":5,"line":7},"text":"a step passes"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":9},"name":"No tags and a failed step","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":10},"text":"a step fails"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":12},"name":"No tags and a undefined step","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"a step does not exist"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Scenario","location":{"column":3,"line":16},"name":"With a tag, a failure in the hook and a passed step","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":17},"text":"a step passes"}],"tags":[{"id":"14","location":{"column":3,"line":15},"name":"@some-tag"}]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":20},"name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"id":"16","keyword":"When ","keywordType":"Action","location":{"column":5,"line":21},"text":"a step passes"}],"tags":[{"id":"17","location":{"column":3,"line":19},"name":"@with-attachment"}]}}],"description":" Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Hooks","tags":[]},"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["8"],"id":"20","language":"en","name":"No tags and a passed step","steps":[{"astNodeIds":["7"],"id":"19","text":"a step passes","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"22","language":"en","name":"No tags and a failed step","steps":[{"astNodeIds":["9"],"id":"21","text":"a step fails","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"24","language":"en","name":"No tags and a undefined step","steps":[{"astNodeIds":["11"],"id":"23","text":"a step does not exist","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["15"],"id":"26","language":"en","name":"With a tag, a failure in the hook and a passed step","steps":[{"astNodeIds":["13"],"id":"25","text":"a step passes","type":"Action"}],"tags":[{"astNodeId":"14","name":"@some-tag"}],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["18"],"id":"28","language":"en","name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"astNodeIds":["16"],"id":"27","text":"a step passes","type":"Action"}],"tags":[{"astNodeId":"17","name":"@with-attachment"}],"uri":"samples/hooks/hooks.feature"}} +{"stepDefinition":{"id":"2","pattern":{"source":"a step passes","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/hooks/hooks.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"a step fails","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":4},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"1","name":"A named hook","sourceReference":{"location":{"line":8},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"4","sourceReference":{"location":{"line":20},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"5","sourceReference":{"location":{"line":24},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@some-tag or @some-other-tag"}} +{"hook":{"id":"6","sourceReference":{"location":{"line":28},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@with-attachment"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"33","pickleId":"20","testSteps":[{"hookId":"0","id":"29"},{"hookId":"1","id":"30"},{"id":"31","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"32"}]}} +{"testCase":{"id":"38","pickleId":"22","testSteps":[{"hookId":"0","id":"34"},{"hookId":"1","id":"35"},{"id":"36","pickleStepId":"21","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"37"}]}} +{"testCase":{"id":"43","pickleId":"24","testSteps":[{"hookId":"0","id":"39"},{"hookId":"1","id":"40"},{"id":"41","pickleStepId":"23","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"hookId":"4","id":"42"}]}} +{"testCase":{"id":"49","pickleId":"26","testSteps":[{"hookId":"0","id":"44"},{"hookId":"1","id":"45"},{"id":"46","pickleStepId":"25","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"5","id":"47"},{"hookId":"4","id":"48"}]}} +{"testCase":{"id":"55","pickleId":"28","testSteps":[{"hookId":"0","id":"50"},{"hookId":"1","id":"51"},{"id":"52","pickleStepId":"27","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"6","id":"53"},{"hookId":"4","id":"54"}]}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"33","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"29","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"29","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"30","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"30","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"31","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"31","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"32","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"32","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"57","testCaseId":"38","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"34","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"34","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"35","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"35","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"36","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"36","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in step","type":"Error"},"message":"Exception in step\nsamples/hooks/hooks.feature:10","status":"FAILED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"37","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"37","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"58","testCaseId":"43","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"39","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"40","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"41","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"41","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"42","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"59","testCaseId":"49","timestamp":{"nanos":31000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"44","timestamp":{"nanos":32000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"46","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"47","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in conditional hook","type":"Error"},"message":"Exception in conditional hook\nsamples/hooks/hooks.feature:16","status":"FAILED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"48","timestamp":{"nanos":40000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"48","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":41000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"nanos":42000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"60","testCaseId":"55","timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"50","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"51","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"52","timestamp":{"nanos":48000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"53","timestamp":{"nanos":50000000,"seconds":0}}} +{"attachment":{"body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","testCaseStartedId":"60","testStepId":"53"}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"53","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"54","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"54","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"nanos":54000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":55000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs new file mode 100644 index 000000000..faad8ebb5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs @@ -0,0 +1,36 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.parameter_types +{ + [Binding] + internal class Parameter_types + { + [StepArgumentTransformation(@"([A-Z]{3})-([A-Z]{3})")] + public Flight FlightConverter(string from, string to) + { + return new Flight + { + From = from, + To = to + }; + } + + [Given("{flight} has been delayed")] + public void GivenFlightHasBeenDelayed(Flight flight) + { + if (flight.From == "LHR" && flight.To == "CDG") { } + else throw new Exception(); + } + } + + public class Flight + { + public string? From { get; internal set; } + public string? To { get; internal set; } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature new file mode 100644 index 000000000..67e099463 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature @@ -0,0 +1,11 @@ +Feature: Parameter Types + Cucumber lets you define your own parameter types, which can be used + in Cucumber Expressions. + + This lets you define a precise domain-specific vocabulary which can be used to + generate a glossary with examples taken from your scenarios. + + Parameter types also enable you to transform strings and tables into different types. + + Scenario: Flight transformer + Given LHR-CDG has been delayed diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson new file mode 100644 index 000000000..b2eddd562 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson @@ -0,0 +1,13 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.\n\n Scenario: Flight transformer\n Given LHR-CDG has been delayed\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/parameter-types/parameter-types.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":10},"name":"Flight transformer","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"LHR-CDG has been delayed"}],"tags":[]}}],"description":" Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/parameter-types/parameter-types.feature"}} +{"pickle":{"astNodeIds":["3"],"id":"5","language":"en","name":"Flight transformer","steps":[{"astNodeIds":["2"],"id":"4","text":"LHR-CDG has been delayed","type":"Context"}],"tags":[],"uri":"samples/parameter-types/parameter-types.feature"}} +{"parameterType":{"id":"0","name":"flight","preferForRegularExpressionMatch":false,"regularExpressions":["([A-Z]{3})-([A-Z]{3})"],"sourceReference":{"location":{"line":8},"uri":"samples/parameter-types/parameter-types.feature.ts"},"useForSnippets":true}} +{"stepDefinition":{"id":"1","pattern":{"source":"{flight} has been delayed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/parameter-types/parameter-types.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"7","pickleId":"5","testSteps":[{"id":"6","pickleStepId":"4","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[],"start":0,"value":"LHR"},{"children":[],"start":4,"value":"CDG"}],"start":0,"value":"LHR-CDG"},"parameterTypeName":"flight"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs new file mode 100644 index 000000000..19104787d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs @@ -0,0 +1,61 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.rules +{ + [Binding] + internal class Rules + { + private int Money; + private Stack Stock = new(); + private string _chocolate = ""; + + [Given("the customer has {int} cents")] + public void GivenTheCustomerHasCents(int p0) + { + Money = p0; + } + + [Given("there are chocolate bars in stock")] + public void GivenThereAreChocolateBarsInStock() + { + Stock = new Stack(); + Stock.Push("Mars"); + } + + [Given("there are no chocolate bars in stock")] + public void GivenThereAreNoChocolateBarsInStock() + { + Stock = new Stack(); + } + + [When("the customer tries to buy a {int} cent chocolate bar")] + public void WhenTheCustomerTriesToBuyACentChocolateBar(int p0) + { + if (Money >= p0) + { + if (!Stock.TryPop(out _chocolate!)) + _chocolate = ""; + } + } + + [Then("the sale should not happen")] + public void ThenTheSaleShouldNotHappen() + { + if (!string.IsNullOrEmpty(_chocolate)) + throw new Exception("Sale should not happen"); + } + + [Then("the sale should happen")] + public void ThenTheSaleShouldHappen() + { + if (string.IsNullOrEmpty(_chocolate)) + throw new Exception("Sale should happen"); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature new file mode 100644 index 000000000..5d576ac70 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature @@ -0,0 +1,29 @@ +Feature: Usage of a `Rule` + You can place scenarios inside rules. This makes it possible to structure Gherkin documents + in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/). + + You can also use the Examples synonym for Scenario to make them even similar. + + Rule: A sale cannot happen if the customer does not have enough money + # Unhappy path + Example: Not enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 125 cent chocolate bar + Then the sale should not happen + + # Happy path + Example: Enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 75 cent chocolate bar + Then the sale should happen + + @some-tag + Rule: a sale cannot happen if there is no stock + # Unhappy path + Example: No chocolates left + Given the customer has 100 cents + And there are no chocolate bars in stock + When the customer tries to buy a 1 cent chocolate bar + Then the sale should not happen diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson new file mode 100644 index 000000000..58839fa63 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson @@ -0,0 +1,47 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/rules/rules.feature"}} +{"gherkinDocument":{"comments":[{"location":{"column":1,"line":8},"text":" # Unhappy path"},{"location":{"column":1,"line":15},"text":" # Happy path"},{"location":{"column":1,"line":24},"text":" # Unhappy path"}],"feature":{"children":[{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"10","keyword":"Example","location":{"column":5,"line":9},"name":"Not enough money","steps":[{"id":"6","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":10},"text":"the customer has 100 cents"},{"id":"7","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":11},"text":"there are chocolate bars in stock"},{"id":"8","keyword":"When ","keywordType":"Action","location":{"column":7,"line":12},"text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"9","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":13},"text":"the sale should not happen"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Example","location":{"column":5,"line":16},"name":"Enough money","steps":[{"id":"11","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":17},"text":"the customer has 100 cents"},{"id":"12","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":18},"text":"there are chocolate bars in stock"},{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":7,"line":19},"text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"14","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":20},"text":"the sale should happen"}],"tags":[]}}],"description":"","id":"16","keyword":"Rule","location":{"column":3,"line":7},"name":"A sale cannot happen if the customer does not have enough money","tags":[]}},{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"21","keyword":"Example","location":{"column":5,"line":25},"name":"No chocolates left","steps":[{"id":"17","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":26},"text":"the customer has 100 cents"},{"id":"18","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":27},"text":"there are no chocolate bars in stock"},{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":7,"line":28},"text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"20","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":29},"text":"the sale should not happen"}],"tags":[]}}],"description":"","id":"23","keyword":"Rule","location":{"column":3,"line":23},"name":"a sale cannot happen if there is no stock","tags":[{"id":"22","location":{"column":3,"line":22},"name":"@some-tag"}]}}],"description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Usage of a `Rule`","tags":[]},"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"28","language":"en","name":"Not enough money","steps":[{"astNodeIds":["6"],"id":"24","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["7"],"id":"25","text":"there are chocolate bars in stock","type":"Context"},{"astNodeIds":["8"],"id":"26","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action"},{"astNodeIds":["9"],"id":"27","text":"the sale should not happen","type":"Outcome"}],"tags":[],"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["15"],"id":"33","language":"en","name":"Enough money","steps":[{"astNodeIds":["11"],"id":"29","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["12"],"id":"30","text":"there are chocolate bars in stock","type":"Context"},{"astNodeIds":["13"],"id":"31","text":"the customer tries to buy a 75 cent chocolate bar","type":"Action"},{"astNodeIds":["14"],"id":"32","text":"the sale should happen","type":"Outcome"}],"tags":[],"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["21"],"id":"38","language":"en","name":"No chocolates left","steps":[{"astNodeIds":["17"],"id":"34","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["18"],"id":"35","text":"there are no chocolate bars in stock","type":"Context"},{"astNodeIds":["19"],"id":"36","text":"the customer tries to buy a 1 cent chocolate bar","type":"Action"},{"astNodeIds":["20"],"id":"37","text":"the sale should not happen","type":"Outcome"}],"tags":[{"astNodeId":"22","name":"@some-tag"}],"uri":"samples/rules/rules.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the customer has {int} cents","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"there are chocolate bars in stock","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"there are no chocolate bars in stock","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"the customer tries to buy a {int} cent chocolate bar","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"the sale should not happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"5","pattern":{"source":"the sale should happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":26},"uri":"samples/rules/rules.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"43","pickleId":"28","testSteps":[{"id":"39","pickleStepId":"24","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"40","pickleStepId":"25","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"41","pickleStepId":"26","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"125"},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"27","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"48","pickleId":"33","testSteps":[{"id":"44","pickleStepId":"29","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"45","pickleStepId":"30","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"46","pickleStepId":"31","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"75"},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"32","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"53","pickleId":"38","testSteps":[{"id":"49","pickleStepId":"34","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"50","pickleStepId":"35","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"51","pickleStepId":"36","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"37","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"43","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"39","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"40","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"41","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"41","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"42","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"55","testCaseId":"48","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"45","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"46","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"53","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"50","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"51","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"52","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":31000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs new file mode 100644 index 000000000..1362f1d5f --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs @@ -0,0 +1,37 @@ +using Reqnroll; +using Reqnroll.UnitTestProvider; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.skipped +{ + [Binding] + internal class Skipped + { + private IUnitTestRuntimeProvider _unitTestRuntimeProvider; + + public Skipped(IUnitTestRuntimeProvider unitTestRuntimeProvider) + { + _unitTestRuntimeProvider = unitTestRuntimeProvider; + } + + [Given("a step that does not skip")] + public void GivenAStepThatDoesNotSkip() + { + } + + [Given("a step that is skipped")] + public void GivenAStepThatSkips() + { + } + + [Given("I skip a step")] + public void GivenISkipAStep() + { + _unitTestRuntimeProvider.TestIgnore("Skipped"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature new file mode 100644 index 000000000..e73a7f8af --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature @@ -0,0 +1,19 @@ +Feature: Skipping scenarios + + Hooks and step definitions are able to signal at runtime that the scenario should + be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED). + + This can be useful in certain situations e.g. the current environment doesn't have + the right conditions for running a particular scenario. + + @ignore + Scenario: Skipping from a Before hook + Given a step that is skipped + + Scenario: Skipping from a step doesn't affect the previous steps + Given a step that does not skip + And I skip a step + + Scenario: Skipping from a step causes the rest of the scenario to be skipped + Given I skip a step + And a step that is skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson new file mode 100644 index 000000000..746921a8b --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson @@ -0,0 +1,33 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Skipping scenarios\n\n Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.\n\n @skip\n Scenario: Skipping from a Before hook\n Given a step that is skipped\n\n Scenario: Skipping from a step doesn't affect the previous steps\n Given a step that does not skip\n And I skip a step\n\n Scenario: Skipping from a step causes the rest of the scenario to be skipped\n Given I skip a step\n And a step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/skipped/skipped.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Skipping from a Before hook","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"a step that is skipped"}],"tags":[{"id":"5","location":{"column":3,"line":9},"name":"@skip"}]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":13},"name":"Skipping from a step doesn't affect the previous steps","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":14},"text":"a step that does not skip"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":15},"text":"I skip a step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":17},"name":"Skipping from a step causes the rest of the scenario to be skipped","steps":[{"id":"10","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":18},"text":"I skip a step"},{"id":"11","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":19},"text":"a step that is skipped"}],"tags":[]}}],"description":" Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Skipping scenarios","tags":[]},"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["6"],"id":"14","language":"en","name":"Skipping from a Before hook","steps":[{"astNodeIds":["4"],"id":"13","text":"a step that is skipped","type":"Context"}],"tags":[{"astNodeId":"5","name":"@skip"}],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["9"],"id":"17","language":"en","name":"Skipping from a step doesn't affect the previous steps","steps":[{"astNodeIds":["7"],"id":"15","text":"a step that does not skip","type":"Context"},{"astNodeIds":["8"],"id":"16","text":"I skip a step","type":"Context"}],"tags":[],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"20","language":"en","name":"Skipping from a step causes the rest of the scenario to be skipped","steps":[{"astNodeIds":["10"],"id":"18","text":"I skip a step","type":"Context"},{"astNodeIds":["11"],"id":"19","text":"a step that is skipped","type":"Context"}],"tags":[],"uri":"samples/skipped/skipped.feature"}} +{"stepDefinition":{"id":"1","pattern":{"source":"a step that does not skip","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/skipped/skipped.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"a step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/skipped/skipped.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"I skip a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":15},"uri":"samples/skipped/skipped.feature.ts"}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":3},"uri":"samples/skipped/skipped.feature.ts"},"tagExpression":"@skip"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"23","pickleId":"14","testSteps":[{"hookId":"0","id":"21"},{"id":"22","pickleStepId":"13","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"26","pickleId":"17","testSteps":[{"id":"24","pickleStepId":"15","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"16","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"29","pickleId":"20","testSteps":[{"id":"27","pickleStepId":"18","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"28","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"30","testCaseId":"23","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"21","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"22","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"22","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"31","testCaseId":"26","timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"25","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"25","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"32","testCaseId":"29","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"27","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"27","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"28","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"28","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"nanos":18000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":19000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs new file mode 100644 index 000000000..b112fcaa9 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs @@ -0,0 +1,23 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.undefined +{ + [Binding] + internal class Undefined + { + [Given("an implemented step")] + public void GivenAnImplementedStep() + { + } + + [Given("a step that will be skipped")] + public void GivenAStepThatWillBeSkipped() + { + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature new file mode 100644 index 000000000..318ebea97 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature @@ -0,0 +1,16 @@ +Feature: Undefined steps + + At runtime, Cucumber may encounter a step in a scenario that it cannot match to a + step definition. In these cases, the scenario is not able to run and so the step status + will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE + + Scenario: An undefined step causes a failure + Given a step that is yet to be defined + + Scenario: Steps before undefined steps are executed + Given an implemented step + And a step that is yet to be defined + + Scenario: Steps after undefined steps are skipped + Given a step that is yet to be defined + And a step that will be skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson new file mode 100644 index 000000000..f15440837 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson @@ -0,0 +1,29 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Undefined steps\n\n At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE\n\n Scenario: An undefined step causes a failure\n Given a step that is yet to be defined\n\n Scenario: Steps before undefined steps are executed\n Given an implemented step\n And a step that is yet to be defined\n\n Scenario: Steps after undefined steps are skipped\n Given a step that is yet to be defined\n And a step that will be skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/undefined/undefined.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":7},"name":"An undefined step causes a failure","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":8},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Steps before undefined steps are executed","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"an implemented step"},{"id":"5","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":12},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":14},"name":"Steps after undefined steps are skipped","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":15},"text":"a step that is yet to be defined"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":16},"text":"a step that will be skipped"}],"tags":[]}}],"description":" At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Undefined steps","tags":[]},"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["3"],"id":"11","language":"en","name":"An undefined step causes a failure","steps":[{"astNodeIds":["2"],"id":"10","text":"a step that is yet to be defined","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["6"],"id":"14","language":"en","name":"Steps before undefined steps are executed","steps":[{"astNodeIds":["4"],"id":"12","text":"an implemented step","type":"Context"},{"astNodeIds":["5"],"id":"13","text":"a step that is yet to be defined","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["9"],"id":"17","language":"en","name":"Steps after undefined steps are skipped","steps":[{"astNodeIds":["7"],"id":"15","text":"a step that is yet to be defined","type":"Context"},{"astNodeIds":["8"],"id":"16","text":"a step that will be skipped","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"an implemented step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/undefined/undefined.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"a step that will be skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/undefined/undefined.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"19","pickleId":"11","testSteps":[{"id":"18","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"22","pickleId":"14","testSteps":[{"id":"20","pickleStepId":"12","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"21","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"25","pickleId":"17","testSteps":[{"id":"23","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"24","pickleStepId":"16","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"26","testCaseId":"19","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"26","testStepId":"18","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"26","testStepId":"18","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"26","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"22","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"20","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"20","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"21","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"21","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"25","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"23","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"23","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"24","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"24","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs new file mode 100644 index 000000000..15dacff91 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs @@ -0,0 +1,23 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.unknown_parameter_type +{ + [Binding] + internal class Unknown_parameter_type + { + [Given("{airport} is closed because of a strike")] + public void GivenAirportIsClosedBecauseOfStrike(Airport airport) + { + throw new Exception("Should not be called because airport parameter type has not been defined"); + } + } + + public class Airport + { + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature new file mode 100644 index 000000000..4ce05c974 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature @@ -0,0 +1,6 @@ +Feature: Parameter Types + Cucumber will generate an error message if a step definition registers + an unknown parameter type, but the suite will run. + + Scenario: undefined parameter type + Given CDG is closed because of a strike diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson new file mode 100644 index 000000000..cc9886734 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"1","keyword":"Scenario","location":{"column":3,"line":5},"name":"undefined parameter type","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":6},"text":"CDG is closed because of a strike"}],"tags":[]}}],"description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"pickle":{"astNodeIds":["1"],"id":"3","language":"en","name":"undefined parameter type","steps":[{"astNodeIds":["0"],"id":"2","text":"CDG is closed because of a strike","type":"Context"}],"tags":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"undefinedParameterType":{"expression":"{airport} is closed because of a strike","name":"airport"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"5","pickleId":"3","testSteps":[{"id":"4","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCaseStarted":{"attempt":0,"id":"6","testCaseId":"5","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"6","testStepId":"4","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"6","testStepId":"4","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"6","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7bc6963ae..2e92ffc88 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -26,31 +26,29 @@ Given I have a passing step [TestMethod] [DataRow("minimal")] + [DataRow("cdata")] + [DataRow("pending")] + [DataRow("examples-tables")] + [DataRow("data-tables")] + [DataRow("parameter-types")] + [DataRow("skipped")] + [DataRow("undefined")] + [DataRow("unknown-parameter-type")] + [DataRow("rules")] public void CCKPassingScenarios(string scenarioName) { AddCucumberMessagePlugIn(); - AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - [TestMethod] - [DataRow("pending")] - public void CCKPendingScenarios(string scenarioName) - { - AddCucumberMessagePlugIn(); + scenarioName = scenarioName.Replace("-", "_"); AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); - ShouldAllScenariosPend(); + ConfirmAllTestsRan(null); } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 4464aac06..8ba25ff8d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,24 +10,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0209bfed8eded8668a1f0117c88012a75ce4abb3 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:48:13 -0500 Subject: [PATCH 023/218] Added Attachments and Hooks bindings. Needs additional work to properly handle binary files. --- .../CCK/attachments/attachments.cs | 19 +++++++ .../CCK/hooks/hooks.cs | 51 +++++++++++++++++++ .../CucumberCompatibilityTestBase.cs | 18 +++++++ .../CucumberCompatibilityTests.cs | 9 +++- ...CucumberMessages.CompatibilityTests.csproj | 4 ++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs new file mode 100644 index 000000000..877a2594d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.attachments +{ + [Binding] + internal class Attachments + { + [When(@"a JPEG image is attached")] + public void WhenAJPEGImageIsAttached() + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs new file mode 100644 index 000000000..117f2cccd --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -0,0 +1,51 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.hooks +{ + [Binding] + internal class Hooks + { + private readonly IReqnrollOutputHelper reqnrollOutputHelper; + + public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) + { + this.reqnrollOutputHelper = reqnrollOutputHelper; + } + + [When("a step passes")] + public void AStepPasses() + { + } + + [When("a step fails")] + public void AStepFails() + { + throw new Exception("Exception in step"); + } + + // When a step does not exist - no implementation should be generated + + // Hook implementations + [BeforeScenario] + [AfterScenario] + public void ScenarioHook() { } + + [AfterScenario(new string[] { "some-tag", "some-other-tag" })] + public void FailingAfterHook() + { + throw new Exception("Exception in conditional hook"); + } + + [AfterScenario("with-attachment")] + public void PassingAfterHook() + { + reqnrollOutputHelper.AddAttachment("cucumber.svg"); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 419805f39..1f2e26629 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -28,5 +28,23 @@ protected void ShouldAllScenariosPend(int? expectedNrOfTestsSpec = null) int expectedNrOfTests = ConfirmAllTestsRan(expectedNrOfTestsSpec); _vsTestExecutionDriver.LastTestExecutionResult.Pending.Should().Be(expectedNrOfTests, "all tests should pend"); } + + protected void AddBinaryFilesFromResource(string scenarioName, string prefix, Assembly assembly) + { + foreach (var fileName in GetTestBinaryFileNames(scenarioName, prefix, assembly)) + { + var content = _testFileManager.GetTestFileContent(fileName, $"{prefix}.{scenarioName}", assembly); + _projectsDriver.AddFile(fileName, content); + } + } + + protected IEnumerable GetTestBinaryFileNames(string scenarioName, string prefix, Assembly? assembly) + { + var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); + string prefixToRemove = $"{prefix}.{scenarioName}."; + return testAssembly.GetManifestResourceNames() + .Where(rn => !rn.EndsWith(".feature") && !rn.EndsWith(".cs") && !rn.EndsWith(".feature.ndjson") && rn.StartsWith(prefixToRemove)) + .Select(rn => rn.Substring(prefixToRemove.Length)); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 2e92ffc88..29804c65c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -25,10 +25,12 @@ Given I have a passing step } [TestMethod] + //[DataRow("attachments")] [DataRow("minimal")] [DataRow("cdata")] [DataRow("pending")] [DataRow("examples-tables")] + [DataRow("hooks")] [DataRow("data-tables")] [DataRow("parameter-types")] [DataRow("skipped")] @@ -43,12 +45,15 @@ public void CCKPassingScenarios(string scenarioName) AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); ConfirmAllTestsRan(null); + if (scenarioName == "attachments") + { + ShouldAllScenariosPass(); + } } - - } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 8ba25ff8d..ee82ca5de 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,9 +10,11 @@ + + @@ -54,6 +56,7 @@ + @@ -69,6 +72,7 @@ + From 1bea51e369602ecaff47d867450c756d0d30e78e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:06:56 -0500 Subject: [PATCH 024/218] Added Hook support. Fails on undefined steps (but not specific to Hooks). Made change to TestExecutionEngine to add Feature, Scenario, and Step context to the HookBinding events. --- .../CucumberMessageFactory.cs | 119 +++++++++++++----- .../CucumberMessagePublisher.cs | 70 +++++------ ...atureState.cs => FeatureEventProcessor.cs} | 104 +++++++++++---- .../CucumberMesssages/HookStepProcessor.cs | 24 ++++ Reqnroll/CucumberMesssages/IStepProcessor.cs | 10 ++ .../{StepState.cs => PickleStepProcessor.cs} | 24 ++-- ...arioState.cs => ScenarioEventProcessor.cs} | 43 +++++-- .../CucumberMesssages/StepProcessorBase.cs | 23 ++++ Reqnroll/Events/ExecutionEvent.cs | 7 ++ .../Infrastructure/TestExecutionEngine.cs | 2 +- .../CCK/hooks/hooks.cs | 9 +- 11 files changed, 310 insertions(+), 125 deletions(-) rename Reqnroll/CucumberMesssages/{FeatureState.cs => FeatureEventProcessor.cs} (53%) create mode 100644 Reqnroll/CucumberMesssages/HookStepProcessor.cs create mode 100644 Reqnroll/CucumberMesssages/IStepProcessor.cs rename Reqnroll/CucumberMesssages/{StepState.cs => PickleStepProcessor.cs} (74%) rename Reqnroll/CucumberMesssages/{ScenarioState.cs => ScenarioEventProcessor.cs} (60%) create mode 100644 Reqnroll/CucumberMesssages/StepProcessorBase.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 8974ba766..1358dc745 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -11,23 +11,34 @@ namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(FeatureState featureState, FeatureStartedEvent featureStartedEvent) + public static TestRunStarted ToTestRunStarted(FeatureEventProcessor featureState, FeatureStartedEvent featureStartedEvent) { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } - public static TestRunFinished ToTestRunFinished(FeatureState featureState, FeatureFinishedEvent featureFinishedEvent) + public static TestRunFinished ToTestRunFinished(FeatureEventProcessor featureState, FeatureFinishedEvent featureFinishedEvent) { return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); } - internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); foreach (var stepState in scenarioState.Steps) { - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState); - testSteps.Add(testStep); + switch (stepState) + { + case PickleStepProcessor _: + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as PickleStepProcessor); + testSteps.Add(testStep); + break; + case HookStepProcessor _: + var hookTestStep = CucumberMessageFactory.ToHookTestStep( stepState as HookStepProcessor); + testSteps.Add(hookTestStep); + break; + default: + throw new NotImplementedException(); + } } var testCase = new TestCase ( @@ -37,11 +48,11 @@ internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStarted ); return testCase; } - internal static TestCaseStarted ToTestCaseStarted(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCaseStarted ToTestCaseStarted(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) { return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); } - internal static TestCaseFinished ToTestCaseFinished(ScenarioState scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) { return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); } @@ -51,12 +62,7 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, var expressionType = binding.ExpressionType; var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); - - var methodName = binding.Method.Name; - var className = binding.Method.Type.Name; - var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); - var methodDescription = new JavaMethod(className, className, paramTypes); - var sourceRef = SourceReference.Create(methodDescription); + SourceReference sourceRef = ToSourceRef(binding); var result = new StepDefinition ( @@ -67,21 +73,31 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } - internal static TestStep ToTestStep(ScenarioState scenarioState, StepState stepState) + private static SourceReference ToSourceRef(IBinding binding) + { + var methodName = binding.Method.Name; + var className = binding.Method.Type.Name; + var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); + var methodDescription = new JavaMethod(className, methodName, paramTypes); + var sourceRef = SourceReference.Create(methodDescription); + return sourceRef; + } + + internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) { //TODO: This only works if the step is properly bound. Need to determine what to do otherwise - var args = stepState.StepArguments - .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) - .ToList(); + var args = stepState.StepArguments + .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) + .ToList(); - var result = new TestStep( - null, - stepState.TestStepID, - stepState.PickleStepID, - new List { stepState.StepDefinitionId }, - new List { new StepMatchArgumentsList(args) } - ); + var result = new TestStep( + null, + stepState.TestStepID, + stepState.PickleStepID, + new List { stepState.StepDefinitionId }, + new List { new StepMatchArgumentsList(args) } + ); return result; } @@ -96,7 +112,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), argument.Type); } - internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -104,16 +120,49 @@ internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStart Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(StepState stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(PickleStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, stepState.TestStepID, - ToTestStepResult(stepState, stepFinishedEvent), + ToTestStepResult(stepState), Converters.ToTimestamp(stepFinishedEvent.Timestamp)); } - private static TestStepResult ToTestStepResult(StepState stepState, StepFinishedEvent stepFinishedEvent) + internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) + { + SourceReference sourceRef = ToSourceRef(hookBinding); + + var result = new Hook + ( + iDGenerator.GetNewId(), + null, + sourceRef, + hookBinding.IsScoped ? hookBinding.BindingScope.Tag : null + ); + return result; + } + + internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) + { + // find the Hook message at the Feature level + var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); + var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; + + return new TestStep(hookId, hookStepState.TestStepID, null, new List(), new List()); + } + internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + { + return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + } + + internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + { + return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + } + + + private static TestStepResult ToTestStepResult(StepProcessorBase stepState) { return new TestStepResult( Converters.ToDuration(stepState.Duration), @@ -141,11 +190,21 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat // utility methods public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { - - var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + string signature = GenerateSignature(stepDefinition); return $"{stepDefinition.SourceExpression}({signature})"; } + public static string CanonicalizeHookBinding(IHookBinding hookBinding) + { + string signature = GenerateSignature(hookBinding); + return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + } + + private static string GenerateSignature(IBinding stepDefinition) + { + return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5f415cabf..6915f60d1 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -22,7 +22,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private ConcurrentDictionary featureStatesByFeatureName = new(); + private ConcurrentDictionary featureProcessorsByFeatureName = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -50,22 +50,24 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - var featureState = new FeatureState + var featureProc = new FeatureEventProcessor { Name = featureName, Enabled = enabled }; - if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) { // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + var featureState_alreadyrunning = featureProcessorsByFeatureName[featureName]; featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well // None of the rest of this method should be executed @@ -78,41 +80,35 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(featureStartedEvent, featureName); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; + var featureProcessor = featureProcessorsByFeatureName[featureName]; - lock (featureState) + lock (featureProcessor) { // Remove the worker thread marker for this thread - featureState.workerThreadMarkers.TryPop(out int result); + featureProcessor.workerThreadMarkers.TryPop(out int result); // Check if there are other threads still working on this feature - if (featureState.workerThreadMarkers.TryPeek(out result)) + if (featureProcessor.workerThreadMarkers.TryPeek(out result)) { // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet return; } - featureState.Finished = true; + featureProcessor.Finished = true; } - if (!featureState.Enabled) + if (!featureProcessor.Enabled) return; - foreach (Envelope e in featureState.ProcessEvent(featureFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(featureFinishedEvent, featureName); - foreach (var message in featureState.Messages) + foreach (var message in featureProcessor.Messages) { broker.Publish(message); } @@ -123,43 +119,37 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - - foreach (Envelope e in featureState.ProcessEvent(scenarioStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(scenarioStartedEvent, featureName); } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(scenarioFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(scenarioFinishedEvent, featureName); } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(stepStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(stepStartedEvent, featureName); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(stepFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(stepFinishedEvent, featureName); } + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) + { + var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookBindingEvent, featureName); + } + private void ProcessEvent(ExecutionEvent anEvent, string featureName) + { + var featureProcessor = featureProcessorsByFeatureName[featureName]; + featureProcessor.ProcessEvent(anEvent); + } } } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs similarity index 53% rename from Reqnroll/CucumberMesssages/FeatureState.cs rename to Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 7357d780a..e30faf089 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -12,7 +12,7 @@ namespace Reqnroll.CucumberMesssages { - public class FeatureState + public class FeatureEventProcessor { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled @@ -26,24 +26,56 @@ public bool Success { get { - return Enabled && Finished && ScenarioName2StateMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK) ; + return Enabled && Finished && ScenarioName2ScenarioProcessorMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); } } - //ID Generator to use when generating IDs for TestCase messages and beyond + // ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID - // otherwise we'll use a GUID ID generator + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } //Lookup tables - public Dictionary StepDefinitionsByPattern = new(); - public Dictionary PicklesByScenarioName = new(); - - public Dictionary ScenarioName2StateMap = new(); - + // + // These three dictionaries hold the mapping of steps, hooks, and pickles to their IDs + // These should only be produced by the first FeatureStartedEvent that this FeatureEventProcessor receives (it might receive multiple if the scenario is run concurrently) + // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once + public ConcurrentDictionary StepDefinitionsByPattern = new(); + public ConcurrentDictionary HookDefinitionsByPattern = new(); + public ConcurrentDictionary PicklesByScenarioName = new(); + + // Scenario event processors by scenario name; + public Dictionary ScenarioName2ScenarioProcessorMap = new(); + + // The list of Cucumber Messages that are ready to be sent to the broker for distribution to consumers public ConcurrentQueue Messages = new(); + + // A set of markers that represent the worker threads that are currently processing events for this feature. + // Once the last worker thread marker is removed, the Messages are then sent to the broker public ConcurrentStack workerThreadMarkers = new(); + internal void ProcessEvent(ExecutionEvent anEvent) + { + foreach (Envelope e in DispatchEvent(anEvent)) + { + Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = Name, Envelope = e }); + } + } + private IEnumerable DispatchEvent(ExecutionEvent anEvent) + { + return anEvent switch + { + FeatureStartedEvent featureStartedEvent => ProcessEvent(featureStartedEvent), + FeatureFinishedEvent featureFinishedEvent => ProcessEvent(featureFinishedEvent), + ScenarioStartedEvent scenarioStartedEvent => ProcessEvent(scenarioStartedEvent), + ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), + StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), + StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), + HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + _ => throw new NotImplementedException(), + }; + } + internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { yield return Envelope.Create(new Meta( @@ -72,7 +104,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var pickle in pickles) { - PicklesByScenarioName.Add(pickle.Name, pickle); + PicklesByScenarioName.TryAdd(pickle.Name, pickle); yield return Envelope.Create(pickle); } @@ -83,17 +115,24 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + HookDefinitionsByPattern.TryAdd(hookId, hook.Id); + yield return Envelope.Create(hook); + } } yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); } - internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) { yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); @@ -102,26 +141,21 @@ internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinished internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, this); - ScenarioName2StateMap.Add(scenarioName, scenarioState); + var scenarioEP = new ScenarioEventProcessor(scenarioStartedEvent.ScenarioContext, this); + ScenarioName2ScenarioProcessorMap.Add(scenarioName, scenarioEP); - foreach (var e in scenarioState.ProcessEvent(scenarioStartedEvent)) + foreach (var e in scenarioEP.ProcessEvent(scenarioStartedEvent)) { yield return e; } } - private string ExtractLastID(List pickles) - { - return pickles.Last().Id; - } - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(scenarioFinishedEvent)) { yield return e; } @@ -130,9 +164,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(stepStartedEvent)) + foreach (var e in scenarioEP.ProcessEvent(stepStartedEvent)) { yield return e; } @@ -141,12 +175,30 @@ internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(stepFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(stepFinishedEvent)) { yield return e; } } + + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + { + var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + { + yield return e; + } + + } + + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs new file mode 100644 index 000000000..a2383daf6 --- /dev/null +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -0,0 +1,24 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + public class HookStepProcessor : StepProcessorBase + { + public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } + public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + { + } + + public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + { + HookBindingFinishedEvent = hookFinishedEvent; + TestStepID = parentScenario.IdGenerator.GetNewId(); + return Enumerable.Empty(); + } + } + + +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMesssages/IStepProcessor.cs new file mode 100644 index 000000000..667d02467 --- /dev/null +++ b/Reqnroll/CucumberMesssages/IStepProcessor.cs @@ -0,0 +1,10 @@ +namespace Reqnroll.CucumberMesssages +{ + public interface IStepProcessor + { + public string TestStepID { get; set; } + + } + + +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/StepState.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs similarity index 74% rename from Reqnroll/CucumberMesssages/StepState.cs rename to Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 96a5360da..1ae523a10 100644 --- a/Reqnroll/CucumberMesssages/StepState.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -2,7 +2,6 @@ using Reqnroll.Assist; using Reqnroll.Bindings; using Reqnroll.Events; -using System; using System.Collections.Generic; using System.Linq; @@ -14,20 +13,14 @@ public class StepArgument public string Type; } - public class StepState + public class PickleStepProcessor : StepProcessorBase { - private ScenarioState scenarioState; private StepStartedEvent stepStartedEvent; - public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStartedEvent) + public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { - scenarioState = parentScenarioState; - this.stepStartedEvent = stepStartedEvent; - } - public string TestStepID { get; set; } - public string TestCaseStartedID => scenarioState.TestCaseStartedID; public string PickleStepID { get; set; } public bool Bound { get; set; } public string CanonicalizedStepPattern { get; set; } @@ -35,25 +28,22 @@ public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStarted public IStepDefinitionBinding StepDefinition { get; set; } public StepArgument[] StepArguments { get; set; } - public TimeSpan Duration { get; set; } - public ScenarioExecutionStatus Status { get; set; } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { - TestStepID = scenarioState.IdGenerator.GetNewId(); + this.stepStartedEvent = stepStartedEvent; + TestStepID = parentScenario.IdGenerator.GetNewId(); return Enumerable.Empty(); } private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) { - return scenarioState.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + return parentScenario.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; } private string FindPickleStepIDByStepText(string stepText) { - return scenarioState.FeatureState.PicklesByScenarioName[scenarioState.Name].Steps.Where(st => st.Text == stepText).First().Id; + return parentScenario.FeatureState.PicklesByScenarioName[parentScenario.Name].Steps.Where(st => st.Text == stepText).First().Id; } internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) @@ -81,4 +71,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + + } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs similarity index 60% rename from Reqnroll/CucumberMesssages/ScenarioState.cs rename to Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index bd0189f26..cd6a4c54b 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -9,10 +9,10 @@ namespace Reqnroll.CucumberMesssages { - public class ScenarioState + public class ScenarioEventProcessor { internal readonly IIdGenerator IdGenerator; - internal readonly FeatureState FeatureState; + internal readonly FeatureEventProcessor FeatureState; public string TestCaseStartedID; public string Name { get; set; } @@ -23,17 +23,17 @@ public class ScenarioState // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); - public Dictionary StepsByEvent { get; set; } = new(); - public List Steps + public Dictionary StepsByEvent { get; private set; } = new(); + public List Steps { get { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Select(kvp => kvp.Value).ToList(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value ).ToList(); } } public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } - public ScenarioState(IScenarioContext context, FeatureState featureState) + public ScenarioEventProcessor(IScenarioContext context, FeatureEventProcessor featureState) { IdGenerator = featureState.IDGenerator; FeatureState = featureState; @@ -51,11 +51,24 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + _events.Enqueue(hookBindingFinishedEvent); + var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingFinishedEvent); + StepsByEvent.Add(hookBindingFinishedEvent, step); + return Enumerable.Empty(); + } + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); - var stepState = new StepState(this, stepStartedEvent); + var stepState = new PickleStepProcessor(this); StepsByEvent.Add(stepStartedEvent, stepState); stepState.ProcessEvent(stepStartedEvent); @@ -65,13 +78,18 @@ internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { _events.Enqueue(stepFinishedEvent); - var stepState = StepsByEvent.Values.Last(); + var stepState = FindMatchingStepStartEvent(stepFinishedEvent); stepState.ProcessEvent(stepFinishedEvent); StepsByEvent.Add(stepFinishedEvent, stepState); return Enumerable.Empty(); } + private PickleStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as PickleStepProcessor).LastOrDefault(); + } + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); @@ -92,11 +110,16 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState, stepStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as PickleStepProcessor, stepStartedEvent)); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState, stepFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as PickleStepProcessor, stepFinishedEvent)); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + var hookStepState = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); break; // add more cases for other event types default: diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs new file mode 100644 index 000000000..b348987ed --- /dev/null +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Reqnroll.CucumberMesssages +{ + public class StepProcessorBase : IStepProcessor + { + public string TestStepID { get; set; } + public string TestCaseStartedID => parentScenario.TestCaseStartedID; + public ScenarioExecutionStatus Status { get; set; } + public TimeSpan Duration { get; set; } + + + + public ScenarioEventProcessor parentScenario; + + public StepProcessorBase(ScenarioEventProcessor parentScenario) + { + this.parentScenario = parentScenario; + } + } + + +} \ No newline at end of file diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index cc6ab3742..60944380d 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -1,5 +1,6 @@ using System; using Reqnroll.Bindings; +using Reqnroll.Infrastructure; namespace Reqnroll.Events { @@ -182,12 +183,18 @@ public class HookBindingFinishedEvent : ExecutionEvent public IHookBinding HookBinding { get; } public TimeSpan Duration { get; } + public IContextManager ContextManager { get; private set; } public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { HookBinding = hookBinding; Duration = duration; } + + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager) : this(hookBinding, duration) + { + ContextManager = contextManager; + } } public interface IExecutionOutputEvent diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index c0af86828..f4996f6b5 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -362,7 +362,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager)); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 117f2cccd..7f2497446 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -32,10 +32,15 @@ public void AStepFails() // Hook implementations [BeforeScenario] + public void BeforeScenarioHook() { } + + [BeforeScenario()] + public void NamedBeforeHook() { } + [AfterScenario] - public void ScenarioHook() { } + public void AfterScenarioHook() { } - [AfterScenario(new string[] { "some-tag", "some-other-tag" })] + [AfterScenario("some-tag or some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); From 704a7dde4e22bbf56997c30544d68a247fba178a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:57:56 -0500 Subject: [PATCH 025/218] Fixed how Undefined steps are handled in the StepEventProcessor and in how they are rendered as TestStep messages. --- .../CucumberMessageFactory.cs | 15 ++++++---- .../CucumberMessagePublisher.cs | 2 ++ .../CucumberMesssages/PickleStepProcessor.cs | 30 ++++++++++--------- .../CucumberCompatibilityTests.cs | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 1358dc745..524f036e2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -85,7 +85,7 @@ private static SourceReference ToSourceRef(IBinding binding) internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) { - //TODO: This only works if the step is properly bound. Need to determine what to do otherwise + bool bound = stepState.StepDefinitionId != null; var args = stepState.StepArguments .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) @@ -95,9 +95,9 @@ internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, Pickle null, stepState.TestStepID, stepState.PickleStepID, - new List { stepState.StepDefinitionId }, - new List { new StepMatchArgumentsList(args) } - ); + bound ? new List { stepState.StepDefinitionId } : new List(), + bound ? new List { new StepMatchArgumentsList(args) } : new List() + ); return result; } @@ -149,7 +149,12 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; - return new TestStep(hookId, hookStepState.TestStepID, null, new List(), new List()); + return new TestStep( + hookId, + hookStepState.TestStepID, + null, + new List(), + new List()); } internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 6915f60d1..b17a13702 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -15,6 +15,7 @@ using System.Reflection; using System.Collections.Concurrent; using System.Linq; +using System.Diagnostics; namespace Reqnroll.CucumberMesssages { @@ -30,6 +31,7 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + //Debugger.Launch(); runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 1ae523a10..57cb04674 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -27,7 +27,7 @@ public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(pa public string StepDefinitionId { get; private set; } public IStepDefinitionBinding StepDefinition { get; set; } - public StepArgument[] StepArguments { get; set; } + public List StepArguments { get; set; } internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { @@ -48,29 +48,31 @@ private string FindPickleStepIDByStepText(string stepText) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { - StepDefinition = stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding; - Bound = !(StepDefinition == null || StepDefinition == BindingMatch.NonMatching); + var bindingMatch = stepFinishedEvent.StepContext?.StepInfo?.BindingMatch; + Bound = !(bindingMatch == null || bindingMatch == BindingMatch.NonMatching); - if (Bound) - { - CanonicalizedStepPattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition); - StepDefinitionId = FindStepDefIDByStepPattern(CanonicalizedStepPattern); + StepDefinition = Bound ? bindingMatch.StepBinding : null; + CanonicalizedStepPattern = Bound ? CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition) : ""; + StepDefinitionId = Bound ? FindStepDefIDByStepPattern(CanonicalizedStepPattern) : null; - PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); + PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); - Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; - Status = stepFinishedEvent.StepContext.Status; + Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; + Status = stepFinishedEvent.StepContext.Status; - StepArguments = stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument + StepArguments = Bound ? + stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument { Value = arg.ToString(), Type = arg.GetType().Name - }).ToArray(); - } + }).ToList() + : Enumerable.Empty().ToList(); + + return Enumerable.Empty(); } } - + } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 29804c65c..0a26ec531 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -37,7 +37,7 @@ Given I have a passing step [DataRow("undefined")] [DataRow("unknown-parameter-type")] [DataRow("rules")] - public void CCKPassingScenarios(string scenarioName) + public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); From 3aa4cef9473ca03c8a567d3aa9f83d8b0c8fae8a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:42:39 -0500 Subject: [PATCH 026/218] Moved NDJSON serialization into Reqnroll from the File Plugin making it centrally available. NdJson files in CCK tests were moved out of assembly resource to be plain project content files. --- .../CucumberMessagEnumConverter.cs | 0 .../CucumberMesssages}/NdjsonSerializer.cs | 0 .../CucumberCompatibilityTestBase.cs | 6 +++ .../CucumberCompatibilityTests.cs | 13 ++++- ...CucumberMessages.CompatibilityTests.csproj | 48 ++++++++++++++----- 5 files changed, 54 insertions(+), 13 deletions(-) rename {Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Reqnroll/CucumberMesssages}/CucumberMessagEnumConverter.cs (100%) rename {Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Reqnroll/CucumberMesssages}/NdjsonSerializer.cs (100%) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs similarity index 100% rename from Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs similarity index 100% rename from Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs rename to Reqnroll/CucumberMesssages/NdjsonSerializer.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 1f2e26629..b40613b5c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -46,5 +46,11 @@ protected IEnumerable GetTestBinaryFileNames(string scenarioName, string .Where(rn => !rn.EndsWith(".feature") && !rn.EndsWith(".cs") && !rn.EndsWith(".feature.ndjson") && rn.StartsWith(prefixToRemove)) .Select(rn => rn.Substring(prefixToRemove.Length)); } + + protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix, Assembly? assembly) + { + var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); + return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 0a26ec531..d1bda7c50 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,10 +1,13 @@ +using Cucumber.Messages; +using Io.Cucumber.Messages.Types; +using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using System.Reflection; namespace CucumberMessages.CompatibilityTests { [TestClass] - public class CucumberCompatibilitySmokeTest : CucumberCompatibilityTestBase + public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { [TestMethod] public void SmokeTest() @@ -55,5 +58,13 @@ public void CCKScenarios(string scenarioName) ShouldAllScenariosPass(); } } + + private IEnumerable GetExpectedResults(string scenarioName) + { + var workingDirectory = Assembly.GetExecutingAssembly().GetAssemblyLocation(); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory, $"{scenarioName}.feature.ndjson")); + + foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); + } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index ee82ca5de..25827ab34 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -58,44 +58,68 @@ - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + From 15cb91a8de62d6844a34171df50c9e81ed438f98 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:09:47 -0500 Subject: [PATCH 027/218] Configuration of File output driven by a json configuration file. --- .../CucumberMessages-config-schema.json | 34 +++++++++++ .../FileSinkConfiguration.cs | 35 +++++++++++ .../FileSinkPlugin.cs | 59 +++++++++++++------ .../CucumberCompatibilityTestBase.cs | 6 ++ .../CucumberCompatibilityTests.cs | 2 + ...CucumberMessages.CompatibilityTests.csproj | 8 ++- .../CucumberMessages.configuration.json | 10 ++++ 7 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json create mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json new file mode 100644 index 000000000..f7fcd8f17 --- /dev/null +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "filSinkEnabled": { + "type": "boolean", + "description": "Indicates whether the program is enabled or not" + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether this destination setting is enabled" + }, + "basePath": { + "type": "string", + "description": "The base path for output files" + }, + "outputDirectory": { + "type": "string", + "description": "The directory path where output should go" + } + }, + "required": [ "enabled", "basePath", "outputDirectory" ] + }, + "minItems": 1, + "description": "Array of destination settings" + } + }, + "required": [ "programEnabled", "destinations" ] +} \ No newline at end of file diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs new file mode 100644 index 000000000..7a56f4233 --- /dev/null +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 7a6165a15..8807fde2a 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -9,32 +9,39 @@ using System.Diagnostics; using Reqnroll.Events; using System.Collections.Concurrent; +using System.Text.Json; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - //TODO: Add support for Reqnroll Configuration to initialize the FileSinkPlugin by specifying the path to the base directory. - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; //Thread safe collections to hold: // 1. Inbound Cucumber Messages - BlockingCollection // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - + private object _lock = new(); private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); + private FileSinkConfiguration? configuration; + private string baseDirectory = ""; public FileSinkPlugin() { - //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + return; + + baseDirectory = ProcessConfiguration(configuration); + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => @@ -45,6 +52,30 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar }; } + private string ProcessConfiguration(FileSinkConfiguration configuration) + { + var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeDestination != null) + { + var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); + if (!Directory.Exists(basePath)) + { + lock(_lock) + { + if (!Directory.Exists(basePath)) + Directory.CreateDirectory(basePath); + } + } + + return basePath; + } + else + { + return Assembly.GetExecutingAssembly().Location; + } + } + private void CloseFileSink(TestRunFinishedEvent @event) { postedMessages.CompleteAdding(); @@ -54,13 +85,6 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - Console.WriteLine("LaunchFileSink called"); - - if (!Directory.Exists(baseDirectory)) - { - Directory.CreateDirectory(baseDirectory); - } - fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } @@ -71,17 +95,13 @@ public void Publish(ReqnrollCucumberMessage message) private async Task ConsumeAndWriteToFiles() { - Console.WriteLine("ConsumeAndWriteToFiles called"); - foreach (var message in postedMessages.GetConsumingEnumerable()) { var featureName = message.CucumberMessageSource; - Console.WriteLine("ConsumeAndWriteToFiles: " + featureName); if (message.Envelope != null) { var cm = Serialize(message.Envelope); - Console.WriteLine("ConsumeAndWriteToFiles: " + cm); await Write(featureName, cm); } else @@ -91,7 +111,6 @@ private async Task ConsumeAndWriteToFiles() } } - private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); private bool disposedValue; private string Serialize(Envelope message) @@ -103,7 +122,13 @@ private async Task Write(string featureName, string cucumberMessage) if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + lock (_lock) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + } } await fileStreams[featureName].WriteLineAsync(cucumberMessage); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index b40613b5c..0b3f95052 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -52,5 +52,11 @@ protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); } + + protected void CucumberMessagesAddConfigurationFile(string configFileName) + { + var configFileContent = File.ReadAllText(configFileName); + _projectsDriver.AddFile(configFileName, configFileContent); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d1bda7c50..ea9a43376 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -13,6 +13,7 @@ public class CucumberCompatibilityTests : CucumberCompatibilityTestBase public void SmokeTest() { AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" Feature: Cucumber Messages Smoke Test @@ -43,6 +44,7 @@ Given I have a passing step public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); scenarioName = scenarioName.Replace("-", "_"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 25827ab34..73668f893 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -142,4 +142,10 @@ + + + Always + + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json new file mode 100644 index 000000000..cb6aa6ecb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -0,0 +1,10 @@ +{ + "fileSinkEnabled": true, + "destinations": [ + { + "enabled": true, + "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", + "outputDirectory": "CucumberMessages" + } + ] +} \ No newline at end of file From f7302db4919d285fabbb584637716f744b6c20c1 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:19:12 -0500 Subject: [PATCH 028/218] Eliminated some tracing that is no longer useful for debugging. Clean up of source files. --- Reqnroll/CucumberMesssages/CucumberMessageBroker.cs | 2 -- Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs | 6 +----- Reqnroll/CucumberMesssages/PickleStepProcessor.cs | 4 ---- Reqnroll/CucumberMesssages/StepProcessorBase.cs | 4 ---- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 54411bd83..777cdee28 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -33,7 +33,6 @@ public void Publish(ReqnrollCucumberMessage message) //TODO: find a way to populate this list a single time var registeredSinks = _objectContainer.ResolveAll().ToList(); - _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); foreach (var sink in registeredSinks) { @@ -49,7 +48,6 @@ public void Complete(string cucumberMessageSource) var registeredSinks = _objectContainer.ResolveAll().ToList(); var _traceListener = _objectContainer.Resolve(); - _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); var completionMessage = new ReqnrollCucumberMessage { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 524f036e2..03018637c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -192,7 +192,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat } - // utility methods + #region utility methods public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { string signature = GenerateSignature(stepDefinition); @@ -210,6 +210,6 @@ private static string GenerateSignature(IBinding stepDefinition) { return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; } - + #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index b17a13702..1d335b97a 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -42,10 +42,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - - var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); - testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); @@ -77,7 +73,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) } var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); if (!enabled) return; diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 57cb04674..cfc86abfa 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -68,11 +68,7 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) }).ToList() : Enumerable.Empty().ToList(); - return Enumerable.Empty(); } - } - - } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index b348987ed..090b5055d 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -9,8 +9,6 @@ public class StepProcessorBase : IStepProcessor public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } - - public ScenarioEventProcessor parentScenario; public StepProcessorBase(ScenarioEventProcessor parentScenario) @@ -18,6 +16,4 @@ public StepProcessorBase(ScenarioEventProcessor parentScenario) this.parentScenario = parentScenario; } } - - } \ No newline at end of file From d010db077e2de039aa2ba8438fa6261e71ef06b6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:57:39 -0500 Subject: [PATCH 029/218] Upgrade CucumberMessages nuget package to 26.0.0 --- .../Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- Reqnroll/Reqnroll.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 7dbb46029..9624408fd 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,7 +10,7 @@ - + diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 583dd298c..d38ef61af 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,7 @@ - + From 98a17a99e4501386a09fde720bf16f8eaac40dc0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:48:22 -0500 Subject: [PATCH 030/218] Added FileExtension to MIMEType map for the attachment Added handling logic. --- .../FileExtensionToMIMETypeMap.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs new file mode 100644 index 000000000..c4deddb92 --- /dev/null +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public static class FileExtensionToMIMETypeMap + { + public static string GetMimeType(string extension) + { + if (ExtensionToMimeType.TryGetValue(extension, out var mimeType)) + return mimeType; + + return "application/octet-stream"; + } + + // Source of this list: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + public static readonly Dictionary ExtensionToMimeType = new Dictionary + { + {".aac", "audio/aac"}, + {".abw", "application/x-abiword"}, + {".apng", "image/apng"}, + {".arc", "application/x-freearc"}, + {".avif", "image/avif"}, + {".avi", "video/x-msvideo"}, + {".azw", "application/vnd.amazon.ebook"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".bz", "application/x-bzip"}, + {".bz2", "application/x-bzip2"}, + {".cda", "application/x-cdf"}, + {".csh", "application/x-csh"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".doc", "application/msword"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".eot", "application/vnd.ms-fontobject"}, + {".epub", "application/epub+zip"}, + {".gz", "application/gzip"}, + {".gif", "image/gif"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".ico", "image/vnd.microsoft.icon"}, + {".ics", "text/calendar"}, + {".jar", "application/java-archive"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "text/javascript"}, + {".json", "application/json"}, + {".jsonld", "application/ld+json"}, + {".mid", "audio/midi"}, + {".midi", "audio/midi"}, + {".mjs", "text/javascript"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mpeg", "video/mpeg"}, + {".mpkg", "application/vnd.apple.installer+xml"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".opus", "audio/ogg"}, + {".otf", "font/otf"}, + {".png", "image/png"}, + {".pdf", "application/pdf"}, + {".php", "application/x-httpd-php"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".rar", "application/vnd.rar"}, + {".rtf", "application/rtf"}, + {".sh", "application/x-sh"}, + {".svg", "image/svg+xml"}, + {".tar", "application/x-tar"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".ts", "video/mp2t"}, + {".ttf", "font/ttf"}, + {".txt", "text/plain"}, + {".vsd", "application/vnd.visio"}, + {".wav", "audio/wav"}, + {".weba", "audio/webm"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, + {".woff", "font/woff"}, + {".woff2", "font/woff2"}, + {".xhtml", "application/xhtml+xml"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xml", "application/xml"}, + {".xul", "application/vnd.mozilla.xul+xml"}, + {".zip", "application/zip"}, + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".7z", "application/x-7z-compressed"} + }; + } +} From 132dcb3f9731a51a3b4af6fead50f33683ba67db Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:18:59 -0500 Subject: [PATCH 031/218] AddAttachment and OutputHelper.WriteLine added with Message output as an 'Attachment'. --- .../AttachmentAddedEventWrapper.cs | 18 ++++ .../CucumberMessageFactory.cs | 58 +++++++++--- .../CucumberMessagePublisher.cs | 17 +++- .../CucumberMessageTransformer.cs | 4 +- .../FeatureEventProcessor.cs | 26 ++++++ .../FileExtensionToMIMETypeMap.cs | 2 +- .../CucumberMesssages/HookStepProcessor.cs | 9 +- .../OutputAddedEventWrapper.cs | 18 ++++ .../ScenarioEventProcessor.cs | 90 ++++++++++++++++--- ...pProcessor.cs => ScenarioStepProcessor.cs} | 4 +- Reqnroll/Events/ExecutionEvent.cs | 20 +++++ .../Infrastructure/ReqnrollOutputHelper.cs | 15 +++- .../CCK/attachments/attachments.cs | 36 +++++++- .../CCK/attachments/attachments.feature | 10 +++ .../CucumberCompatibilityTests.cs | 23 +++-- ...CucumberMessages.CompatibilityTests.csproj | 12 +-- .../ReqnrollOutputHelperTests.cs | 3 +- 17 files changed, 311 insertions(+), 54 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs create mode 100644 Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs rename Reqnroll/CucumberMesssages/{PickleStepProcessor.cs => ScenarioStepProcessor.cs} (94%) diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs new file mode 100644 index 000000000..25c2eb53c --- /dev/null +++ b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs @@ -0,0 +1,18 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + internal class AttachmentAddedEventWrapper : ExecutionEvent + { + public AttachmentAddedEventWrapper(AttachmentAddedEvent attachmentAddedEvent, string pickleStepId) + { + AttachmentAddedEvent = attachmentAddedEvent; + PickleStepID = pickleStepId; + } + + public AttachmentAddedEvent AttachmentAddedEvent { get; } + public string PickleStepID { get; } + public string TestCaseStartedID { get; set; } + public string TestCaseStepID { get; set; } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 03018637c..0402af6ad 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -5,7 +5,10 @@ using Reqnroll.Events; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; namespace Reqnroll.CucumberMesssages { @@ -28,12 +31,12 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar { switch (stepState) { - case PickleStepProcessor _: - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as PickleStepProcessor); + case ScenarioStepProcessor _: + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as ScenarioStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: - var hookTestStep = CucumberMessageFactory.ToHookTestStep( stepState as HookStepProcessor); + var hookTestStep = CucumberMessageFactory.ToHookTestStep(stepState as HookStepProcessor); testSteps.Add(hookTestStep); break; default: @@ -83,7 +86,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) + internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { bool bound = stepState.StepDefinitionId != null; @@ -97,7 +100,7 @@ internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, Pickle stepState.PickleStepID, bound ? new List { stepState.StepDefinitionId } : new List(), bound ? new List { new StepMatchArgumentsList(args) } : new List() - ); + ); return result; } @@ -112,7 +115,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), argument.Type); } - internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -120,7 +123,7 @@ internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(PickleStepProcessor stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(ScenarioStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, @@ -150,15 +153,15 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; return new TestStep( - hookId, - hookStepState.TestStepID, - null, - new List(), + hookId, + hookStepState.TestStepID, + null, + new List(), new List()); } - internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) { - return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingStartedEvent.Timestamp)); } internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) @@ -166,6 +169,30 @@ internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepPr return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); } + internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) + { + return new Attachment( + Base64EncodeFile(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), + AttachmentContentEncoding.BASE64, + Path.GetFileName(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), + FileExtensionToMIMETypeMap.GetMimeType(Path.GetExtension(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath)), + null, + attachmentAddedEventWrapper.TestCaseStartedID, + attachmentAddedEventWrapper.TestCaseStepID, + null); + } + internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, OutputAddedEventWrapper outputAddedEventWrapper) + { + return new Attachment( + outputAddedEventWrapper.OutputAddedEvent.Text, + AttachmentContentEncoding.IDENTITY, + null, + "text/x.cucumber.log+plain", + null, + outputAddedEventWrapper.TestCaseStartedID, + outputAddedEventWrapper.TestCaseStepID, + null); + } private static TestStepResult ToTestStepResult(StepProcessorBase stepState) { @@ -210,6 +237,11 @@ private static string GenerateSignature(IBinding stepDefinition) { return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; } + public static string Base64EncodeFile(string filePath) + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1d335b97a..c4bea07ba 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -31,7 +31,6 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - //Debugger.Launch(); runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; @@ -49,8 +48,9 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); + testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); + testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } - private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; @@ -143,6 +143,19 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingEvent, featureName); } + + private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) + { + var featureName = attachmentAddedEvent.FeatureName; + ProcessEvent(attachmentAddedEvent, featureName); + } + + private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) + { + ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); + } + + private void ProcessEvent(ExecutionEvent anEvent, string featureName) { var featureProcessor = featureProcessorsByFeatureName[featureName]; diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index 17ccc9d7e..e6e8135a3 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -314,8 +314,8 @@ private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gher return null; } return new Io.Cucumber.Messages.Types.PickleDocString( - pickleDocString.Content, - pickleDocString.MediaType + pickleDocString.MediaType, + pickleDocString.Content ); } diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index e30faf089..30da3786d 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Reqnroll.CucumberMesssages @@ -42,8 +43,11 @@ public bool Success // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once public ConcurrentDictionary StepDefinitionsByPattern = new(); public ConcurrentDictionary HookDefinitionsByPattern = new(); + + //TODO: fix this; there will be multiple Pickles with the same scenario name when executing Example table rows public ConcurrentDictionary PicklesByScenarioName = new(); + //TODO: Fix this for thread-safety; there will be multiple active Scenarios with the same name when executing Example table rows in parallel // Scenario event processors by scenario name; public Dictionary ScenarioName2ScenarioProcessorMap = new(); @@ -72,6 +76,8 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), + OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), }; } @@ -192,7 +198,27 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishe { yield return e; } + } + + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var scenarioName = attachmentAddedEvent.ScenarioName; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + foreach (var e in scenarioEP.ProcessEvent(attachmentAddedEvent)) + { + yield return e; + } + } + + internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) + { + var scenarioName = outputAddedEvent.ScenarioName; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + foreach (var e in scenarioEP.ProcessEvent(outputAddedEvent)) + { + yield return e; + } } private string ExtractLastID(List pickles) diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs index c4deddb92..29d96c701 100644 --- a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -8,7 +8,7 @@ public static class FileExtensionToMIMETypeMap { public static string GetMimeType(string extension) { - if (ExtensionToMimeType.TryGetValue(extension, out var mimeType)) + if (ExtensionToMimeType.TryGetValue(extension.ToLower(), out var mimeType)) return mimeType; return "application/octet-stream"; diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index a2383daf6..7fe99228a 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -7,15 +7,22 @@ namespace Reqnroll.CucumberMesssages { public class HookStepProcessor : StepProcessorBase { + public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { } + public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEvent) + { + TestStepID = parentScenario.IdGenerator.GetNewId(); + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(stepFinishedEvent.HookBinding); + return Enumerable.Empty(); + } + public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; - TestStepID = parentScenario.IdGenerator.GetNewId(); return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs new file mode 100644 index 000000000..bf05e99ae --- /dev/null +++ b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs @@ -0,0 +1,18 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + internal class OutputAddedEventWrapper : ExecutionEvent + { + internal OutputAddedEvent OutputAddedEvent; + internal string PickleStepID; + internal string TestCaseStepID; + internal string TestCaseStartedID; + + public OutputAddedEventWrapper(OutputAddedEvent outputAddedEvent, string pickleStepId) + { + OutputAddedEvent = outputAddedEvent; + PickleStepID = pickleStepId; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index cd6a4c54b..ad4639c7e 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -4,11 +4,13 @@ using Reqnroll.Bindings; using Reqnroll.Events; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace Reqnroll.CucumberMesssages { + public class ScenarioEventProcessor { internal readonly IIdGenerator IdGenerator; @@ -19,6 +21,9 @@ public class ScenarioEventProcessor public string TestCaseID { get; set; } public string PickleID { get; set; } public Pickle Pickle { get; set; } + private TestCase TestCase; + + private TestCaseStarted TestCaseStarted; // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); @@ -51,24 +56,38 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted return Enumerable.Empty(); } - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) return Enumerable.Empty(); - _events.Enqueue(hookBindingFinishedEvent); + _events.Enqueue(hookBindingStartedEvent); var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingStartedEvent); + StepsByEvent.Add(hookBindingStartedEvent, step); + return Enumerable.Empty(); + } + + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + _events.Enqueue(hookBindingFinishedEvent); + var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); step.ProcessEvent(hookBindingFinishedEvent); StepsByEvent.Add(hookBindingFinishedEvent, step); return Enumerable.Empty(); } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); + } + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); - var stepState = new PickleStepProcessor(this); + var stepState = new ScenarioStepProcessor(this); StepsByEvent.Add(stepStartedEvent, stepState); stepState.ProcessEvent(stepStartedEvent); @@ -84,10 +103,31 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = matchingPickleStep.Id; + + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); + _events.Enqueue(attachmentExecutionEventWrapper); + + return Enumerable.Empty(); + } - private PickleStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as PickleStepProcessor).LastOrDefault(); + var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = matchingPickleStep.Id; + + var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); + _events.Enqueue(outputExecutionEventWrapper); + + return Enumerable.Empty(); + } + + private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) @@ -101,8 +141,10 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish switch (executionEvent) { case ScenarioStartedEvent scenarioStartedEvent: - yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); + TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); + yield return Envelope.Create(TestCase); + yield return Envelope.Create(TestCaseStarted); break; case ScenarioFinishedEvent scenarioFinished: ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; @@ -110,16 +152,36 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as PickleStepProcessor, stepStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent)); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as PickleStepProcessor, stepFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); + break; + //TODO: this isn't right; shouuld be one hook processor per hook that ran + case HookBindingStartedEvent hookBindingStartedEvent: + var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent)); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepState = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); + var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepFinishedState as HookStepProcessor, hookBindingFinishedEvent)); + break; + case AttachmentAddedEventWrapper attachmentAddedEventWrapper: + // find the TestCaseStepId and testCaseStartedId + var testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == attachmentAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + var testCaseStartedId = TestCaseStarted.Id; + attachmentAddedEventWrapper.TestCaseStepID = testStepID; + attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); + break; + case OutputAddedEventWrapper outputAddedEventWrapper: + // find the TestCaseStepId and testCaseStartedId + testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == outputAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + testCaseStartedId = TestCaseStarted.Id; + outputAddedEventWrapper.TestCaseStepID = testStepID; + outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); break; // add more cases for other event types default: diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs similarity index 94% rename from Reqnroll/CucumberMesssages/PickleStepProcessor.cs rename to Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index cfc86abfa..29c71c5a9 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -13,11 +13,11 @@ public class StepArgument public string Type; } - public class PickleStepProcessor : StepProcessorBase + public class ScenarioStepProcessor : StepProcessorBase { private StepStartedEvent stepStartedEvent; - public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { } diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index 60944380d..e2d83b446 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -203,20 +203,40 @@ public interface IExecutionOutputEvent public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string Text { get; } + public string FeatureName { get; } + public string ScenarioName { get; } + public string StepText { get; } public OutputAddedEvent(string text) { Text = text; } + + public OutputAddedEvent(string text, string featureName, string scenarioName, string stepText) : this(text) + { + FeatureName = featureName; + ScenarioName = scenarioName; + StepText = stepText; + } } public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } + public string FeatureName { get; } + public string ScenarioName { get; } + public string StepText { get; } public AttachmentAddedEvent(string filePath) { FilePath = filePath; } + + public AttachmentAddedEvent(string filePath, string featureName, string scenarioName, string stepText) : this(filePath) + { + FeatureName = featureName; + ScenarioName = scenarioName; + StepText = stepText; + } } } diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index accb95460..22bb9c82f 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -8,17 +8,23 @@ public class ReqnrollOutputHelper : IReqnrollOutputHelper private readonly ITestThreadExecutionEventPublisher _testThreadExecutionEventPublisher; private readonly ITraceListener _traceListener; private readonly IReqnrollAttachmentHandler _reqnrollAttachmentHandler; + private readonly IContextManager contextManager; - public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler) + public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler, IContextManager contextManager) { _testThreadExecutionEventPublisher = testThreadExecutionEventPublisher; _traceListener = traceListener; _reqnrollAttachmentHandler = reqnrollAttachmentHandler; + this.contextManager = contextManager; } public void WriteLine(string message) { - _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message)); + var featureName = contextManager.FeatureContext.FeatureInfo?.Title; + var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; + var stepText = contextManager.StepContext.StepInfo?.Text; + + _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); _traceListener.WriteToolOutput(message); } @@ -29,7 +35,10 @@ public void WriteLine(string format, params object[] args) public void AddAttachment(string filePath) { - _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath)); + var featureName = contextManager.FeatureContext.FeatureInfo?.Title; + var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; + var stepText = contextManager.StepContext.StepInfo?.Text; + _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); _reqnrollAttachmentHandler.AddAttachment(filePath); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs index 877a2594d..5ab856d0d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs @@ -10,10 +10,44 @@ namespace CucumberMessages.CompatibilityTests.CCK.attachments [Binding] internal class Attachments { + private readonly IReqnrollOutputHelper reqnrollOutputHelper; + + internal Attachments(IReqnrollOutputHelper reqnrollOutputHelper) + { + this.reqnrollOutputHelper = reqnrollOutputHelper; + } + + [When(@"the string {string} is logged")] + public void WhenLogText(string text) + { + reqnrollOutputHelper.WriteLine(text); + } + + [When(@"text with ANSI escapes is logged")] + public void WhenTextWithANSIEscapedIsLogged() + { + reqnrollOutputHelper.WriteLine("This displays a \x1b[31mr\x1b[0m\x1b[91ma\x1b[0m\x1b[33mi\x1b[0m\x1b[32mn\x1b[0m\x1b[34mb\x1b[0m\x1b[95mo\x1b[0m\x1b[35mw\x1b[0m"); + } + [When(@"a JPEG image is attached")] public void WhenAJPEGImageIsAttached() { - throw new NotImplementedException(); + var ext = "jpeg" ; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "attachments", $"cucumber.{ext}"); + + reqnrollOutputHelper.AddAttachment(attachment); + } + + [When(@"a PNG image is attached")] + public void WhenAPNGImageIsAttached() + { + var ext = "png"; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "attachments", $"cucumber.{ext}"); + + reqnrollOutputHelper.AddAttachment(attachment); } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature index 063960e9c..59f9c0987 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature @@ -9,6 +9,8 @@ Feature: Attachments Attachments must have a body and a content type + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Strings can be attached with a media type Beware that some formatters such as @cucumber/react use the media type to determine how to display an attachment. @@ -21,12 +23,16 @@ Feature: Attachments Scenario: Log ANSI coloured text When text with ANSI escapes is logged + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Log JSON When the following string is attached as "application/json": ``` {"message": "The big question", "foo": "bar"} ``` + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Byte arrays are base64-encoded regardless of media type When an array with 10 bytes is attached as "text/plain" @@ -44,8 +50,12 @@ Feature: Attachments | JPEG | | PNG | + # Reqnroll does not support attaching a file by name and rename in the same operation + @ignore Scenario: Attaching PDFs with a different filename When a PDF document is attached and renamed + # Reqnroll does not support attaching a URL + @ignore Scenario: Attaching URIs When a link to "https://cucumber.io" is attached diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index ea9a43376..93f22226f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -17,11 +17,14 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test - Scenario: Smoke Test - Given I have a passing step + Scenario: Log JSON + When the following string is attached as "application/json": + ``` + {"message": "The big question", "foo": "bar"} + ``` """); - AddPassingStepBinding("Given"); + AddPassingStepBinding("When"); ExecuteTests(); @@ -29,7 +32,7 @@ Given I have a passing step } [TestMethod] - //[DataRow("attachments")] + [DataRow("attachments")] [DataRow("minimal")] [DataRow("cdata")] [DataRow("pending")] @@ -45,6 +48,7 @@ public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + AddUtilClassWithFileSystemPath(); scenarioName = scenarioName.Replace("-", "_"); @@ -55,10 +59,13 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); ConfirmAllTestsRan(null); - if (scenarioName == "attachments") - { - ShouldAllScenariosPass(); - } + } + + private void AddUtilClassWithFileSystemPath() + { + string location = AppContext.BaseDirectory; + AddBindingClass( + $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\CCK\"; }} }} "); } private IEnumerable GetExpectedResults(string scenarioName) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 73668f893..b2d5b1598 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -27,9 +27,6 @@ - - - @@ -61,9 +58,6 @@ Always - - - @@ -143,6 +137,12 @@ + + Always + + + Always + Always diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 04d1c1552..8bbc7d653 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -37,8 +37,9 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() _testThreadExecutionEventPublisher = new Mock(); var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); + var contextManager = new Mock(); - return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object); + return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } } } From 0c326546daa2f7319674d99a8196352d8b0d5ba7 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:03:13 -0500 Subject: [PATCH 032/218] Support for generating ParameterType Messages from StepArgumentTransformations. --- .../CucumberMessageFactory.cs | 20 ++++++++++++++++++ .../FeatureEventProcessor.cs | 16 ++++++++++++++ .../CCK/data-tables/data_tables.cs | 21 +++++++++++++++++-- .../CCK/parameter-types/parameter_types.cs | 2 +- ...CucumberMessages.CompatibilityTests.csproj | 2 +- 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 0402af6ad..64c989471 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -76,6 +76,26 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } + + internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) + { + var regex = stepTransform.Regex.ToString(); + var name = stepTransform.Name; + var result = new ParameterType + ( + name, + new List + { + regex + }, + true, + true, + iDGenerator.GetNewId(), + ToSourceRef(stepTransform) + ); + return result; + } + private static SourceReference ToSourceRef(IBinding binding) { var methodName = binding.Method.Name; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 30da3786d..2dcc355a5 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -117,6 +117,11 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); if (bindingRegistry.IsValid) { + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); @@ -144,6 +149,17 @@ internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinished yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); } + private string GenerateScenarioKey(ScenarioInfo scenarioInfo) + { + var scenarioArguments = new List(); + foreach (string v in scenarioInfo.Arguments.Values) + { + scenarioArguments.Add(v); + } + return scenarioInfo.Title + + scenarioArguments + + scenarioInfo.CombinedTags; + } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs index 0e696ae16..5f6a435e6 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; @@ -27,6 +28,7 @@ public void ThenItShouldBe(Table expected) private void TablesEqual(Table expected, Table transposedTable) { + expected = MakeHeaderLess(expected); var ExpectednumRows = expected.Rows.Count; var ExpectednumCols = expected.Rows[0].Count; @@ -49,8 +51,9 @@ private void TablesEqual(Table expected, Table transposedTable) private Table Transpose(Table table) { + Table tempTable = MakeHeaderLess(table); - string[][] matrix = GetStringArray(table.Rows); + string[][] matrix = GetStringArray(tempTable.Rows); var t = TransposeMatrix(matrix); return CreateTable(t); @@ -96,7 +99,7 @@ static Table CreateTable(string[][] matrix) { var columnCount = matrix[0].Length; var headers = Enumerable.Range(0, columnCount).Select(i => $"").ToArray(); - var table = new Table(); + var table = new Table(headers); foreach (var row in matrix) { @@ -106,5 +109,19 @@ static Table CreateTable(string[][] matrix) return table; } } + + private static Table MakeHeaderLess(Table table) + { + // push the header into a new Table as the first row of that table + var header = table.Header; + var tempTable = new Table(Enumerable.Range(0, header.Count).Select(i => $"").ToArray()); + tempTable.AddRow(header.ToArray()); + foreach (var row in table.Rows) + { + tempTable.AddRow(row.Select(kvp => kvp.Value).ToArray()); + } + + return tempTable; + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs index faad8ebb5..27e151de4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs @@ -10,7 +10,7 @@ namespace CucumberMessages.CompatibilityTests.CCK.parameter_types [Binding] internal class Parameter_types { - [StepArgumentTransformation(@"([A-Z]{3})-([A-Z]{3})")] + [StepArgumentTransformation(Name ="flight", Regex = @"([A-Z]{3})-([A-Z]{3})")] public Flight FlightConverter(string from, string to) { return new Flight diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index b2d5b1598..6b52388e4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -63,11 +63,11 @@ Always + Always - From c9e1a22b510cb88659306e39bb02e52da4a80c76 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:03:59 -0500 Subject: [PATCH 033/218] Working except for the TestStepStarted/Finished for the hook that throws an exception. --- .../CucumberMessagePublisher.cs | 8 +++++ .../FeatureEventProcessor.cs | 12 ++++++++ .../ScenarioEventProcessor.cs | 29 ++++++++++++++----- Reqnroll/Events/ExecutionEvent.cs | 6 ++++ .../Infrastructure/ReqnrollOutputHelper.cs | 4 +-- .../Infrastructure/TestExecutionEngine.cs | 2 +- .../CCK/hooks/hooks.cs | 9 ++++-- ...CucumberMessages.CompatibilityTests.csproj | 5 ++-- 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index c4bea07ba..eabd2afdf 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -47,10 +47,12 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; @@ -138,6 +140,12 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) ProcessEvent(stepFinishedEvent, featureName); } + private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + { + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookBindingStartedEvent, featureName); + } + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) { var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 2dcc355a5..604d358d8 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -75,6 +75,7 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), + HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), @@ -205,6 +206,17 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedEvent) + { + var scenarioName = hookStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookStartedEvent)) + { + yield return e; + } + } + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index ad4639c7e..8fbb0aa2b 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Reqnroll.CucumberMesssages @@ -71,6 +72,12 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingS internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { + + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + _events.Enqueue(hookBindingFinishedEvent); var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); step.ProcessEvent(hookBindingFinishedEvent); @@ -105,8 +112,8 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { - var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = matchingPickleStep.Id; + //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = ""; var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); _events.Enqueue(attachmentExecutionEventWrapper); @@ -116,9 +123,10 @@ internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAdded internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) { - var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = matchingPickleStep.Id; + //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); + //var pickleStepId = matchingPickleStep.Id; + var pickleStepId = ""; var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); _events.Enqueue(outputExecutionEventWrapper); @@ -133,6 +141,7 @@ private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepF internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); + TestStepStarted mostRecentTestStepStarted = null; while (_events.Count > 0) { @@ -152,7 +161,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent)); + var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent); + mostRecentTestStepStarted = stepStarted; + yield return Envelope.Create(stepStarted); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; @@ -161,7 +172,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish //TODO: this isn't right; shouuld be one hook processor per hook that ran case HookBindingStartedEvent hookBindingStartedEvent: var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent)); + var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); + mostRecentTestStepStarted = hookStepStarted; + yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; @@ -169,7 +182,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId - var testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == attachmentAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + var testStepID = mostRecentTestStepStarted.TestStepId; var testCaseStartedId = TestCaseStarted.Id; attachmentAddedEventWrapper.TestCaseStepID = testStepID; attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; @@ -177,7 +190,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case OutputAddedEventWrapper outputAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId - testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == outputAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + testStepID = mostRecentTestStepStarted.TestStepId; testCaseStartedId = TestCaseStarted.Id; outputAddedEventWrapper.TestCaseStepID = testStepID; outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e2d83b446..e9aabf778 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -171,11 +171,17 @@ public StepBindingFinishedEvent(IStepDefinitionBinding stepDefinitionBinding, Ti public class HookBindingStartedEvent : ExecutionEvent { public IHookBinding HookBinding { get; } + public IContextManager ContextManager { get; private set; } public HookBindingStartedEvent(IHookBinding hookBinding) { HookBinding = hookBinding; } + + public HookBindingStartedEvent(IHookBinding hookBinding, IContextManager contextManager) : this(hookBinding) + { + ContextManager = contextManager; + } } public class HookBindingFinishedEvent : ExecutionEvent diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index 22bb9c82f..604d08d88 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -22,7 +22,7 @@ public void WriteLine(string message) { var featureName = contextManager.FeatureContext.FeatureInfo?.Title; var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext.StepInfo?.Text; + var stepText = contextManager.StepContext?.StepInfo?.Text; _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); _traceListener.WriteToolOutput(message); @@ -37,7 +37,7 @@ public void AddAttachment(string filePath) { var featureName = contextManager.FeatureContext.FeatureInfo?.Title; var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext.StepInfo?.Text; + var stepText = contextManager.StepContext?.StepInfo?.Text; _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); _reqnrollAttachmentHandler.AddAttachment(filePath); } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index f4996f6b5..c1164a7ee 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -353,7 +353,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin var currentContainer = GetHookContainer(hookType); var arguments = ResolveArguments(hookBinding, currentContainer); - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); try diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 7f2497446..5f43c19c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Diagnostics; namespace CucumberMessages.CompatibilityTests.CCK.hooks { @@ -49,8 +50,12 @@ public void FailingAfterHook() [AfterScenario("with-attachment")] public void PassingAfterHook() { - reqnrollOutputHelper.AddAttachment("cucumber.svg"); - } + Debugger.Launch(); + var ext = "svg"; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "hooks", $"cucumber.{ext}"); + reqnrollOutputHelper.AddAttachment(attachment); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 6b52388e4..c5c455a05 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -33,7 +33,6 @@ - @@ -73,7 +72,6 @@ Always - @@ -143,6 +141,9 @@ Always + + Always + Always From 34a1b8acde8706907c3240ee8e8554d886069e2b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:52:34 -0500 Subject: [PATCH 034/218] Hardcode ParameterType Message properties of useForSnippets and preferForRegularExpressionMatch to false (as these aren't used by Reqnroll). --- Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 64c989471..8f40ed7f8 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -88,8 +88,8 @@ internal static ParameterType ToParameterType(IStepArgumentTransformationBinding { regex }, - true, - true, + false, + false, iDGenerator.GetNewId(), ToSourceRef(stepTransform) ); From a6b547c045a7c2cd3bf92aee66197d18e3787cbf Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:04:08 -0500 Subject: [PATCH 035/218] First draft of obtaining Product infos from Analytics Provider. Needs CI and Git info. Needs to be refactored. --- .../FeatureEventProcessor.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 604d358d8..43fbb1ca7 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -4,12 +4,14 @@ using Reqnroll.Bindings; using Reqnroll.BoDi; using Reqnroll.Events; +using Reqnroll.Analytics; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; namespace Reqnroll.CucumberMesssages { @@ -85,12 +87,27 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { + //HACK: temporary implementation to obtain information for the Meta message + var analtyicsEventProcessor = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + var envInfo = analtyicsEventProcessor.CreateProjectRunningEvent(""); + var implementation = new Product("Reqnroll", envInfo.ReqnrollVersion); + var runTime = new Product("dotNet", envInfo.TargetFramework); + var os = new Product(envInfo.Platform, envInfo.PlatformDescription); + + var cpu = RuntimeInformation.ProcessArchitecture switch { + Architecture.Arm => new Product("arm", null), + Architecture.Arm64 => new Product("arm64", null), + Architecture.X86 => new Product("x86", null), + Architecture.X64 => new Product("x64", null), + _ => new Product(null, null), + }; + yield return Envelope.Create(new Meta( - Cucumber.Messages.ProtocolVersion.Version, - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), + (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], + implementation, + runTime, + os, + cpu, null)); Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); From b621d3403faf93ba371028b8e55d43cc666d0508 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:07:23 -0500 Subject: [PATCH 036/218] Refactored Meta, CI and Git creation to the MessageFactory. Refactored OS and CI information gathering out of AnalyticsEventProvider to EnvironmentWrapper. --- Reqnroll/Analytics/AnalyticsEventProvider.cs | 94 ++----------------- .../CucumberMessageFactory.cs | 66 +++++++++++++ .../FeatureEventProcessor.cs | 23 +---- .../EnvironmentAccess/EnvironmentWrapper.cs | 81 ++++++++++++++++ .../EnvironmentAccess/IEnvironmentWrapper.cs | 10 ++ 5 files changed, 167 insertions(+), 107 deletions(-) diff --git a/Reqnroll/Analytics/AnalyticsEventProvider.cs b/Reqnroll/Analytics/AnalyticsEventProvider.cs index 90a446c3f..301835a22 100644 --- a/Reqnroll/Analytics/AnalyticsEventProvider.cs +++ b/Reqnroll/Analytics/AnalyticsEventProvider.cs @@ -27,11 +27,11 @@ public ReqnrollProjectCompilingEvent CreateProjectCompilingEvent(string msbuildV { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string buildServerName = GetBuildServerName(); - bool isDockerContainer = IsRunningInDockerContainer(); + string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); + string buildServerName = _environmentWrapper.GetBuildServerName(); + bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); string hashedAssemblyName = ToSha256(assemblyName); - string platform = GetOSPlatform(); + string platform = _environmentWrapper.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var compiledEvent = new ReqnrollProjectCompilingEvent( @@ -56,13 +56,13 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string targetFramework = GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; - bool isDockerContainer = IsRunningInDockerContainer(); - string buildServerName = GetBuildServerName(); + string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); + string targetFramework = _environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); + string buildServerName = _environmentWrapper.GetBuildServerName(); string hashedAssemblyName = ToSha256(testAssemblyName); - string platform = GetOSPlatform(); + string platform = _environmentWrapper.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var runningEvent = new ReqnrollProjectRunningEvent( @@ -80,70 +80,6 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly return runningEvent; } - private string GetOSPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Linux"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "OSX"; - } - - throw new InvalidOperationException("Platform cannot be identified"); - } - - private readonly Dictionary buildServerTypes - = new Dictionary { - { "TF_BUILD","Azure Pipelines"}, - { "TEAMCITY_VERSION","TeamCity"}, - { "JENKINS_HOME","Jenkins"}, - { "GITHUB_ACTIONS","GitHub Actions"}, - { "GITLAB_CI","GitLab CI/CD"}, - { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, - { "TRAVIS","Travis CI"}, - { "APPVEYOR","AppVeyor"}, - { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, - { "bamboo_agentId", "Atlassian Bamboo" }, - { "CIRCLECI", "CircleCI" }, - { "GO_PIPELINE_NAME", "GoCD" }, - { "BUDDY", "Buddy" }, - { "NEVERCODE", "Nevercode" }, - { "SEMAPHORE", "SEMAPHORE" }, - { "BROWSERSTACK_USERNAME", "BrowserStack" }, - { "CF_BUILD_ID", "Codefresh" }, - { "TentacleVersion", "Octopus Deploy" }, - - { "CI_NAME", "CodeShip" } - }; - - private string GetBuildServerName() - { - foreach (var buildServerType in buildServerTypes) - { - var envVariable = _environmentWrapper.GetEnvironmentVariable(buildServerType.Key); - if (envVariable is ISuccess) - return buildServerType.Value; - } - return null; - } - - private bool IsRunningInDockerContainer() - { - return _environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; - } - - private string GetReqnrollVersion() - { - return VersionInfo.AssemblyInformationalVersion; - } private string ToSha256(string inputString) { @@ -163,17 +99,5 @@ private string ToSha256(string inputString) return stringBuilder.ToString(); } - private string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - - return null; - } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 8f40ed7f8..9a4cf7c40 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,12 +1,16 @@ using Cucumber.Messages; using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; +using Reqnroll.Analytics; using Reqnroll.Bindings; +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; using Reqnroll.Events; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; @@ -262,6 +266,68 @@ public static string Base64EncodeFile(string filePath) byte[] fileBytes = File.ReadAllBytes(filePath); return Convert.ToBase64String(fileBytes); } + + public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) + { + var environmentWrapper = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + + var implementation = new Product("Reqnroll", environmentWrapper.GetReqnrollVersion()); + string targetFramework = environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + + var runTime = new Product("dotNet", targetFramework); + var os = new Product(environmentWrapper.GetOSPlatform(), RuntimeInformation.OSDescription); + + var cpu = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm => new Product("arm", null), + Architecture.Arm64 => new Product("arm64", null), + Architecture.X86 => new Product("x86", null), + Architecture.X64 => new Product("x64", null), + _ => new Product(null, null), + }; + + var ci_name = environmentWrapper.GetBuildServerName(); + + var ci = ToCi(ci_name, environmentWrapper); + + return Envelope.Create(new Meta( + (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], + implementation, + runTime, + os, + cpu, + ci)); + } + + private static Ci ToCi(string ci_name, IEnvironmentWrapper environmentWrapper) + { + //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. + // Until then, these will be hard coded as null + if (String.IsNullOrEmpty(ci_name)) return null; + + var git = ToGit(environmentWrapper); + + return new Ci(ci_name, null, null, git); + } + + private static Git ToGit(IEnvironmentWrapper environmentWrapper) + { + Git git; + var git_url = environmentWrapper.GetEnvironmentVariable("GIT_URL"); + var git_branch = environmentWrapper.GetEnvironmentVariable("GIT_BRANCH"); + var git_commit = environmentWrapper.GetEnvironmentVariable("GIT_COMMIT"); + var git_tag = environmentWrapper.GetEnvironmentVariable("GIT_TAG"); + if (git_url is not ISuccess) git = null; + else + git = new Git + ( + (git_url as ISuccess).Result, + git_branch is ISuccess ? (git_branch as ISuccess).Result : null, + git_commit is ISuccess ? (git_commit as ISuccess).Result : null, + git_tag is ISuccess ? (git_tag as ISuccess).Result : null + ); + return git; + } #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 43fbb1ca7..d9fdeb7af 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -87,28 +87,7 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { - //HACK: temporary implementation to obtain information for the Meta message - var analtyicsEventProcessor = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - var envInfo = analtyicsEventProcessor.CreateProjectRunningEvent(""); - var implementation = new Product("Reqnroll", envInfo.ReqnrollVersion); - var runTime = new Product("dotNet", envInfo.TargetFramework); - var os = new Product(envInfo.Platform, envInfo.PlatformDescription); - - var cpu = RuntimeInformation.ProcessArchitecture switch { - Architecture.Arm => new Product("arm", null), - Architecture.Arm64 => new Product("arm64", null), - Architecture.X86 => new Product("x86", null), - Architecture.X64 => new Product("x64", null), - _ => new Product(null, null), - }; - - yield return Envelope.Create(new Meta( - (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], - implementation, - runTime, - os, - cpu, - null)); + yield return CucumberMessageFactory.ToMeta(featureStartedEvent); Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 4ac9ebb9f..6bfd79e74 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; using Reqnroll.CommonModels; namespace Reqnroll.EnvironmentAccess @@ -36,5 +39,83 @@ public void SetEnvironmentVariable(string name, string value) } public string GetCurrentDirectory() => Environment.CurrentDirectory; + + public string GetOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "OSX"; + } + + throw new InvalidOperationException("Platform cannot be identified"); + } + + private readonly Dictionary buildServerTypes + = new Dictionary { + { "TF_BUILD","Azure Pipelines"}, + { "TEAMCITY_VERSION","TeamCity"}, + { "JENKINS_HOME","Jenkins"}, + { "GITHUB_ACTIONS","GitHub Actions"}, + { "GITLAB_CI","GitLab CI/CD"}, + { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, + { "TRAVIS","Travis CI"}, + { "APPVEYOR","AppVeyor"}, + { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, + { "bamboo_agentId", "Atlassian Bamboo" }, + { "CIRCLECI", "CircleCI" }, + { "GO_PIPELINE_NAME", "GoCD" }, + { "BUDDY", "Buddy" }, + { "NEVERCODE", "Nevercode" }, + { "SEMAPHORE", "SEMAPHORE" }, + { "BROWSERSTACK_USERNAME", "BrowserStack" }, + { "CF_BUILD_ID", "Codefresh" }, + { "TentacleVersion", "Octopus Deploy" }, + + { "CI_NAME", "CodeShip" } + }; + + public string GetBuildServerName() + { + foreach (var buildServerType in buildServerTypes) + { + var envVariable = GetEnvironmentVariable(buildServerType.Key); + if (envVariable is ISuccess) + return buildServerType.Value; + } + return null; + } + + public bool IsRunningInDockerContainer() + { + return GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + } + + public string GetReqnrollVersion() + { + return VersionInfo.AssemblyInformationalVersion; + } + public string GetNetCoreVersion() + { + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; + } + } } diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs index 5e88ba8bc..d6edfd6bc 100644 --- a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs @@ -13,5 +13,15 @@ public interface IEnvironmentWrapper void SetEnvironmentVariable(string name, string value); string GetCurrentDirectory(); + + string GetOSPlatform(); + + string GetBuildServerName(); + + bool IsRunningInDockerContainer(); + + string GetReqnrollVersion(); + + string GetNetCoreVersion(); } } From cd6d30471d5a456ca3151c7d397f08aa2dfc6cfe Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:30:38 -0500 Subject: [PATCH 037/218] Exceptions in steps produce Exception Messages. Hooks partially work. --- .../CucumberMessageFactory.cs | 18 +++++++++-- .../CucumberMessagePublisher.cs | 8 +++++ .../FeatureEventProcessor.cs | 30 ++++++++++++++++--- .../ScenarioEventProcessor.cs | 30 +++++++++++++++++-- .../ScenarioStepProcessor.cs | 5 ++++ .../CucumberMesssages/StepProcessorBase.cs | 1 + .../CCK/hooks/hooks.cs | 3 +- .../CucumberCompatibilityTests.cs | 3 +- 8 files changed, 86 insertions(+), 12 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 9a4cf7c40..94699675c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -188,9 +188,9 @@ internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProc return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookFinishedEvent) { - return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) @@ -224,8 +224,20 @@ private static TestStepResult ToTestStepResult(StepProcessorBase stepState) Converters.ToDuration(stepState.Duration), "", ToTestStepResultStatus(stepState.Status), - null); + ToException(stepState.Exception) + ); + + } + private static Io.Cucumber.Messages.Types.Exception ToException(System.Exception exception) + { + if (exception == null) return null; + + return new Io.Cucumber.Messages.Types.Exception( + exception.GetType().Name, + exception.Message, + exception.StackTrace + ); } private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStatus status) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index eabd2afdf..908aa7254 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -27,6 +27,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { + //Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -49,6 +50,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } @@ -152,6 +154,12 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin ProcessEvent(hookBindingEvent, featureName); } + private void HookFinishedEventHandler(HookFinishedEvent hookFinishedEvent) + { + var featureName = hookFinishedEvent.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookFinishedEvent, featureName); + } + private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureName; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index d9fdeb7af..18d151c33 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -78,7 +78,8 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), - HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), + HookFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), @@ -213,17 +214,38 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedE } } - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioName = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(hookBindingFinishedEvent)) { yield return e; } } + internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature + || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) + foreach(var e in Enumerable.Empty()) + { + yield return e; + } + else + { + var scenarioName = hookFinishedEvent.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + { + yield return e; + } + } + } + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var scenarioName = attachmentAddedEvent.ScenarioName; diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 8fbb0aa2b..3e08097f4 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -85,6 +85,18 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature + || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + + // The HookFinishedEvent does not tell us which hook(binding) was finished. We'll find out later during replay by tracking the last HookBindingFinishedEvent + _events.Enqueue(hookFinishedEvent); + return Enumerable.Empty(); + } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); @@ -142,6 +154,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish { _events.Enqueue(scenarioFinishedEvent); TestStepStarted mostRecentTestStepStarted = null; + StepProcessorBase mostRecentHookStep = null; while (_events.Count > 0) { @@ -169,7 +182,6 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish var stepFinishedState = StepsByEvent[stepFinishedEvent]; yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); break; - //TODO: this isn't right; shouuld be one hook processor per hook that ran case HookBindingStartedEvent hookBindingStartedEvent: var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); @@ -177,8 +189,20 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepFinishedState as HookStepProcessor, hookBindingFinishedEvent)); + // Find the hookStep that matches the hookBinding and store it temporarily; to be processed when the hook finished event is processed + mostRecentHookStep = StepsByEvent[hookBindingFinishedEvent]; + break; + case HookFinishedEvent hookFinishedEvent: + // mostRecentHookStep will be null when we've already created a TestStepFinished for the hookBinding as there may be multiple HookFinishedEvents for a Step or Block or Scenario + if (mostRecentHookStep == null) + break; + if (mostRecentHookStep != null && hookFinishedEvent.HookException != null) + { + mostRecentHookStep.Exception = hookFinishedEvent.HookException; + } + var hookStepProcessor = mostRecentHookStep as HookStepProcessor; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor, hookStepProcessor.HookBindingFinishedEvent)); + mostRecentHookStep = null; break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 29c71c5a9..9f53ad4d2 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -60,6 +60,11 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; Status = stepFinishedEvent.StepContext.Status; + if (Status == ScenarioExecutionStatus.TestError && stepFinishedEvent.ScenarioContext.TestError != null) + { + Exception = stepFinishedEvent.ScenarioContext.TestError; + } + StepArguments = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument { diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index 090b5055d..af4834a6f 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -8,6 +8,7 @@ public class StepProcessorBase : IStepProcessor public string TestCaseStartedID => parentScenario.TestCaseStartedID; public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } + public Exception Exception { get; set; } public ScenarioEventProcessor parentScenario; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 5f43c19c0..851e25555 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -15,6 +15,7 @@ internal class Hooks public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) { + //Debugger.Launch(); this.reqnrollOutputHelper = reqnrollOutputHelper; } @@ -50,7 +51,7 @@ public void FailingAfterHook() [AfterScenario("with-attachment")] public void PassingAfterHook() { - Debugger.Launch(); + var ext = "svg"; var path = FileSystemPath.GetFilePathForAttachments(); var attachment = Path.Combine(path, "hooks", $"cucumber.{ext}"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 93f22226f..6ff839517 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -26,6 +26,7 @@ public void SmokeTest() AddPassingStepBinding("When"); + ExecuteTests(); ShouldAllScenariosPass(); @@ -54,7 +55,7 @@ public void CCKScenarios(string scenarioName) AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + //AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); From 3dd8f1d3ede477907c9bf71f295ed475a348b516 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 31 Aug 2024 11:06:20 -0500 Subject: [PATCH 038/218] Moved back to using HookBindingFinished; eliminated use of HookFinished event. --- .../CucumberMessagePublisher.cs | 7 ---- .../FeatureEventProcessor.cs | 22 ------------ .../CucumberMesssages/HookStepProcessor.cs | 1 + .../ScenarioEventProcessor.cs | 35 +++---------------- Reqnroll/Events/ExecutionEvent.cs | 4 ++- .../Infrastructure/TestExecutionEngine.cs | 12 +++++-- .../CCK/hooks/hooks.cs | 2 +- 7 files changed, 20 insertions(+), 63 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 908aa7254..7f48a76dc 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -50,7 +50,6 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); - testThreadEventPublisher.AddHandler(HookFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } @@ -154,12 +153,6 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin ProcessEvent(hookBindingEvent, featureName); } - private void HookFinishedEventHandler(HookFinishedEvent hookFinishedEvent) - { - var featureName = hookFinishedEvent.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookFinishedEvent, featureName); - } - private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureName; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 18d151c33..6f534827c 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -79,7 +79,6 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), - HookFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), @@ -225,27 +224,6 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding } } - internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature - || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) - foreach(var e in Enumerable.Empty()) - { - yield return e; - } - else - { - var scenarioName = hookFinishedEvent.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) - { - yield return e; - } - } - } - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var scenarioName = attachmentAddedEvent.ScenarioName; diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 7fe99228a..7c17966ce 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -23,6 +23,7 @@ public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEv public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; + Exception = hookFinishedEvent.HookException; return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 3e08097f4..11dae1307 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -34,7 +34,7 @@ public List Steps { get { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value ).ToList(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); } } public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } @@ -85,18 +85,6 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding return Enumerable.Empty(); } - internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature - || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - - // The HookFinishedEvent does not tell us which hook(binding) was finished. We'll find out later during replay by tracking the last HookBindingFinishedEvent - _events.Enqueue(hookFinishedEvent); - return Enumerable.Empty(); - } - private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); @@ -126,7 +114,7 @@ internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAdded { //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); var pickleStepId = ""; - + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); _events.Enqueue(attachmentExecutionEventWrapper); @@ -147,14 +135,13 @@ internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent)(kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); TestStepStarted mostRecentTestStepStarted = null; - StepProcessorBase mostRecentHookStep = null; while (_events.Count > 0) { @@ -189,20 +176,8 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - // Find the hookStep that matches the hookBinding and store it temporarily; to be processed when the hook finished event is processed - mostRecentHookStep = StepsByEvent[hookBindingFinishedEvent]; - break; - case HookFinishedEvent hookFinishedEvent: - // mostRecentHookStep will be null when we've already created a TestStepFinished for the hookBinding as there may be multiple HookFinishedEvents for a Step or Block or Scenario - if (mostRecentHookStep == null) - break; - if (mostRecentHookStep != null && hookFinishedEvent.HookException != null) - { - mostRecentHookStep.Exception = hookFinishedEvent.HookException; - } - var hookStepProcessor = mostRecentHookStep as HookStepProcessor; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor, hookStepProcessor.HookBindingFinishedEvent)); - mostRecentHookStep = null; + var hookStepProcessor = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e9aabf778..a3f2a0003 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -190,6 +190,7 @@ public class HookBindingFinishedEvent : ExecutionEvent public TimeSpan Duration { get; } public IContextManager ContextManager { get; private set; } + public Exception HookException { get; private set; } public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { @@ -197,9 +198,10 @@ public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) Duration = duration; } - public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager) : this(hookBinding, duration) + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager, Exception hookException = null) : this(hookBinding, duration) { ContextManager = contextManager; + HookException = hookException; } } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index c1164a7ee..af7f87026 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -355,14 +355,22 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); - + Exception exceptionthrown = null; try { await invoker.InvokeBindingAsync(hookBinding, _contextManager, arguments, _testTracer, durationHolder); } + catch (Exception exception) + { + // This exception is caught in order to be able to inform consumers of the HookBindingFinishedEvent; + // This is used by CucumberMessages to include information about the exception in the hook TestStepResult + // The throw; statement ensures that the exception is propagated up to the FireEventsAsync method + exceptionthrown = exception; + throw; + } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager, exceptionthrown)); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 851e25555..011fe1d83 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -42,7 +42,7 @@ public void NamedBeforeHook() { } [AfterScenario] public void AfterScenarioHook() { } - [AfterScenario("some-tag or some-other-tag")] + [AfterScenario(), Scope(Tag = "some-tag or some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); From 77dafdec13f920d39b3a5016eccb01a4721184ff Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:02:04 -0500 Subject: [PATCH 039/218] Hooks now working per CCK pending outcome of bug analysis of TEE. --- .../CucumberMesssages/CucumberMessagePublisher.cs | 2 +- Reqnroll/CucumberMesssages/HookStepProcessor.cs | 2 ++ .../CucumberMesssages/ScenarioEventProcessor.cs | 1 - Reqnroll/Infrastructure/TestExecutionEngine.cs | 3 ++- .../CCK/hooks/hooks.cs | 5 +++-- .../CucumberCompatibilityTests.cs | 14 +++++++++++++- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 7f48a76dc..296727ccb 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -27,7 +27,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - //Debugger.Launch(); + // Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 7c17966ce..649d2f330 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -24,6 +24,8 @@ public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedE { HookBindingFinishedEvent = hookFinishedEvent; Exception = hookFinishedEvent.HookException; + Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; + return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 11dae1307..b8f8d9836 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -72,7 +72,6 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingS internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - // At this point we only care about hooks that wrap scenarios or steps if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index af7f87026..aca42938a 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -231,10 +231,11 @@ public virtual async Task OnScenarioEndAsync() { await FireScenarioEventsAsync(HookType.AfterScenario); } - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); } finally { + _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); + _contextManager.CleanupScenarioContext(); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 011fe1d83..5b6294857 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -15,7 +15,6 @@ internal class Hooks public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) { - //Debugger.Launch(); this.reqnrollOutputHelper = reqnrollOutputHelper; } @@ -42,7 +41,9 @@ public void NamedBeforeHook() { } [AfterScenario] public void AfterScenarioHook() { } - [AfterScenario(), Scope(Tag = "some-tag or some-other-tag")] + [AfterScenario()] + [Scope(Tag = "some-tag")] + [Scope(Tag = "some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 6ff839517..2ca8afac3 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -17,6 +17,7 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test + @some-tag Scenario: Log JSON When the following string is attached as "application/json": ``` @@ -25,7 +26,18 @@ public void SmokeTest() """); AddPassingStepBinding("When"); - + AddBindingClass(""" + [Binding] + public class TaggedScenario + { + [AfterScenario()] + [Scope(Tag = "some-tag")] + public void FailingAfterHook() + { + throw new Exception("Exception in conditional hook"); + } + } + """); ExecuteTests(); From 024cb209f86aafa157a409256cbaf5b8a7732b7e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:21:33 -0500 Subject: [PATCH 040/218] Given default Gherkin.Ast.Locations of (0,0) to generated Table elements (headers, rows, cells). --- .../Transformation/IncludeExternalDataTransformation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs index c649d032b..3441f3d5d 100644 --- a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs +++ b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs @@ -85,7 +85,7 @@ private Scenario GetTransformedScenario(Scenario scenario, ExternalDataSpecifica { var exampleRecords = specification.GetExampleRecords(examplesHeaderNames); var exampleRows = exampleRecords.Items - .Select(rec => new Gherkin.Ast.TableRow(null, exampleRecords.Header.Select(h => new TableCell(null, rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) + .Select(rec => new Gherkin.Ast.TableRow(new Location(0, 0), exampleRecords.Header.Select(h => new TableCell(new Location(0, 0), rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) .ToArray(); var examplesBlock = CreateExamplesBlock(exampleRecords.Header, exampleRows, examplesKeyword); @@ -108,8 +108,8 @@ private Examples CreateExamplesBlock(string[] headerNames, Gherkin.Ast.TableRow[ { keyword ??= "External Examples"; var name = "External Examples"; - var tableHeader = new Gherkin.Ast.TableRow(null, headerNames.Select(h => new TableCell(null, h)).ToArray()); - return new Examples(new Tag[0], null, keyword, name, "", tableHeader, exampleRows); + var tableHeader = new Gherkin.Ast.TableRow(new Location(0, 0), headerNames.Select(h => new TableCell(new Location(0, 0), h)).ToArray()); + return new Examples(new Tag[0], new Location(0, 0), keyword, name, "", tableHeader, exampleRows); } } } From 03d1589011b0ce6bc9fb70fc2f2c47ff509c7506 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:42:52 -0500 Subject: [PATCH 041/218] Added a smoke test to prove that ExternalData generated Example tables are properly parsed as DataTables by the Gherkin pickler. --- .../CucumberCompatibilityTests.cs | 120 ++++++++++++++++++ ...CucumberMessages.CompatibilityTests.csproj | 4 + .../products.csv | 4 + 3 files changed, 128 insertions(+) create mode 100644 Tests/CucumberMessages.CompatibilityTests/products.csv diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 2ca8afac3..7220109e9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -2,6 +2,7 @@ using Cucumber.Messages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; +using Reqnroll; using System.Reflection; namespace CucumberMessages.CompatibilityTests @@ -44,6 +45,125 @@ public void FailingAfterHook() ShouldAllScenariosPass(); } + [TestMethod] + public void CucumberMessagesInteropWithExternalData() + { + // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages + AddCucumberMessagePlugIn(); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.0-local"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest + var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.CompatibilityTests", Assembly.GetExecutingAssembly()); + _projectsDriver.AddFile("products.csv", content); + AddFeatureFile(""" + Feature: External Data from CSV file + + @DataSource:products.csv + Scenario: Valid product prices are calculated + The scenario will be treated as a scenario outline with the examples from the CSV file. + Given the customer has put 1 pcs of to the basket + When the basket price is calculated + Then the basket price should be greater than zero + + """); + + AddBindingClass(""" + using System; + using System.Collections.Generic; + using System.Linq; + + namespace Reqnroll.ExternalData.ReqnrollPlugin.IntegrationTest.StepDefinitions + { + [Binding] + public class PricingStepDefinitions + { + class PriceCalculator + { + private readonly Dictionary _basket = new(); + private readonly Dictionary _itemPrices = new(); + + public void AddToBasket(string productName, int quantity) + { + if (!_basket.TryGetValue(productName, out var currentQuantity)) + currentQuantity = 0; + _basket[productName] = currentQuantity + quantity; + } + + public decimal CalculatePrice() + { + return _basket.Sum(bi => GetPrice(bi.Key) * bi.Value); + } + + private decimal GetPrice(string productName) + { + if (_itemPrices.TryGetValue(productName, out var itemPrice)) + return itemPrice; + return 1.5m; + } + + public void SetPrice(string productName, in decimal itemPrice) + { + _itemPrices[productName] = itemPrice; + } + } + + private readonly ScenarioContext _scenarioContext; + private readonly PriceCalculator _priceCalculator = new(); + private decimal _calculatedPrice; + + public PricingStepDefinitions(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [Given(@"the price of (.*) is €(.*)")] + public void GivenThePriceOfProductIs(string productName, decimal itemPrice) + { + _priceCalculator.SetPrice(productName, itemPrice); + } + + [Given(@"the customer has put (.*) pcs of (.*) to the basket")] + public void GivenTheCustomerHasPutPcsOfProductToTheBasket(int quantity, string productName) + { + _priceCalculator.AddToBasket(productName, quantity); + } + + [Given(@"the customer has put a product to the basket")] + public void GivenTheCustomerHasPutAProductToTheBasket() + { + var productName = _scenarioContext.ScenarioInfo.Arguments["product"]?.ToString(); + _priceCalculator.AddToBasket(productName, 1); + } + + [When(@"the basket price is calculated")] + public void WhenTheBasketPriceIsCalculated() + { + _calculatedPrice = _priceCalculator.CalculatePrice(); + } + + [Then(@"the basket price should be greater than zero")] + public void ThenTheBasketPriceShouldBeGreaterThanZero() + { + if (_calculatedPrice <= 0) throw new Exception("Basket price is less than zero: " + _calculatedPrice ); + } + + [Then(@"the basket price should be €(.*)")] + public void ThenTheBasketPriceShouldBe(decimal expectedPrice) + { + if(expectedPrice != _calculatedPrice) throw new Exception("Basket price is not as expected: " + _calculatedPrice + " vs " + expectedPrice); + } + + } + } + + """); + ExecuteTests(); + + ShouldAllScenariosPass(); + + } + [TestMethod] [DataRow("attachments")] [DataRow("minimal")] diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index c5c455a05..1c4df4f41 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -49,6 +49,7 @@ + @@ -109,6 +110,9 @@ + + Always + Always diff --git a/Tests/CucumberMessages.CompatibilityTests/products.csv b/Tests/CucumberMessages.CompatibilityTests/products.csv new file mode 100644 index 000000000..b47bfe36b --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/products.csv @@ -0,0 +1,4 @@ +product,price +Chocolate,2.5 +Apple,1.0 +Orange,1.2 From 593cdc80c0f7f905ee3663249e42c3210d44f42d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:03:36 -0500 Subject: [PATCH 042/218] Ensure that CucumberMessages doesn't run when no Sink is registered. --- .../CucumberMessageBroker.cs | 4 ++ .../CucumberMessagePublisher.cs | 60 +++++++++++++++++-- .../CucumberCompatibilityTests.cs | 18 ++++++ 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 777cdee28..87fe56f4f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -14,6 +14,7 @@ namespace Reqnroll.CucumberMesssages public interface ICucumberMessageBroker { + bool Enabled { get; } void Complete(string cucumberMessageSource); void Publish(ReqnrollCucumberMessage message); } @@ -21,6 +22,9 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { private IObjectContainer _objectContainer; + + public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; + //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 296727ccb..28916baa2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -24,6 +24,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi private ICucumberMessageBroker broker; private IObjectContainer objectContainer; private ConcurrentDictionary featureProcessorsByFeatureName = new(); + bool Enabled = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -57,14 +58,23 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; + // if not, then this is disabled for this feature + // if true, then it checks with the broker to confirm that a listener/sink has been registered + Enabled = broker.Enabled; + if (!Enabled) + return; + + var featureEnabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; var featureProc = new FeatureEventProcessor { Name = featureName, - Enabled = enabled + Enabled = featureEnabled }; + // todo: need a lock around this if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) { // This feature has already been started by another thread (executing a different scenario) @@ -78,7 +88,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); - if (!enabled) + if (!featureEnabled) return; ProcessEvent(featureStartedEvent, featureName); @@ -86,6 +96,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { + if (!Enabled) + return; + + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureProcessor = featureProcessorsByFeatureName[featureName]; @@ -119,55 +133,93 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { + if (!Enabled) + return; + + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(scenarioStartedEvent, featureName); } private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { + if (!Enabled) + return; + + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(scenarioFinishedEvent, featureName); } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { + if (!Enabled) + return; + + var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(stepStartedEvent, featureName); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { + if (!Enabled) + return; + + var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(stepFinishedEvent, featureName); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { + if (!Enabled) + return; + + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingStartedEvent, featureName); } private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) { + if (!Enabled) + return; + + var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingEvent, featureName); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { + if (!Enabled) + return; + + var featureName = attachmentAddedEvent.FeatureName; ProcessEvent(attachmentAddedEvent, featureName); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) { - ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); + if (!Enabled) + return; + + + ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); } private void ProcessEvent(ExecutionEvent anEvent, string featureName) { + if (!Enabled) + return; + + var featureProcessor = featureProcessorsByFeatureName[featureName]; + if (!featureProcessor.Enabled) + return; featureProcessor.ProcessEvent(anEvent); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7220109e9..b38e389ab 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -2,6 +2,7 @@ using Cucumber.Messages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; +using Newtonsoft.Json.Bson; using Reqnroll; using System.Reflection; @@ -10,6 +11,23 @@ namespace CucumberMessages.CompatibilityTests [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { + [TestMethod] + public void NullTest() + { + // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else + + AddFeatureFile(""" + Feature: Cucumber Messages Null Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } [TestMethod] public void SmokeTest() { From d1dbcdcde1dcb825eb05e79073773ea1e0983224 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 08:47:59 -0500 Subject: [PATCH 043/218] Refactored Environment Info away from EnvironmentWrapper --- ...atureFileCodeBehindTaskContainerBuilder.cs | 2 +- Reqnroll/Analytics/AnalyticsEventProvider.cs | 24 ++--- .../CucumberMessageFactory.cs | 69 ++++++------- .../EnvironmentInfoProvider.cs | 97 +++++++++++++++++++ .../EnvironmentAccess/EnvironmentWrapper.cs | 77 --------------- .../IEnvironmentInfoProvider.cs | 11 +++ .../EnvironmentAccess/IEnvironmentWrapper.cs | 10 -- .../DefaultDependencyProvider.cs | 1 + .../Analytics/AnalyticsEventProviderTests.cs | 12 ++- 9 files changed, 167 insertions(+), 136 deletions(-) create mode 100644 Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs create mode 100644 Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs diff --git a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs index fafbf4afa..5ea8ba55d 100644 --- a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs +++ b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs @@ -40,7 +40,7 @@ public IObjectContainer BuildRootContainer( objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); - + objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); diff --git a/Reqnroll/Analytics/AnalyticsEventProvider.cs b/Reqnroll/Analytics/AnalyticsEventProvider.cs index 301835a22..6c1f8dc07 100644 --- a/Reqnroll/Analytics/AnalyticsEventProvider.cs +++ b/Reqnroll/Analytics/AnalyticsEventProvider.cs @@ -13,13 +13,13 @@ namespace Reqnroll.Analytics public class AnalyticsEventProvider : IAnalyticsEventProvider { private readonly IUserUniqueIdStore _userUniqueIdStore; - private readonly IEnvironmentWrapper _environmentWrapper; + private readonly IEnvironmentInfoProvider _environmentInfoProvider; private readonly string _unitTestProvider; - public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentWrapper environmentWrapper) + public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentInfoProvider environmentInfoProvider) { _userUniqueIdStore = userUniqueIdStore; - _environmentWrapper = environmentWrapper; + _environmentInfoProvider = environmentInfoProvider; _unitTestProvider = unitTestProviderConfiguration.UnitTestProvider; } @@ -27,11 +27,11 @@ public ReqnrollProjectCompilingEvent CreateProjectCompilingEvent(string msbuildV { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); - string buildServerName = _environmentWrapper.GetBuildServerName(); - bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); string hashedAssemblyName = ToSha256(assemblyName); - string platform = _environmentWrapper.GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var compiledEvent = new ReqnrollProjectCompilingEvent( @@ -56,13 +56,13 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); - string targetFramework = _environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; - bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); - string buildServerName = _environmentWrapper.GetBuildServerName(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string targetFramework = _environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); string hashedAssemblyName = ToSha256(testAssemblyName); - string platform = _environmentWrapper.GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var runningEvent = new ReqnrollProjectRunningEvent( diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 94699675c..3660b1364 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -254,40 +254,17 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat }; } - - #region utility methods - public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) - { - string signature = GenerateSignature(stepDefinition); - - return $"{stepDefinition.SourceExpression}({signature})"; - } - - public static string CanonicalizeHookBinding(IHookBinding hookBinding) - { - string signature = GenerateSignature(hookBinding); - return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; - } - - private static string GenerateSignature(IBinding stepDefinition) - { - return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; - } - public static string Base64EncodeFile(string filePath) - { - byte[] fileBytes = File.ReadAllBytes(filePath); - return Convert.ToBase64String(fileBytes); - } - public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) { - var environmentWrapper = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + var featureContainer = featureStartedEvent.FeatureContext.FeatureContainer; + var environmentInfoProvider = featureContainer.Resolve(); + var environmentWrapper = featureContainer.Resolve(); - var implementation = new Product("Reqnroll", environmentWrapper.GetReqnrollVersion()); - string targetFramework = environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + var implementation = new Product("Reqnroll", environmentInfoProvider.GetReqnrollVersion()); + string targetFramework = environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; var runTime = new Product("dotNet", targetFramework); - var os = new Product(environmentWrapper.GetOSPlatform(), RuntimeInformation.OSDescription); + var os = new Product(environmentInfoProvider.GetOSPlatform(), RuntimeInformation.OSDescription); var cpu = RuntimeInformation.ProcessArchitecture switch { @@ -298,9 +275,9 @@ public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) _ => new Product(null, null), }; - var ci_name = environmentWrapper.GetBuildServerName(); + var ci_name = environmentInfoProvider.GetBuildServerName(); - var ci = ToCi(ci_name, environmentWrapper); + var ci = ToCi(ci_name, environmentInfoProvider, environmentWrapper); return Envelope.Create(new Meta( (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], @@ -311,12 +288,12 @@ public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) ci)); } - private static Ci ToCi(string ci_name, IEnvironmentWrapper environmentWrapper) + private static Ci ToCi(string ci_name, IEnvironmentInfoProvider environmentInfoProvider, IEnvironmentWrapper environmentWrapper) { //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. // Until then, these will be hard coded as null if (String.IsNullOrEmpty(ci_name)) return null; - + var git = ToGit(environmentWrapper); return new Ci(ci_name, null, null, git); @@ -340,6 +317,32 @@ private static Git ToGit(IEnvironmentWrapper environmentWrapper) ); return git; } + + #region utility methods + public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + { + string signature = GenerateSignature(stepDefinition); + + return $"{stepDefinition.SourceExpression}({signature})"; + } + + public static string CanonicalizeHookBinding(IHookBinding hookBinding) + { + string signature = GenerateSignature(hookBinding); + return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + } + + private static string GenerateSignature(IBinding stepDefinition) + { + return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + } + public static string Base64EncodeFile(string filePath) + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } + + #endregion } } \ No newline at end of file diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs new file mode 100644 index 000000000..6a9a1cc50 --- /dev/null +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -0,0 +1,97 @@ +using Reqnroll.CommonModels; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace Reqnroll.EnvironmentAccess +{ + public class EnvironmentInfoProvider : IEnvironmentInfoProvider + { + private readonly IEnvironmentWrapper environmentWrapper; + + public EnvironmentInfoProvider(IEnvironmentWrapper environmentWrapper) + { + this.environmentWrapper = environmentWrapper; + } + + public string GetOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "OSX"; + } + + throw new InvalidOperationException("Platform cannot be identified"); + } + + private readonly Dictionary buildServerTypes + = new Dictionary { + { "TF_BUILD","Azure Pipelines"}, + { "TEAMCITY_VERSION","TeamCity"}, + { "JENKINS_HOME","Jenkins"}, + { "GITHUB_ACTIONS","GitHub Actions"}, + { "GITLAB_CI","GitLab CI/CD"}, + { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, + { "TRAVIS","Travis CI"}, + { "APPVEYOR","AppVeyor"}, + { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, + { "bamboo_agentId", "Atlassian Bamboo" }, + { "CIRCLECI", "CircleCI" }, + { "GO_PIPELINE_NAME", "GoCD" }, + { "BUDDY", "Buddy" }, + { "NEVERCODE", "Nevercode" }, + { "SEMAPHORE", "SEMAPHORE" }, + { "BROWSERSTACK_USERNAME", "BrowserStack" }, + { "CF_BUILD_ID", "Codefresh" }, + { "TentacleVersion", "Octopus Deploy" }, + + { "CI_NAME", "CodeShip" } + }; + + public string GetBuildServerName() + { + foreach (var buildServerType in buildServerTypes) + { + var envVariable = environmentWrapper.GetEnvironmentVariable(buildServerType.Key); + if (envVariable is ISuccess) + return buildServerType.Value; + } + return null; + } + + public bool IsRunningInDockerContainer() + { + return environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + } + + public string GetReqnrollVersion() + { + return VersionInfo.AssemblyInformationalVersion; + } + public string GetNetCoreVersion() + { + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; + } + + } +} diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 6bfd79e74..40d9f0ef3 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -40,82 +40,5 @@ public void SetEnvironmentVariable(string name, string value) public string GetCurrentDirectory() => Environment.CurrentDirectory; - public string GetOSPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Linux"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "OSX"; - } - - throw new InvalidOperationException("Platform cannot be identified"); - } - - private readonly Dictionary buildServerTypes - = new Dictionary { - { "TF_BUILD","Azure Pipelines"}, - { "TEAMCITY_VERSION","TeamCity"}, - { "JENKINS_HOME","Jenkins"}, - { "GITHUB_ACTIONS","GitHub Actions"}, - { "GITLAB_CI","GitLab CI/CD"}, - { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, - { "TRAVIS","Travis CI"}, - { "APPVEYOR","AppVeyor"}, - { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, - { "bamboo_agentId", "Atlassian Bamboo" }, - { "CIRCLECI", "CircleCI" }, - { "GO_PIPELINE_NAME", "GoCD" }, - { "BUDDY", "Buddy" }, - { "NEVERCODE", "Nevercode" }, - { "SEMAPHORE", "SEMAPHORE" }, - { "BROWSERSTACK_USERNAME", "BrowserStack" }, - { "CF_BUILD_ID", "Codefresh" }, - { "TentacleVersion", "Octopus Deploy" }, - - { "CI_NAME", "CodeShip" } - }; - - public string GetBuildServerName() - { - foreach (var buildServerType in buildServerTypes) - { - var envVariable = GetEnvironmentVariable(buildServerType.Key); - if (envVariable is ISuccess) - return buildServerType.Value; - } - return null; - } - - public bool IsRunningInDockerContainer() - { - return GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; - } - - public string GetReqnrollVersion() - { - return VersionInfo.AssemblyInformationalVersion; - } - public string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - - return null; - } - } } diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs new file mode 100644 index 000000000..562372ec4 --- /dev/null +++ b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.EnvironmentAccess +{ + public interface IEnvironmentInfoProvider + { + string GetOSPlatform(); + string GetBuildServerName(); + bool IsRunningInDockerContainer(); + string GetReqnrollVersion(); + string GetNetCoreVersion(); + } +} \ No newline at end of file diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs index d6edfd6bc..5e88ba8bc 100644 --- a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs @@ -13,15 +13,5 @@ public interface IEnvironmentWrapper void SetEnvironmentVariable(string name, string value); string GetCurrentDirectory(); - - string GetOSPlatform(); - - string GetBuildServerName(); - - bool IsRunningInDockerContainer(); - - string GetReqnrollVersion(); - - string GetNetCoreVersion(); } } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index d4f0e2d98..8e0810ccf 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -72,6 +72,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); diff --git a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs index 0d28c52c0..33e26f385 100644 --- a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs @@ -20,7 +20,9 @@ public void Should_return_the_build_server_name_in_Compiling_Event() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); environmentMock .Setup(m => m.GetEnvironmentVariable("TF_BUILD")) @@ -36,7 +38,9 @@ public void Should_return_the_build_server_name_in_Running_Event() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); environmentMock .Setup(m => m.GetEnvironmentVariable("TEAMCITY_VERSION")) @@ -52,7 +56,9 @@ public void Should_return_null_for_the_build_server_name_when_not_detected() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); var compilingEvent = sut.CreateProjectRunningEvent(null); From 367a595e20a1f5aa7cf261d96681ebb34a9f51ba Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:55:53 -0500 Subject: [PATCH 044/218] Update nuget dependencies to match Main --- .../Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- .../CucumberMessages.CompatibilityTests.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 9624408fd..4df9424dd 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -11,7 +11,7 @@ - + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 1c4df4f41..31421d204 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,9 +120,9 @@ - - - + + + From d808f27a7ec162a57210a96009a215e57daeff7b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 5 Sep 2024 08:22:01 -0500 Subject: [PATCH 045/218] Establishes cross-reference data structures to find Messages by type and ID. Validates that top-level Messages match by Type and Count. --- .../CucumberCompatibilityTests.cs | 48 ++++- ...CucumberMessages.CompatibilityTests.csproj | 1 + .../CucumberMessagesValidator.cs | 192 ++++++++++++++++++ 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index b38e389ab..818e94ac8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -5,6 +5,8 @@ using Newtonsoft.Json.Bson; using Reqnroll; using System.Reflection; +using FluentAssertions; +using System.Text.Json; namespace CucumberMessages.CompatibilityTests { @@ -209,6 +211,9 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); + var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + validator.ResultShouldPassAllComparisonTests(); + ConfirmAllTestsRan(null); } @@ -221,10 +226,49 @@ private void AddUtilClassWithFileSystemPath() private IEnumerable GetExpectedResults(string scenarioName) { - var workingDirectory = Assembly.GetExecutingAssembly().GetAssemblyLocation(); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory, $"{scenarioName}.feature.ndjson")); + var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); } + + private IEnumerable GetActualResults(string scenarioName) + { + var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; + var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; + var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); + + foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); + } } + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } + } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 31421d204..613faa012 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,6 +120,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs new file mode 100644 index 000000000..4cc8a13eb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -0,0 +1,192 @@ +using FluentAssertions; +using Io.Cucumber.Messages.Types; +using System.Diagnostics.Eventing.Reader; + +namespace CucumberMessages.CompatibilityTests +{ + internal class CucumberMessagesValidator + { + private IEnumerable actualEnvelopes; + private IEnumerable expectedEnvelopes; + + // cross-reference metadata + private Dictionary> actuals_IDsByType = new(); + private Dictionary> expecteds_IDsByType = new(); + private Dictionary> actuals_elementsByType = new(); + private Dictionary> expecteds_elementsByType = new(); + private Dictionary> actuals_elementsByID = new(); + private Dictionary> expecteds_elementsByID = new(); + + public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) + { + actualEnvelopes = actual; + expectedEnvelopes = expected; + + SetupCrossReferences(actual, actuals_IDsByType, actuals_elementsByType, actuals_elementsByID); + SetupCrossReferences(expected, expecteds_IDsByType, expecteds_elementsByType, expecteds_elementsByID); + + } + private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) + { + foreach (var message in messages) + { + var msg = message.Content(); + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + } + } + private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) + { + if (!IDsByType.ContainsKey(msg.GetType())) + { + IDsByType.Add(msg.GetType(), new HashSet()); + } + IDsByType[msg.GetType()].Add(msg.Id()); + } + + private static void InsertIntoElementsById(object msg, Dictionary> elementsByID) + { + if (!elementsByID.ContainsKey(msg.Id())) + { + elementsByID.Add(msg.Id(), new HashSet()); + } + elementsByID[msg.Id()].Add(msg); + } + + private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) + { + if (!elementsByType.ContainsKey(msg.GetType())) + { + elementsByType.Add(msg.GetType(), new HashSet()); + } + elementsByType[msg.GetType()].Add(msg); + } + + internal void ResultShouldPassAllComparisonTests() + { + ShouldPassBasicStructuralChecks(actualEnvelopes, actualEnvelopes); + } + + internal void ResultShouldPassBasicSanityChecks() + { + throw new NotImplementedException(); + } + internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnumerable expected) + { + actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); + + //todo: modify this to use lists of types from actual and expected and use FluentAssertions directly + foreach (var messageType in MessageExtensions.EnvelopeContentTypes) + { + if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Count().Should().BeGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); + } + } + } + + internal static class MessageExtensions + { + public static List MessagesWithIds = new(){ typeof(Background), + typeof(Examples), + typeof(Hook), + typeof(ParameterType), + typeof(Pickle), + typeof(PickleStep), + typeof(Rule), + typeof(Scenario), + typeof(Step), + typeof(StepDefinition), + typeof(TableRow), + typeof(Tag), + typeof(TestCase), + typeof(TestCaseStarted), + typeof(TestStep) + }; + + internal static bool HasId(this object element) + { + return MessagesWithIds.Contains(element.GetType()); + } + internal static string Id(this object message) + { + return message switch + { + Background bgd => bgd.Id, + Examples ex => ex.Id, + Hook hook => hook.Id, + ParameterType pt => pt.Id, + Pickle p => p.Id, + PickleStep ps => ps.Id, + Rule r => r.Id, + Scenario sc => sc.Id, + Step st => st.Id, + StepDefinition sd => sd.Id, + TableRow tr => tr.Id, + Tag tag => tag.Id, + TestCase tc => tc.Id, + TestCaseStarted tcs => tcs.Id, + TestStep ts => ts.Id, + _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") + }; + } + internal static List EnvelopeContentTypes = new() + { + typeof(Attachment), + typeof(GherkinDocument), + typeof(Hook), + typeof(Meta), + typeof(ParameterType), + typeof(ParseError), + typeof(Pickle), + typeof(Source), + typeof(StepDefinition), + typeof(TestCase), + typeof(TestCaseFinished), + typeof(TestCaseStarted), + typeof(TestRunFinished), + typeof(TestRunStarted), + typeof(TestStepFinished), + typeof(TestStepStarted), + typeof(UndefinedParameterType) + }; + internal static object Content(this Envelope envelope) + { + object? result = null; + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + else if (envelope.Hook != null) { result = envelope.Hook; } + else if (envelope.Meta != null) { result = envelope.Meta; } + else if (envelope.ParameterType != null) { result = envelope.ParameterType; } + else if (envelope.ParseError != null) { result = envelope.ParseError; } + else if (envelope.Pickle != null) { result = envelope.Pickle; } + else if (envelope.Source != null) { result = envelope.Source; } + else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } + else if (envelope.TestCase != null) { result = envelope.TestCase; } + else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } + else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } + return result!; + } + } +} \ No newline at end of file From 881599568726f3f48674f33ddf77015992e901fb Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:53:18 -0500 Subject: [PATCH 046/218] Reorganization - fixed namespace naming mistake; Introduced visitor pattern over the Messages types. --- .../Generation/UnitTestFeatureGenerator.cs | 2 +- .../AttachmentAddedEventWrapper.cs | 2 +- .../CucumberMessagEnumConverter.cs | 2 +- .../CucumberMessageBroker.cs | 2 +- .../CucumberMessageExtensions.cs | 95 ++ .../CucumberMessageFactory.cs | 2 +- .../CucumberMessagePublisher.cs | 2 +- .../CucumberMessageTransformer.cs | 2 +- .../CucumberMessageVisitor.cs | 186 ++++ .../CucumberMessage_TraversalVisitorBase.cs | 811 ++++++++++++++++++ .../FeatureEventProcessor.cs | 2 +- .../FeatureLevelCucumberMessages.cs | 2 +- .../FileExtensionToMIMETypeMap.cs | 2 +- .../CucumberMesssages/HookStepProcessor.cs | 2 +- .../ICucumberMessagePublisher.cs | 2 +- .../CucumberMesssages/ICucumberMessageSink.cs | 2 +- .../ICucumberMessageVisitor.cs | 66 ++ Reqnroll/CucumberMesssages/IStepProcessor.cs | 2 +- .../CucumberMesssages/IdGeneratorFactory.cs | 2 +- .../CucumberMesssages/NdjsonSerializer.cs | 2 +- .../OutputAddedEventWrapper.cs | 2 +- .../ReqnrollCucumberMessage.cs | 2 +- .../ScenarioEventProcessor.cs | 2 +- .../ScenarioStepProcessor.cs | 2 +- .../CucumberMesssages/StepProcessorBase.cs | 2 +- Reqnroll/FeatureInfo.cs | 2 +- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTests.cs | 15 +- .../CucumberMessagesValidator.cs | 94 +- 29 files changed, 1184 insertions(+), 129 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 1e17b09b3..0b0ca86b6 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -5,7 +5,7 @@ using System.Reflection; using Gherkin.CucumberMessages; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs index 25c2eb53c..44f371407 100644 --- a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs +++ b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class AttachmentAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs index c5fb0457e..3722e8c4e 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Cucumber.Messages +namespace Reqnroll.CucumberMessages { internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 87fe56f4f..485667467 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessageBroker diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs new file mode 100644 index 000000000..6cb1a658c --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -0,0 +1,95 @@ +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages +{ + public static class CucumberMessageExtensions + { + public static List MessagesWithIds = new(){ typeof(Background), + typeof(Examples), + typeof(Hook), + typeof(ParameterType), + typeof(Pickle), + typeof(PickleStep), + typeof(Rule), + typeof(Scenario), + typeof(Step), + typeof(StepDefinition), + typeof(TableRow), + typeof(Tag), + typeof(TestCase), + typeof(TestCaseStarted), + typeof(TestStep) + }; + + public static bool HasId(this object element) + { + return MessagesWithIds.Contains(element.GetType()); + } + public static string Id(this object message) + { + return message switch + { + Background bgd => bgd.Id, + Examples ex => ex.Id, + Hook hook => hook.Id, + ParameterType pt => pt.Id, + Pickle p => p.Id, + PickleStep ps => ps.Id, + Rule r => r.Id, + Scenario sc => sc.Id, + Step st => st.Id, + StepDefinition sd => sd.Id, + TableRow tr => tr.Id, + Tag tag => tag.Id, + TestCase tc => tc.Id, + TestCaseStarted tcs => tcs.Id, + TestStep ts => ts.Id, + _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") + }; + } + public static List EnvelopeContentTypes = new() + { + typeof(Attachment), + typeof(GherkinDocument), + typeof(Hook), + typeof(Meta), + typeof(ParameterType), + typeof(ParseError), + typeof(Pickle), + typeof(Source), + typeof(StepDefinition), + typeof(TestCase), + typeof(TestCaseFinished), + typeof(TestCaseStarted), + typeof(TestRunFinished), + typeof(TestRunStarted), + typeof(TestStepFinished), + typeof(TestStepStarted), + typeof(UndefinedParameterType) + }; + public static object Content(this Envelope envelope) + { + object result = null; + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + else if (envelope.Hook != null) { result = envelope.Hook; } + else if (envelope.Meta != null) { result = envelope.Meta; } + else if (envelope.ParameterType != null) { result = envelope.ParameterType; } + else if (envelope.ParseError != null) { result = envelope.ParseError; } + else if (envelope.Pickle != null) { result = envelope.Pickle; } + else if (envelope.Source != null) { result = envelope.Source; } + else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } + else if (envelope.TestCase != null) { result = envelope.TestCase; } + else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } + else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } + return result; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 3660b1364..e5b96dd01 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class CucumberMessageFactory { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 28916baa2..b6f18124c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -17,7 +17,7 @@ using System.Linq; using System.Diagnostics; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index e6e8135a3..c4e019639 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { /// /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace diff --git a/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs b/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs new file mode 100644 index 000000000..c8d1c9274 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages; + +public class CucumberMessageVisitor +{ + public static void Accept(ICucumberMessageVisitor visitor, object message) + { + switch (message) + { + // Existing cases + case Envelope envelope: + visitor.Visit(envelope); + break; + case Attachment attachment: + visitor.Visit(attachment); + break; + case GherkinDocument gherkinDocument: + visitor.Visit(gherkinDocument); + break; + case Feature feature: + visitor.Visit(feature); + break; + case FeatureChild featureChild: + visitor.Visit(featureChild); + break; + case Rule rule: + visitor.Visit(rule); + break; + case RuleChild ruleChild: + visitor.Visit(ruleChild); + break; + case Background background: + visitor.Visit(background); + break; + case Scenario scenario: + visitor.Visit(scenario); + break; + case Examples examples: + visitor.Visit(examples); + break; + case Step step: + visitor.Visit(step); + break; + case TableRow tableRow: + visitor.Visit(tableRow); + break; + case TableCell tableCell: + visitor.Visit(tableCell); + break; + case Tag tag: + visitor.Visit(tag); + break; + case Pickle pickle: + visitor.Visit(pickle); + break; + case PickleStep pickleStep: + visitor.Visit(pickleStep); + break; + case PickleStepArgument pickleStepArgument: + visitor.Visit(pickleStepArgument); + break; + case PickleTable pickleTable: + visitor.Visit(pickleTable); + break; + case PickleTableRow pickleTableRow: + visitor.Visit(pickleTableRow); + break; + case PickleTableCell pickleTableCell: + visitor.Visit(pickleTableCell); + break; + case PickleTag pickleTag: + visitor.Visit(pickleTag); + break; + case TestCase testCase: + visitor.Visit(testCase); + break; + case TestCaseStarted testCaseStarted: + visitor.Visit(testCaseStarted); + break; + case TestCaseFinished testCaseFinished: + visitor.Visit(testCaseFinished); + break; + case TestStep testStep: + visitor.Visit(testStep); + break; + case TestStepStarted testStepStarted: + visitor.Visit(testStepStarted); + break; + case TestStepFinished testStepFinished: + visitor.Visit(testStepFinished); + break; + case TestStepResult testStepResult: + visitor.Visit(testStepResult); + break; + case Hook hook: + visitor.Visit(hook); + break; + case StepDefinition stepDefinition: + visitor.Visit(stepDefinition); + break; + case ParameterType parameterType: + visitor.Visit(parameterType); + break; + case UndefinedParameterType undefinedParameterType: + visitor.Visit(undefinedParameterType); + break; + case SourceReference sourceReference: + visitor.Visit(sourceReference); + break; + case Duration duration: + visitor.Visit(duration); + break; + case Timestamp timestamp: + visitor.Visit(timestamp); + break; + case Io.Cucumber.Messages.Types.Exception exception: + visitor.Visit(exception); + break; + case Meta meta: + visitor.Visit(meta); + break; + case Product product: + visitor.Visit(product); + break; + case Ci ci: + visitor.Visit(ci); + break; + case Git git: + visitor.Visit(git); + break; + case Source source: + visitor.Visit(source); + break; + case Comment comment: + visitor.Visit(comment); + break; + case Io.Cucumber.Messages.Types.DataTable dataTable: + visitor.Visit(dataTable); + break; + case DocString docString: + visitor.Visit(docString); + break; + case Group group: + visitor.Visit(group); + break; + case JavaMethod javaMethod: + visitor.Visit(javaMethod); + break; + case JavaStackTraceElement javaStackTraceElement: + visitor.Visit(javaStackTraceElement); + break; + case Location location: + visitor.Visit(location); + break; + case ParseError parseError: + visitor.Visit(parseError); + break; + case PickleDocString pickleDocString: + visitor.Visit(pickleDocString); + break; + case StepDefinitionPattern stepDefinitionPattern: + visitor.Visit(stepDefinitionPattern); + break; + case StepMatchArgument stepMatchArgument: + visitor.Visit(stepMatchArgument); + break; + case StepMatchArgumentsList stepMatchArgumentsList: + visitor.Visit(stepMatchArgumentsList); + break; + case TestRunStarted testRunStarted: + visitor.Visit(testRunStarted); + break; + case TestRunFinished testRunFinished: + visitor.Visit(testRunFinished); + break; + + default: + throw new ArgumentException($"Unsupported message type:{message.GetType().Name}", nameof(message)); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs new file mode 100644 index 000000000..0bb4ac6d7 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs @@ -0,0 +1,811 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages +{ + public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor + { + private void Accept(object message) + { + if (message != null) CucumberMessageVisitor.Accept(this, message); + } + + public virtual void Visit(Envelope envelope) + { + OnVisiting(envelope); + Accept(envelope.Content()); + OnVisited(envelope); + } + + public virtual void Visit(Attachment attachment) + { + OnVisiting(attachment); + OnVisited(attachment); + } + + public virtual void Visit(GherkinDocument gherkinDocument) + { + OnVisiting(gherkinDocument); + + if (gherkinDocument.Feature != null) + Accept(gherkinDocument.Feature); + + OnVisited(gherkinDocument); + } + + public virtual void Visit(Feature feature) + { + OnVisiting(feature); + foreach (var featureChild in feature.Children) + { + Accept(featureChild); + } + OnVisited(feature); + } + + public virtual void Visit(FeatureChild featureChild) + { + OnVisiting(featureChild); + if (featureChild.Rule != null) + Accept(featureChild.Rule); + else if (featureChild.Background != null) + Accept(featureChild.Background); + else if (featureChild.Scenario != null) + Accept(featureChild.Scenario); + OnVisited(featureChild); + } + + public virtual void Visit(Rule rule) + { + OnVisiting(rule); + foreach (var ruleChild in rule.Children) + { + Accept(ruleChild); + } + foreach (var tag in rule.Tags) + { + Accept(tag); + } + OnVisited(rule); + } + + public virtual void Visit(RuleChild ruleChild) + { + OnVisiting(ruleChild); + if (ruleChild.Background != null) + Accept(ruleChild.Background); + else if (ruleChild.Scenario != null) + Accept(ruleChild.Scenario); + OnVisited(ruleChild); + } + + public virtual void Visit(Background background) + { + OnVisiting(background); + Accept(background.Location); + foreach (var step in background.Steps) + { + Accept(step); + } + OnVisited(background); + } + + public virtual void Visit(Scenario scenario) + { + OnVisiting(scenario); + Accept(scenario.Location); + foreach (var tag in scenario.Tags) + { + Accept(tag); + } + foreach (var step in scenario.Steps) + { + Accept(step); + } + foreach (var example in scenario.Examples) + { + Accept(example); + } + OnVisited(scenario); + } + + public virtual void Visit(Examples examples) + { + OnVisiting(examples); + Accept(examples.Location); + foreach (var tag in examples.Tags) + { + Accept(tag); + } + Accept(examples.TableHeader); + foreach (var tableRow in examples.TableBody) + { + Accept(tableRow); + } + OnVisited(examples); + } + + public virtual void Visit(Step step) + { + OnVisiting(step); + Accept(step.Location); + Accept(step.DocString); + Accept(step.DataTable); + OnVisited(step); + } + + public virtual void Visit(TableRow tableRow) + { + OnVisiting(tableRow); + Accept(tableRow.Location); + foreach (var tableCell in tableRow.Cells) + { + Accept(tableCell); + } + OnVisited(tableRow); + } + + public virtual void Visit(TableCell tableCell) + { + OnVisiting(tableCell); + Accept(tableCell.Location); + OnVisited(tableCell); + } + + public virtual void Visit(Tag tag) + { + OnVisiting(tag); + Accept(tag.Location); + OnVisited(tag); + } + + public virtual void Visit(Pickle pickle) + { + OnVisiting(pickle); + foreach (var pickleStep in pickle.Steps) + { + Accept(pickleStep); + } + foreach (var tag in pickle.Tags) + { + Accept(tag); + } + OnVisited(pickle); + } + + public virtual void Visit(PickleStep pickleStep) + { + OnVisiting(pickleStep); + Accept(pickleStep.Argument); + OnVisited(pickleStep); + } + + public virtual void Visit(PickleStepArgument pickleStepArgument) + { + OnVisiting(pickleStepArgument); + if (pickleStepArgument.DataTable != null) + Accept(pickleStepArgument.DataTable); + else if (pickleStepArgument.DocString != null) + Accept(pickleStepArgument.DocString); + OnVisited(pickleStepArgument); + } + + public virtual void Visit(PickleTable pickleTable) + { + OnVisiting(pickleTable); + foreach (var pickleTableRow in pickleTable.Rows) + { + Accept(pickleTableRow); + } + OnVisited(pickleTable); + } + + public virtual void Visit(PickleTableRow pickleTableRow) + { + OnVisiting(pickleTableRow); + foreach (var pickleTableCell in pickleTableRow.Cells) + { + Accept(pickleTableCell); + } + OnVisited(pickleTableRow); + } + + public virtual void Visit(PickleTableCell pickleTableCell) + { + OnVisiting(pickleTableCell); + OnVisited(pickleTableCell); + } + + public virtual void Visit(PickleTag pickleTag) + { + OnVisiting(pickleTag); + OnVisited(pickleTag); + } + + public virtual void Visit(TestCase testCase) + { + OnVisiting(testCase); + foreach (var step in testCase.TestSteps) + { + Accept(step); + } + OnVisited(testCase); + } + + public virtual void Visit(TestCaseStarted testCaseStarted) + { + OnVisiting(testCaseStarted); + Accept(testCaseStarted.Timestamp); + OnVisited(testCaseStarted); + } + + public virtual void Visit(TestCaseFinished testCaseFinished) + { + OnVisiting(testCaseFinished); + Accept(testCaseFinished.Timestamp); + OnVisited(testCaseFinished); + } + + public virtual void Visit(TestStep testStep) + { + OnVisiting(testStep); + foreach (var argumentList in testStep.StepMatchArgumentsLists) + { + Accept(argumentList); + } + OnVisited(testStep); + } + + public virtual void Visit(TestStepStarted testStepStarted) + { + OnVisiting(testStepStarted); + Accept(testStepStarted.Timestamp); + OnVisited(testStepStarted); + } + + public virtual void Visit(TestStepFinished testStepFinished) + { + OnVisiting(testStepFinished); + Accept(testStepFinished.TestStepResult); + Accept(testStepFinished.Timestamp); + OnVisited(testStepFinished); + } + + public virtual void Visit(TestStepResult testStepResult) + { + OnVisiting(testStepResult); + Accept(testStepResult.Duration); + Accept(testStepResult.Exception); + OnVisited(testStepResult); + } + + public virtual void Visit(Hook hook) + { + OnVisiting(hook); + Accept(hook.SourceReference); + OnVisited(hook); + } + + public virtual void Visit(StepDefinition stepDefinition) + { + OnVisiting(stepDefinition); + Accept(stepDefinition.Pattern); + Accept(stepDefinition.SourceReference); + OnVisited(stepDefinition); + } + + public virtual void Visit(ParameterType parameterType) + { + OnVisiting(parameterType); + Accept(parameterType.SourceReference); + OnVisited(parameterType); + } + + public virtual void Visit(UndefinedParameterType undefinedParameterType) + { + OnVisiting(undefinedParameterType); + OnVisited(undefinedParameterType); + } + + public virtual void Visit(SourceReference sourceReference) + { + OnVisiting(sourceReference); + if (sourceReference.Location != null) Accept(sourceReference.Location); + else if (sourceReference.JavaMethod != null) Accept(sourceReference.JavaMethod); + else if (sourceReference.JavaStackTraceElement != null) Accept(sourceReference.JavaStackTraceElement); + OnVisited(sourceReference); + } + + public virtual void Visit(Duration duration) + { + OnVisiting(duration); + OnVisited(duration); + } + + public virtual void Visit(Timestamp timestamp) + { + OnVisiting(timestamp); + OnVisited(timestamp); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.Exception exception) + { + OnVisiting(exception); + OnVisited(exception); + } + + public virtual void Visit(Meta meta) + { + OnVisiting(meta); + Accept(meta.Implementation); + Accept(meta.Runtime); + Accept(meta.Os); + Accept(meta.Cpu); + Accept(meta.Ci); + OnVisited(meta); + } + + public virtual void Visit(Product product) + { + OnVisiting(product); + OnVisited(product); + } + + public virtual void Visit(Ci ci) + { + OnVisiting(ci); + Accept(ci.Git); + OnVisited(ci); + } + + public virtual void Visit(Git git) + { + OnVisiting(git); + OnVisited(git); + } + + public virtual void Visit(Source source) + { + OnVisiting(source); + OnVisited(source); + } + + public virtual void Visit(Comment comment) + { + OnVisiting(comment); + Accept(comment.Location); + OnVisited(comment); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) + { + OnVisiting(dataTable); + Accept(dataTable.Location); + foreach (var row in dataTable.Rows) + { + Accept(row); + } + OnVisited(dataTable); + } + + public virtual void Visit(DocString docString) + { + OnVisiting(docString); + Accept(docString.Location); + OnVisited(docString); + } + + public virtual void Visit(Group group) + { + OnVisiting(group); + foreach (var child in group.Children) + { + Accept(child); + } + OnVisited(group); + } + + public virtual void Visit(JavaMethod javaMethod) + { + OnVisiting(javaMethod); + OnVisited(javaMethod); + } + + public virtual void Visit(JavaStackTraceElement javaStackTraceElement) + { + OnVisiting(javaStackTraceElement); + OnVisited(javaStackTraceElement); + } + + public virtual void Visit(Location location) + { + OnVisiting(location); + OnVisited(location); + } + + public virtual void Visit(ParseError parseError) + { + OnVisiting(parseError); + Accept(parseError.Source); + OnVisited(parseError); + } + + public virtual void Visit(PickleDocString pickleDocString) + { + OnVisiting(pickleDocString); + OnVisited(pickleDocString); + } + + public virtual void Visit(StepDefinitionPattern stepDefinitionPattern) + { + OnVisiting(stepDefinitionPattern); + OnVisited(stepDefinitionPattern); + } + + public virtual void Visit(StepMatchArgument stepMatchArgument) + { + OnVisiting(stepMatchArgument); + Accept(stepMatchArgument.Group); + OnVisited(stepMatchArgument); + } + + public virtual void Visit(StepMatchArgumentsList stepMatchArgumentsList) + { + OnVisiting(stepMatchArgumentsList); + foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments) + { + Accept(stepMatchArgument); + } + OnVisited(stepMatchArgumentsList); + } + + public virtual void Visit(TestRunStarted testRunStarted) + { + OnVisiting(testRunStarted); + Accept(testRunStarted.Timestamp); + OnVisited(testRunStarted); + } + + public virtual void Visit(TestRunFinished testRunFinished) + { + OnVisiting(testRunFinished); + Accept(testRunFinished.Timestamp); + Accept(testRunFinished.Exception); + OnVisited(testRunFinished); + } + + public virtual void OnVisiting(Attachment attachment) + { } + + public virtual void OnVisited(Attachment attachment) + { } + + public virtual void OnVisiting(Envelope envelope) + { } + + public virtual void OnVisited(Envelope envelope) + { } + + public virtual void OnVisiting(Feature feature) + { } + + public virtual void OnVisited(Feature feature) + { } + + public virtual void OnVisiting(FeatureChild featureChild) + { } + + public virtual void OnVisited(FeatureChild featureChild) + { } + + public virtual void OnVisiting(Examples examples) + { } + + public virtual void OnVisited(Examples examples) + { } + + public virtual void OnVisiting(Step step) + { } + + public virtual void OnVisited(Step step) + { } + + public virtual void OnVisiting(TableRow tableRow) + { } + + public virtual void OnVisited(TableRow tableRow) + { } + + public virtual void OnVisiting(TableCell tableCell) + { } + + public virtual void OnVisited(TableCell tableCell) + { } + + public virtual void OnVisiting(Tag tag) + { } + + public virtual void OnVisited(Tag tag) + { } + + public virtual void OnVisiting(Pickle pickle) + { } + + public virtual void OnVisited(Pickle pickle) + { } + + public virtual void OnVisiting(PickleStep pickleStep) + { } + + public virtual void OnVisited(PickleStep pickleStep) + { } + + public virtual void OnVisiting(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisited(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisiting(PickleTable pickleTable) + { } + + public virtual void OnVisited(PickleTable pickleTable) + { } + + public virtual void OnVisiting(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisited(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisiting(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisited(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisiting(PickleTag pickelTag) + { } + + public virtual void OnVisited(PickleTag pickelTag) + { } + + public virtual void OnVisiting(Rule rule) + { } + + public virtual void OnVisited(Rule rule) + { } + + public virtual void OnVisiting(RuleChild ruleChild) + { } + + public virtual void OnVisited(RuleChild ruleChild) + { } + + public virtual void OnVisiting(Background background) + { } + + public virtual void OnVisited(Background background) + { } + + public virtual void OnVisiting(Scenario scenario) + { } + + public virtual void OnVisited(Scenario scenario) + { } + + public virtual void OnVisiting(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisited(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisiting(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisited(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisiting(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisited(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisiting(TestStep testStep) + { } + + public virtual void OnVisited(TestStep testStep) + { } + + public virtual void OnVisiting(TestStepFinished testStepFinished) + { } + + public virtual void OnVisited(TestStepFinished testStepFinished) + { } + + public virtual void OnVisiting(TestStepStarted testStepStarted) + { } + + public virtual void OnVisited(TestStepStarted testStepStarted) + { } + + public virtual void OnVisiting(TestStepResult testStepResult) + { } + + public virtual void OnVisited(TestStepResult testStepResult) + { } + + public virtual void OnVisiting(TestCase testCase) + { } + + public virtual void OnVisited(TestCase testCase) + { } + + public virtual void OnVisiting(StepDefinition stepDefinition) + { } + + public virtual void OnVisited(StepDefinition stepDefinition) + { } + + public virtual void OnVisiting(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisited(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisiting(ParameterType parameterType) + { } + + public virtual void OnVisited(ParameterType parameterType) + { } + + public virtual void OnVisiting(ParseError parseError) + { } + + public virtual void OnVisited(ParseError parseError) + { } + + public virtual void OnVisiting(Source source) + { } + + public virtual void OnVisited(Source source) + { } + + public virtual void OnVisiting(Hook hook) + { } + + public virtual void OnVisited(Hook hook) + { } + + public virtual void OnVisiting(Meta meta) + { } + + public virtual void OnVisited(Meta meta) + { } + + public virtual void OnVisiting(Ci ci) + { } + + public virtual void OnVisited(Ci ci) + { } + + public virtual void OnVisiting(Comment comment) + { } + + public virtual void OnVisited(Comment comment) + { } + + public virtual void OnVisiting(DocString docString) + { } + + public virtual void OnVisited(DocString docString) + { } + + public virtual void OnVisiting(Duration duration) + { } + + public virtual void OnVisited(Duration duration) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisiting(JavaMethod javaMethod) + { } + + public virtual void OnVisited(JavaMethod javaMethod) + { } + + public virtual void OnVisiting(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisited(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisiting(Location location) + { } + + public virtual void OnVisited(Location location) + { } + + public virtual void OnVisiting(Product product) + { } + + public virtual void OnVisited(Product product) + { } + + public virtual void OnVisiting(SourceReference sourceReference) + { } + + public virtual void OnVisited(SourceReference sourceReference) + { } + + public virtual void OnVisiting(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisited(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisiting(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisited(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisiting(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisited(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisiting(Timestamp timestamp) + { } + + public virtual void OnVisited(Timestamp timestamp) + { } + + public virtual void OnVisiting(Git git) + { } + + public virtual void OnVisited(Git git) + { } + + public virtual void OnVisiting(Group group) + { } + + public virtual void OnVisited(Group group) + { } + + public virtual void OnVisiting(PickleDocString pickleDocString) + { } + + public virtual void OnVisited(PickleDocString pickleDocString) + { } + + public virtual void OnVisiting(TestRunStarted testRunStarted) + { } + + public virtual void OnVisited(TestRunStarted testRunStarted) + { } + + public virtual void OnVisiting(TestRunFinished testRunFinished) + { } + + public virtual void OnVisited(TestRunFinished testRunFinished) + { } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 6f534827c..049968e47 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -13,7 +13,7 @@ using System.Linq; using System.Runtime.InteropServices; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class FeatureEventProcessor { diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs index bdf39518d..5a48bc4b3 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class FeatureLevelCucumberMessages { diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs index 29d96c701..6a6cc6ae0 100644 --- a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public static class FileExtensionToMIMETypeMap { diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 649d2f330..0754bde6f 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class HookStepProcessor : StepProcessorBase { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs index 458747f4f..633ad6025 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessagePublisher { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs index 926e2cf1c..2c1219506 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessageSink { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs new file mode 100644 index 000000000..c0e1464c6 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs @@ -0,0 +1,66 @@ +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages; + +// This interface is used to support the implementation of an External Vistor pattern against the Cucumber Messages. +// Visitors impmlement this interface and then invoke it using the helper class below. + +public interface ICucumberMessageVisitor +{ + // Existing methods + void Visit(Envelope envelope); + void Visit(Attachment attachment); + void Visit(GherkinDocument gherkinDocument); + void Visit(Feature feature); + void Visit(FeatureChild featureChild); + void Visit(Rule rule); + void Visit(RuleChild ruleChild); + void Visit(Background background); + void Visit(Scenario scenario); + void Visit(Examples examples); + void Visit(Step step); + void Visit(TableRow tableRow); + void Visit(TableCell tableCell); + void Visit(Tag tag); + void Visit(Pickle pickle); + void Visit(PickleStep pickleStep); + void Visit(PickleStepArgument pickleStepArgument); + void Visit(PickleTable pickleTable); + void Visit(PickleTableRow pickleTableRow); + void Visit(PickleTableCell pickleTableCell); + void Visit(PickleTag pickleTag); + void Visit(TestCase testCase); + void Visit(TestCaseStarted testCaseStarted); + void Visit(TestCaseFinished testCaseFinished); + void Visit(TestStep testStep); + void Visit(TestStepStarted testStepStarted); + void Visit(TestStepFinished testStepFinished); + void Visit(TestStepResult testStepResult); + void Visit(Hook hook); + void Visit(StepDefinition stepDefinition); + void Visit(ParameterType parameterType); + void Visit(UndefinedParameterType undefinedParameterType); + void Visit(SourceReference sourceReference); + void Visit(Duration duration); + void Visit(Timestamp timestamp); + void Visit(Io.Cucumber.Messages.Types.Exception exception); + void Visit(Meta meta); + void Visit(Product product); + void Visit(Ci ci); + void Visit(Git git); + void Visit(Source source); + void Visit(Comment comment); + void Visit(Io.Cucumber.Messages.Types.DataTable dataTable); + void Visit(DocString docString); + void Visit(Group group); + void Visit(JavaMethod javaMethod); + void Visit(JavaStackTraceElement javaStackTraceElement); + void Visit(Location location); + void Visit(ParseError parseError); + void Visit(PickleDocString pickleDocString); + void Visit(StepDefinitionPattern stepDefinitionPattern); + void Visit(StepMatchArgument stepMatchArgument); + void Visit(StepMatchArgumentsList stepMatchArgumentsList); + void Visit(TestRunStarted testRunStarted); + void Visit(TestRunFinished testRunFinished); +} diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMesssages/IStepProcessor.cs index 667d02467..02ba5ed1b 100644 --- a/Reqnroll/CucumberMesssages/IStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/IStepProcessor.cs @@ -1,4 +1,4 @@ -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface IStepProcessor { diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs index 8ff9bc652..80bfbf10d 100644 --- a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs @@ -1,7 +1,7 @@ using Gherkin.CucumberMessages; using System; using System.Threading; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class IdGeneratorFactory { diff --git a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs index 4ea75d028..17954295a 100644 --- a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs @@ -2,7 +2,7 @@ using System; using System.Text.Json; -namespace Cucumber.Messages +namespace Reqnroll.CucumberMessages { /// /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs index bf05e99ae..62e5491a5 100644 --- a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs +++ b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class OutputAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs index d91af932d..12c6a3784 100644 --- a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs +++ b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs @@ -1,7 +1,7 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class ReqnrollCucumberMessage { diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index b8f8d9836..d20816a1e 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -9,7 +9,7 @@ using System.Diagnostics; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class ScenarioEventProcessor diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 9f53ad4d2..13eb9e251 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class StepArgument { diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index af4834a6f..3638404b0 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -1,6 +1,6 @@ using System; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class StepProcessorBase : IStepProcessor { diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index dcdbc9cea..5235ec7c7 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,6 +1,6 @@ using System; using System.Globalization; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Tracing; namespace Reqnroll diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 8e0810ccf..61d0c283b 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,7 +16,7 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; namespace Reqnroll.Infrastructure { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 818e94ac8..34f50d93e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,5 +1,5 @@ -using Cucumber.Messages; +using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using Newtonsoft.Json.Bson; @@ -47,19 +47,6 @@ public void SmokeTest() """); AddPassingStepBinding("When"); - AddBindingClass(""" - [Binding] - public class TaggedScenario - { - [AfterScenario()] - [Scope(Tag = "some-tag")] - public void FailingAfterHook() - { - throw new Exception("Exception in conditional hook"); - } - } - """); - ExecuteTests(); ShouldAllScenariosPass(); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 4cc8a13eb..a55c18009 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -1,6 +1,6 @@ using FluentAssertions; +using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; -using System.Diagnostics.Eventing.Reader; namespace CucumberMessages.CompatibilityTests { @@ -80,8 +80,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu { actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); - //todo: modify this to use lists of types from actual and expected and use FluentAssertions directly - foreach (var messageType in MessageExtensions.EnvelopeContentTypes) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) { @@ -100,93 +99,4 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu } } } - - internal static class MessageExtensions - { - public static List MessagesWithIds = new(){ typeof(Background), - typeof(Examples), - typeof(Hook), - typeof(ParameterType), - typeof(Pickle), - typeof(PickleStep), - typeof(Rule), - typeof(Scenario), - typeof(Step), - typeof(StepDefinition), - typeof(TableRow), - typeof(Tag), - typeof(TestCase), - typeof(TestCaseStarted), - typeof(TestStep) - }; - - internal static bool HasId(this object element) - { - return MessagesWithIds.Contains(element.GetType()); - } - internal static string Id(this object message) - { - return message switch - { - Background bgd => bgd.Id, - Examples ex => ex.Id, - Hook hook => hook.Id, - ParameterType pt => pt.Id, - Pickle p => p.Id, - PickleStep ps => ps.Id, - Rule r => r.Id, - Scenario sc => sc.Id, - Step st => st.Id, - StepDefinition sd => sd.Id, - TableRow tr => tr.Id, - Tag tag => tag.Id, - TestCase tc => tc.Id, - TestCaseStarted tcs => tcs.Id, - TestStep ts => ts.Id, - _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") - }; - } - internal static List EnvelopeContentTypes = new() - { - typeof(Attachment), - typeof(GherkinDocument), - typeof(Hook), - typeof(Meta), - typeof(ParameterType), - typeof(ParseError), - typeof(Pickle), - typeof(Source), - typeof(StepDefinition), - typeof(TestCase), - typeof(TestCaseFinished), - typeof(TestCaseStarted), - typeof(TestRunFinished), - typeof(TestRunStarted), - typeof(TestStepFinished), - typeof(TestStepStarted), - typeof(UndefinedParameterType) - }; - internal static object Content(this Envelope envelope) - { - object? result = null; - if (envelope.Attachment != null) { result = envelope.Attachment; } - else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } - else if (envelope.Hook != null) { result = envelope.Hook; } - else if (envelope.Meta != null) { result = envelope.Meta; } - else if (envelope.ParameterType != null) { result = envelope.ParameterType; } - else if (envelope.ParseError != null) { result = envelope.ParseError; } - else if (envelope.Pickle != null) { result = envelope.Pickle; } - else if (envelope.Source != null) { result = envelope.Source; } - else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } - else if (envelope.TestCase != null) { result = envelope.TestCase; } - else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } - else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } - else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } - else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } - else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } - else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } - else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } - return result!; - } - } } \ No newline at end of file From 15e52ae453de98afdfbd65b3d45245dd8b800ca6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:52:32 -0500 Subject: [PATCH 047/218] The Validator fully builds out cross-reference tables using the visitor for all element types. --- .../CrossReferenceBuilder.cs | 304 ++++++++++++++++++ .../CucumberMessagesValidator.cs | 22 +- 2 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs new file mode 100644 index 000000000..b28ea5835 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs @@ -0,0 +1,304 @@ +using Io.Cucumber.Messages.Types; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using Reqnroll.CucumberMessages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + internal class CrossReferenceBuilder : CucumberMessage_TraversalVisitorBase + { + private Action buildCrossReferences; + public CrossReferenceBuilder(Action buildCrossReferences) + { + this.buildCrossReferences = buildCrossReferences; + } + + public override void OnVisiting(Attachment attachment) + { + buildCrossReferences(attachment); + base.OnVisiting(attachment); + } + public override void OnVisiting(Background background) + { + buildCrossReferences(background); + base.OnVisiting(background); + } + public override void OnVisiting(Ci ci) + { + buildCrossReferences(ci); + base.OnVisiting(ci); + } + public override void OnVisiting(Comment comment) + { + buildCrossReferences(comment); + base.OnVisiting(comment); + } + public override void OnVisiting(Duration duration) + { + buildCrossReferences(duration); + base.OnVisiting(duration); + } + + public override void OnVisiting(Examples examples) + { + buildCrossReferences(examples); + base.OnVisiting(examples); + } + public override void OnVisiting(Io.Cucumber.Messages.Types.Exception exception) + { + buildCrossReferences(exception); + base.OnVisiting(exception); + } + public override void OnVisiting(GherkinDocument gherkinDocument) + { + buildCrossReferences(gherkinDocument); + base.OnVisiting(gherkinDocument); + } + + public override void OnVisiting(Feature feature) + { + buildCrossReferences(feature); + base.OnVisiting(feature); + } + public override void OnVisiting(FeatureChild featureChild) + { + buildCrossReferences(featureChild); + base.OnVisiting(featureChild); + } + public override void OnVisiting(Git git) + { + buildCrossReferences(git); + base.OnVisiting(git); + } + public override void OnVisiting(Group group) + { + buildCrossReferences(group); + base.OnVisiting(group); + } + public override void OnVisiting(JavaMethod javaMethod) + { + buildCrossReferences(javaMethod); + base.OnVisiting(javaMethod); + } + public override void OnVisiting(JavaStackTraceElement javaStackTraceElement) + { + buildCrossReferences(javaStackTraceElement); + base.OnVisiting(javaStackTraceElement); + } + public override void OnVisiting(Location location) + { + buildCrossReferences(location); + base.OnVisiting(location); + } + public override void OnVisiting(Meta meta) + { + buildCrossReferences(meta); + base.OnVisiting(meta); + } + public override void OnVisiting(ParameterType parameterType) + { + buildCrossReferences(parameterType); + base.OnVisiting(parameterType); + } + public override void OnVisiting(ParseError parseError) + { + buildCrossReferences(parseError); + base.OnVisiting(parseError); + } + public override void OnVisiting(PickleStepArgument pickleStepArgument) + { + buildCrossReferences(pickleStepArgument); + base.OnVisiting(pickleStepArgument); + } + public override void OnVisiting(PickleTable pickleTable) + { + buildCrossReferences(pickleTable); + base.OnVisiting(pickleTable); + } + + public override void OnVisiting(PickleTableRow pickleTableRow) + { + buildCrossReferences(pickleTableRow); + base.OnVisiting(pickleTableRow); + } + public override void OnVisiting(PickleTableCell pickleTableCell) + { + buildCrossReferences(pickleTableCell); + base.OnVisiting(pickleTableCell); + } + public override void OnVisiting(PickleTag pickelTag) + { + buildCrossReferences(pickelTag); + base.OnVisiting(pickelTag); + } + public override void OnVisiting(Product product) + { + buildCrossReferences(product); + base.OnVisiting(product); + } + public override void OnVisiting(Rule rule) + { + buildCrossReferences(rule); + base.OnVisiting(rule); + } + public override void OnVisiting(RuleChild ruleChild) + { + buildCrossReferences(ruleChild); + base.OnVisiting(ruleChild); + } + public override void OnVisiting(Scenario scenario) + { + buildCrossReferences(scenario); + base.OnVisiting(scenario); + } + public override void OnVisiting(Source source) + { + buildCrossReferences(source); + base.OnVisiting(source); + } + public override void OnVisiting(SourceReference sourceReference) + { + buildCrossReferences(sourceReference); + base.OnVisiting(sourceReference); + } + public override void OnVisiting(Step step) + { + buildCrossReferences(step); + base.OnVisiting(step); + } + public override void OnVisiting(StepDefinition stepDefinition) + { + buildCrossReferences(stepDefinition); + base.OnVisiting(stepDefinition); + } + public override void OnVisiting(StepDefinitionPattern stepDefinitionPattern) + { + buildCrossReferences(stepDefinitionPattern); + base.OnVisiting(stepDefinitionPattern); + } + public override void OnVisiting(StepMatchArgument stepMatchArgument) + { + buildCrossReferences(stepMatchArgument); + base.OnVisiting(stepMatchArgument); + } + + public override void OnVisiting(StepMatchArgumentsList stepMatchArgumentsList) + { + buildCrossReferences(stepMatchArgumentsList); + base.OnVisiting(stepMatchArgumentsList); + } + public override void OnVisiting(DocString docString) + { + buildCrossReferences(docString); + base.OnVisiting(docString); + } + + public override void OnVisiting(DataTable dataTable) + { + buildCrossReferences(dataTable); + base.OnVisiting(dataTable); + } + + public override void OnVisiting(TableCell tableCell) + { + buildCrossReferences(tableCell); + base.OnVisiting(tableCell); + } + public override void OnVisiting(TableRow tableRow) + { + buildCrossReferences(tableRow); + base.OnVisiting(tableRow); + } + public override void OnVisiting(Tag tag) + { + buildCrossReferences(tag); + base.OnVisiting(tag); + } + public override void OnVisiting(TestCase testCase) + { + buildCrossReferences(testCase); + base.OnVisiting(testCase); + } + public override void OnVisiting(TestCaseFinished testCaseFinished) + { + buildCrossReferences(testCaseFinished); + base.OnVisiting(testCaseFinished); + } + + public override void OnVisiting(TestRunFinished testRunFinished) + { + buildCrossReferences(testRunFinished); + base.OnVisiting(testRunFinished); + } + + public override void OnVisiting(TestRunStarted testRunStarted) + { + buildCrossReferences(testRunStarted); + base.OnVisiting(testRunStarted); + } + + public override void OnVisiting(TestCaseStarted testCaseStarted) + { + buildCrossReferences(testCaseStarted); + base.OnVisiting(testCaseStarted); + } + public override void OnVisiting(TestStep testStep) + { + buildCrossReferences(testStep); + base.OnVisiting(testStep); + } + public override void OnVisiting(TestStepResult testStepResult) + { + buildCrossReferences(testStepResult); + base.OnVisiting(testStepResult); + } + public override void OnVisiting(TestStepFinished testStepFinished) + { + buildCrossReferences(testStepFinished); + base.OnVisiting(testStepFinished); + } + + public override void OnVisiting(TestStepStarted testStepStarted) + { + buildCrossReferences(testStepStarted); + base.OnVisiting(testStepStarted); + } + + public override void OnVisiting(Hook hook) + { + buildCrossReferences(hook); + base.OnVisiting(hook); + } + + public override void OnVisiting(Pickle pickle) + { + buildCrossReferences(pickle); + base.OnVisiting(pickle); + } + + public override void OnVisiting(PickleStep pickleStep) + { + buildCrossReferences(pickleStep); + base.OnVisiting(pickleStep); + } + public override void OnVisiting(PickleDocString pickleDocString) + { + buildCrossReferences(pickleDocString); + base.OnVisiting(pickleDocString); + } + public override void OnVisiting(Timestamp timestamp) + { + buildCrossReferences(timestamp); + base.OnVisiting(timestamp); + } + public override void OnVisiting(UndefinedParameterType undefinedParameterType) + { + buildCrossReferences(undefinedParameterType); + base.OnVisiting(undefinedParameterType); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index a55c18009..018459326 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -28,16 +28,20 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) { + var xrefBuilder = new CrossReferenceBuilder( msg => + { + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + }); foreach (var message in messages) { var msg = message.Content(); - InsertIntoElementsByType(msg, elementsByType); - - if (msg.HasId()) - { - InsertIntoElementsById(msg, elementsByID); - InsertIntoIDsByType(msg, IDsByType); - } + CucumberMessageVisitor.Accept(xrefBuilder, msg); } } private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) @@ -82,7 +86,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { - if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) { throw new System.Exception($"{messageType} present in the actual but not in the expected."); } @@ -91,7 +95,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu throw new System.Exception($"{messageType} present in the expected but not in the actual."); } if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - { + { actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); } if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) From bb446ba2842eb9ae7cbfd9201641e7df7d90f4ba Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:57:49 -0500 Subject: [PATCH 048/218] Flushing out validator tests. Source comparison complete. Feature top-level properties comparison complete. (Feature Children next) --- .../CucumberCompatibilityTests.cs | 2 + .../CucumberMessagesValidator.cs | 77 +++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 34f50d93e..8c86d0ec5 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -199,6 +199,8 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + validator.ShouldPassBasicStructuralChecks(); + validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); ConfirmAllTestsRan(null); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 018459326..f4ab49814 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -4,7 +4,7 @@ namespace CucumberMessages.CompatibilityTests { - internal class CucumberMessagesValidator + public class CucumberMessagesValidator { private IEnumerable actualEnvelopes; private IEnumerable expectedEnvelopes; @@ -71,19 +71,84 @@ private static void InsertIntoElementsByType(object msg, Dictionary actual, IEnumerable expected) + + private void StepDefinitionsShouldBeComparable() + { + } + + private void PicklesShouldBeComparable() + { + } + + private void GherkinDocumentShouldBeComparable() + { + var actualGherkinDocument = actuals_elementsByType[typeof(GherkinDocument)].First().As(); + var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); + + //check top-level items first + // ignore Uri + // comments should be present in the same order; so a simple list comparison should work + actualGherkinDocument.Comments.Should().BeEquivalentTo(expectedGherkinDocument.Comments, options => options.Including(c => c.Text)); + FeatureShouldBeComparable(actualGherkinDocument.Feature, expectedGherkinDocument.Feature); + } + + private void FeatureShouldBeComparable(Feature actual, Feature expected) + { + // ingore Location elements and Id values + actual.Tags.Should().BeEquivalentTo(expected.Tags, options => options.Including(t => t.Name)); + + // CCK expects only the language code, not the language and culture codes + actual.Language.Split('-')[0].Should().Be(expected.Language); + actual.Name.Should().Be(expected.Name); + actual.Description.Replace("\r\n", "\n").Should().Be(expected.Description.Replace("\r\n", "\n")); + actual.Keyword.Should().Be(expected.Keyword); + // expecting that the children are in the same order + + + } + + private void SourceContentShouldBeIdentical() + { + var actualSource = actuals_elementsByType[typeof(Source)].First().As(); + var expectedSource = expecteds_elementsByType[typeof(Source)].First().As(); + actualSource.Data.Replace("\r\n", "\n").Should().Be(expectedSource.Data.Replace("\r\n", "\n")); + actualSource.MediaType.Should().Be(expectedSource.MediaType); + } + + public void ResultShouldPassBasicSanityChecks() + { + EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + } + + private void EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook() + { + } + + private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() + { + } + + public void ShouldPassBasicStructuralChecks() { + var actual = actualEnvelopes; + var expected = expectedEnvelopes; actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) From 545f991c2e1fe4d16197939127643a2a9476dea6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:02:11 -0500 Subject: [PATCH 049/218] Source and GherkinDoc now using FA. Fixed namespace mispelling in FileSinkPlugin (to match changes made in previous commit). Temp fix: updated plug-in version reference to 2.2.0-local; need a way to fetch this from assembly. --- .../FileSinkPlugin.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 2 +- .../CucumberCompatibilityTests.cs | 2 +- .../CucumberMessagesValidator.cs | 106 +++++++++++++----- ...FluentAssertionsCustomStringComparisons.cs | 29 +++++ ...ionCucumberMessagePropertySelectionRule.cs | 44 ++++++++ 6 files changed, 155 insertions(+), 30 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 8807fde2a..f2602a1d4 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -1,4 +1,4 @@ -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; using Io.Cucumber.Messages; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 0b3f95052..0fddf4667 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -14,7 +14,7 @@ public class CucumberCompatibilityTestBase : SystemTestBase protected void AddCucumberMessagePlugIn() { - _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); + _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.1-local"); } protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 8c86d0ec5..e67ad6564 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -57,7 +57,7 @@ public void CucumberMessagesInteropWithExternalData() { // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages AddCucumberMessagePlugIn(); - _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.0-local"); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.2.0-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index f4ab49814..14044e653 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; +using System.ComponentModel.Design; namespace CucumberMessages.CompatibilityTests { @@ -16,6 +17,7 @@ public class CucumberMessagesValidator private Dictionary> expecteds_elementsByType = new(); private Dictionary> actuals_elementsByID = new(); private Dictionary> expecteds_elementsByID = new(); + private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) { @@ -25,10 +27,69 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options + // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances + .ComparingByValue() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + // Using a custom Property Selector so that we can ignore the following properties (Id, Uri, and Location); these will always be different + .Using(FA_CustomCucumberMessagesPropertySelector) + // Using a custom string comparison to ignore the differences in platform line endings + .Using(new FluentAssertionsCustomStringComparisons()) + .AllowingInfiniteRecursion() + .RespectingRuntimeTypes() + ); } private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) { - var xrefBuilder = new CrossReferenceBuilder( msg => + var xrefBuilder = new CrossReferenceBuilder(msg => { InsertIntoElementsByType(msg, elementsByType); @@ -74,11 +135,11 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); - //check top-level items first - // ignore Uri - // comments should be present in the same order; so a simple list comparison should work - actualGherkinDocument.Comments.Should().BeEquivalentTo(expectedGherkinDocument.Comments, options => options.Including(c => c.Text)); - FeatureShouldBeComparable(actualGherkinDocument.Feature, expectedGherkinDocument.Feature); - } - - private void FeatureShouldBeComparable(Feature actual, Feature expected) - { - // ingore Location elements and Id values - actual.Tags.Should().BeEquivalentTo(expected.Tags, options => options.Including(t => t.Name)); - - // CCK expects only the language code, not the language and culture codes - actual.Language.Split('-')[0].Should().Be(expected.Language); - actual.Name.Should().Be(expected.Name); - actual.Description.Replace("\r\n", "\n").Should().Be(expected.Description.Replace("\r\n", "\n")); - actual.Keyword.Should().Be(expected.Keyword); - // expecting that the children are in the same order - + actualGherkinDocument.Should().BeEquivalentTo(expectedGherkinDocument, options => options + .Using(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(inf => inf.Path.EndsWith("Language")) + .WithTracing()); } + private void SourceContentShouldBeIdentical() { - var actualSource = actuals_elementsByType[typeof(Source)].First().As(); - var expectedSource = expecteds_elementsByType[typeof(Source)].First().As(); - actualSource.Data.Replace("\r\n", "\n").Should().Be(expectedSource.Data.Replace("\r\n", "\n")); - actualSource.MediaType.Should().Be(expectedSource.MediaType); + var actualSource = actuals_elementsByType[typeof(Source)].First() as Source; + var expectedSource = expecteds_elementsByType[typeof(Source)].First() as Source; + + actualSource.Should().BeEquivalentTo(expectedSource, options => options.WithTracing() ); } public void ResultShouldPassBasicSanityChecks() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs new file mode 100644 index 000000000..6b09f6b55 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs @@ -0,0 +1,29 @@ +using FluentAssertions.Equivalency; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + public class FluentAssertionsCustomStringComparisons : IEqualityComparer + { + public bool Equals(string? x, string? y) + { + if (x == null && y == null) return true; + if (x == null || y == null) return false; + + x = x.Replace("\r\n", "\n"); + y = y.Replace("\r\n", "\n"); + return x.Equals(y); + } + + public int GetHashCode([DisallowNull] string obj) + { + return obj.GetHashCode(); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs new file mode 100644 index 000000000..cd1477c40 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Cucumber.Messages; +using FluentAssertions; +using FluentAssertions.Equivalency; + +namespace CucumberMessages.CompatibilityTests +{ + public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelectionRule + { + public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes) + { this.CucumberMessageTypes = CucumberMessageTypes; } + + public IEnumerable CucumberMessageTypes { get; } + + public bool IncludesMembers => false; + + public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers, MemberSelectionContext context) + { + if (CucumberMessageTypes.Contains(context.Type)) + { + var propertiesToSelect = new List(); + foreach (var prop in selectedMembers) + { + if (prop.Name != "Id" && prop.Name != "Location" && prop.Name != "Uri" ) + propertiesToSelect.Add(prop); + } + return propertiesToSelect; + } + else + { + return selectedMembers; + } + } + public override string ToString() + { + return "Include only relevant CucumberMessage properties"; + } + + } +} From 31b2513653dfa3b8bb6f77eeab692b626ad496c9 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:16:30 -0500 Subject: [PATCH 050/218] Minimal and Hooks now passing Content and GD checks. Temp patch to Cucumber.Messages - with local build. Will need to revert to global nuget package when PR accepted by them. --- Reqnroll/CucumberMesssages/FeatureEventProcessor.cs | 8 +------- .../CucumberCompatibilityTests.cs | 8 ++++++-- .../CucumberMessages.CompatibilityTests.csproj | 1 + ...luentAsssertionCucumberMessagePropertySelectionRule.cs | 1 - 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 049968e47..8a8338527 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -1,17 +1,11 @@ -using Cucumber.Messages; -using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; -using Reqnroll.BoDi; using Reqnroll.Events; -using Reqnroll.Analytics; using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; namespace Reqnroll.CucumberMessages { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index e67ad6564..e6a3c89b2 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -218,7 +218,11 @@ private IEnumerable GetExpectedResults(string scenarioName) var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); - foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); + foreach (var json in expectedJsonText) + { + var e = NdjsonSerializer.Deserialize(json); + yield return e; + }; } private IEnumerable GetActualResults(string scenarioName) @@ -229,7 +233,7 @@ private IEnumerable GetActualResults(string scenarioName) var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); - foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); + foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } } internal class FileSinkConfiguration diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 613faa012..6dcc8cd5e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,6 +120,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index cd1477c40..49cd2f0d4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Cucumber.Messages; using FluentAssertions; using FluentAssertions.Equivalency; From d6fa6279560064e69094c76b9b8f9125843c1949 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:48:49 -0500 Subject: [PATCH 051/218] Source, GD, Pickle, StepDefinition and TestCase now working for Minimal. --- .../ScenarioStepProcessor.cs | 22 +++++++--- .../CucumberCompatibilityTests.cs | 12 ++++++ .../CucumberMessagesValidator.cs | 40 +++++++++++-------- ...ionCucumberMessagePropertySelectionRule.cs | 31 ++++++++++++-- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 13eb9e251..f9549d182 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -2,6 +2,7 @@ using Reqnroll.Assist; using Reqnroll.Bindings; using Reqnroll.Events; +using System; using System.Collections.Generic; using System.Linq; @@ -65,15 +66,26 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) Exception = stepFinishedEvent.ScenarioContext.TestError; } + var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); + var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => SimplifyTypeNames(p.Type.Name)).ToList() : new List(); StepArguments = Bound ? - stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument - { - Value = arg.ToString(), - Type = arg.GetType().Name - }).ToList() + argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); return Enumerable.Empty(); } + + private string SimplifyTypeNames(string name) + { + return name switch + { + "Int32" => "int", + "Long" => "long", + "Double" => "float", + "String" => "string", + "Boolean" => "bool", + _ => name + }; + } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index e6a3c89b2..c1175158d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -7,6 +7,7 @@ using System.Reflection; using FluentAssertions; using System.Text.Json; +using System.ComponentModel; namespace CucumberMessages.CompatibilityTests { @@ -235,6 +236,17 @@ private IEnumerable GetActualResults(string scenarioName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } + + [TestMethod] + public void tempTest() + { + var d1 = new Destination(true, "A", "AO"); + var d2 = new Destination(false, "B", "AO"); + + var a = new List() { d1, d2, d2 }; + var b = new List() { d2, d1 }; + a.Should().BeEquivalentTo(b); + } } internal class FileSinkConfiguration { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 14044e653..b7559f9eb 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -2,6 +2,7 @@ using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using System.ComponentModel.Design; +using FluentAssertions.Execution; namespace CucumberMessages.CompatibilityTests { @@ -79,7 +80,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() .ComparingByMembers() .ComparingByMembers() - // Using a custom Property Selector so that we can ignore the following properties (Id, Uri, and Location); these will always be different + // Using a custom Property Selector so that we can ignore the properties that are not comparable .Using(FA_CustomCucumberMessagesPropertySelector) // Using a custom string comparison to ignore the differences in platform line endings .Using(new FluentAssertionsCustomStringComparisons()) @@ -134,32 +135,44 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); } private void StepDefinitionsShouldBeComparable() { + CompareMessageType(); } private void PicklesShouldBeComparable() { + CompareMessageType(); } private void GherkinDocumentShouldBeComparable() { - var actualGherkinDocument = actuals_elementsByType[typeof(GherkinDocument)].First().As(); - var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); + CompareMessageType(); - actualGherkinDocument.Should().BeEquivalentTo(expectedGherkinDocument, options => options + } + + private void CompareMessageType() + { + var actual = actuals_elementsByType[typeof(T)].First().As(); + var expected = expecteds_elementsByType[typeof(T)].First().As(); + + actual.Should().BeEquivalentTo(expected, options => options .Using(ctx => { var actual = ctx.Subject.Split("-")[0]; @@ -168,16 +181,11 @@ private void GherkinDocumentShouldBeComparable() }) .When(inf => inf.Path.EndsWith("Language")) .WithTracing()); - } - private void SourceContentShouldBeIdentical() { - var actualSource = actuals_elementsByType[typeof(Source)].First() as Source; - var expectedSource = expecteds_elementsByType[typeof(Source)].First() as Source; - - actualSource.Should().BeEquivalentTo(expectedSource, options => options.WithTracing() ); + CompareMessageType(); } public void ResultShouldPassBasicSanityChecks() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 49cd2f0d4..8aac867c9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -8,10 +8,35 @@ namespace CucumberMessages.CompatibilityTests { + /// + /// Fluent Asssertion Cucumber Message Property Selection Rule + /// This class is used by Fluent Assertions to override which properties will be compared. + /// These properties will be skipped because they are not comparable across platforms + /// public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelectionRule { - public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes) - { this.CucumberMessageTypes = CucumberMessageTypes; } + // Properties to skip - this is the default set of properties that are not comparable across platforms + // Id: Ids are not assigned in the same order across platforms. + // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds: Ids are not assigned in the same order across platforms. + // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) + // Uri is not always comparable (eg, CCK refers to source file paths in typescript) + // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references + // Seconds and Nanos: time values are not comparable + // Start: Start refers to a column position in source code, which may not be comparable across platforms. + + // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), + // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) + private List PropertiesToSkip = new List() { "Id", "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Seconds", "Nanos", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "Start" }; + + public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) + { + this.CucumberMessageTypes = CucumberMessageTypes; + + if (proportiesToSkip != null) + { + PropertiesToSkip = proportiesToSkip.ToList(); + } + } public IEnumerable CucumberMessageTypes { get; } @@ -24,7 +49,7 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable(); foreach (var prop in selectedMembers) { - if (prop.Name != "Id" && prop.Name != "Location" && prop.Name != "Uri" ) + if (!PropertiesToSkip.Contains(prop.Name)) propertiesToSelect.Add(prop); } return propertiesToSelect; From 25f1a216f22547f56d01a5e59a450ac355bd50b1 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:31:17 -0500 Subject: [PATCH 052/218] All Envelope comparisons working for Minimal. Partial for Hooks. --- .../CucumberCompatibilityTests.cs | 11 -- .../CucumberMessagesValidator.cs | 107 ++++++++++++++---- ...ionCucumberMessagePropertySelectionRule.cs | 11 +- 3 files changed, 94 insertions(+), 35 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index c1175158d..7fac2eb2e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -236,17 +236,6 @@ private IEnumerable GetActualResults(string scenarioName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } - - [TestMethod] - public void tempTest() - { - var d1 = new Destination(true, "A", "AO"); - var d2 = new Destination(false, "B", "AO"); - - var a = new List() { d1, d2, d2 }; - var b = new List() { d2, d1 }; - a.Should().BeEquivalentTo(b); - } } internal class FileSinkConfiguration { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index b7559f9eb..30a48b1bd 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -3,6 +3,7 @@ using Io.Cucumber.Messages.Types; using System.ComponentModel.Design; using FluentAssertions.Execution; +using System.Reflection; namespace CucumberMessages.CompatibilityTests { @@ -20,6 +21,12 @@ public class CucumberMessagesValidator private Dictionary> expecteds_elementsByID = new(); private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; + // Envelope types - these are the top level types in CucumberMessages + // Meta is excluded from the list as there is nothing there for us to compare + private readonly IEnumerable EnvelopeTypes = new Type[] { typeof(Attachment), typeof(GherkinDocument), typeof(Hook), typeof(ParameterType), typeof(Source), + typeof(StepDefinition), typeof(TestCase), typeof(TestCaseFinished), typeof(TestCaseStarted), typeof(TestRunFinished), + typeof(TestRunStarted), typeof(TestStepFinished), typeof(TestStepStarted), typeof(UndefinedParameterType) }; + public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) { actualEnvelopes = actual; @@ -30,7 +37,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options - // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances + // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances .ComparingByValue() .ComparingByMembers() .ComparingByMembers() @@ -80,12 +87,30 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() .ComparingByMembers() .ComparingByMembers() - // Using a custom Property Selector so that we can ignore the properties that are not comparable + + // Using a custom Property Selector so that we can ignore the properties that are not comparable .Using(FA_CustomCucumberMessagesPropertySelector) - // Using a custom string comparison to ignore the differences in platform line endings - .Using(new FluentAssertionsCustomStringComparisons()) + + // Using a custom string comparison to ignore the differences in platform line endings + .Using((ctx) => + { + var subject = ctx.Subject ?? string.Empty; + var expectation = ctx.Expectation ?? string.Empty; + subject = subject.Replace("\r\n", "\n"); + expectation = expectation.Replace("\r\n", "\n"); + subject.Should().Be(expectation); + }) + .WhenTypeIs() + + // A bit of trickery here to tell FluentAssertions that Timestamps are always equal + // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) + .Using(ctx => 1.Should().Be(1)) + .WhenTypeIs() + .AllowingInfiniteRecursion() - .RespectingRuntimeTypes() + //.RespectingRuntimeTypes() + .ExcludingFields() + .WithStrictOrdering() ); } private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) @@ -135,17 +160,18 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); @@ -169,18 +195,57 @@ private void GherkinDocumentShouldBeComparable() private void CompareMessageType() { - var actual = actuals_elementsByType[typeof(T)].First().As(); - var expected = expecteds_elementsByType[typeof(T)].First().As(); + if (!expecteds_elementsByType.ContainsKey(typeof(T))) + return; - actual.Should().BeEquivalentTo(expected, options => options - .Using(ctx => + HashSet? actuals; + List actual; + List expected; + + if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) { - var actual = ctx.Subject.Split("-")[0]; - var expected = ctx.Expectation.Split("-")[0]; - actual.Should().Be(expected); - }) - .When(inf => inf.Path.EndsWith("Language")) - .WithTracing()); + actual = actuals.OfType().ToList(); + } + else + actual = new List(); + + expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; + + actual.Should().BeEquivalentTo(expected, options => options + .Using>(ctx => + { + if (ctx.SelectedNode.IsRoot) + { + var actualList = ctx.Subject; + var expectedList = ctx.Expectation; + + if (expectedList == null || !expectedList.Any()) + { + return; // If expected is null or empty, we don't need to check anything + } + + actualList.Should().NotBeNull(); + actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, + "actual collection should have at least as many items as expected"); + + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } + } + }) + .WhenTypeIs>() + // Using a custom string comparison that deals with ISO langauge codes when the property name ends with "Language" + .Using(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(inf => inf.Path.EndsWith("Language")) + .WithTracing()); } private void SourceContentShouldBeIdentical() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 8aac867c9..8ec3541da 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -17,16 +17,21 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec { // Properties to skip - this is the default set of properties that are not comparable across platforms // Id: Ids are not assigned in the same order across platforms. - // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds: Ids are not assigned in the same order across platforms. + // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds, TestStepId, TestCaseStartedId, TestCaseId, WorkerId: Ids are not assigned in the same order across platforms. // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) // Uri is not always comparable (eg, CCK refers to source file paths in typescript) // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references - // Seconds and Nanos: time values are not comparable + // Duration: time values are not comparable // Start: Start refers to a column position in source code, which may not be comparable across platforms. + // FileName: CCK does not provide the file name of attachments but Reqnroll does + // ProtocolVersion, Implementation, Runtime, Cpu, Os, Ci: These properties of the Meta message are not comparable across platforms. // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) - private List PropertiesToSkip = new List() { "Id", "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Seconds", "Nanos", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "Start" }; + private List PropertiesToSkip = new List() { "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", + "Duration", "Start", "FileName", "Message", "Type", "StackTrace", + "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "TestCaseStartedId","TestStepId", "TestCaseId", "WorkerId", + "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci"}; public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) { From a8de646998e093bb0d13ec95c429492994602d9c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:20:52 -0500 Subject: [PATCH 053/218] Refactoring and minor improvements --- .../CucumberMessageFactory.cs | 41 +++- .../ScenarioStepProcessor.cs | 15 +- .../CucumberCompatibilityTests.cs | 48 ++--- .../CucumberMessagesValidator.cs | 180 ++++++++++-------- ...ionCucumberMessagePropertySelectionRule.cs | 10 +- 5 files changed, 169 insertions(+), 125 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index e5b96dd01..0ef49c3c9 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -36,7 +36,7 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar switch (stepState) { case ScenarioStepProcessor _: - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as ScenarioStepProcessor); + var testStep = CucumberMessageFactory.ToPickleTestStep(scenarioState, stepState as ScenarioStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: @@ -65,10 +65,7 @@ internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scena } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { - var bindingSourceText = binding.SourceExpression; - var expressionType = binding.ExpressionType; - var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; - var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + StepDefinitionPattern stepDefinitionPattern = ToStepDefinitionPattern(binding); SourceReference sourceRef = ToSourceRef(binding); var result = new StepDefinition @@ -80,6 +77,14 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } + internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBinding binding) + { + var bindingSourceText = binding.SourceExpression; + var expressionType = binding.ExpressionType; + var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; + var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + return stepDefinitionPattern; + } internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { @@ -110,7 +115,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) + internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { bool bound = stepState.StepDefinitionId != null; @@ -137,7 +142,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) null, argument.Value ), - argument.Type); + NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); } internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) { @@ -165,7 +170,7 @@ internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) iDGenerator.GetNewId(), null, sourceRef, - hookBinding.IsScoped ? hookBinding.BindingScope.Tag : null + hookBinding.IsScoped ? $"@{hookBinding.BindingScope.Tag}" : null ); return result; } @@ -342,7 +347,25 @@ public static string Base64EncodeFile(string filePath) return Convert.ToBase64String(fileBytes); } - + + private static string NormalizePrimitiveTypeNamesToCucumberTypeNames(string name) + { + return name switch + { + "Int16" => "short", + "Int32" => "int", + "Int64" => "long", + "Single" => "float", + "Double" => "double", + "Byte" => "byte", + "String" => "string", + "Boolean" => "bool", + "Decimal" => "decimal", + "BigInteger" => "biginteger", + _ => name + }; + } + #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index f9549d182..7e6b53aca 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -67,25 +67,12 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); - var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => SimplifyTypeNames(p.Type.Name)).ToList() : new List(); + var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); StepArguments = Bound ? argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); return Enumerable.Empty(); } - - private string SimplifyTypeNames(string name) - { - return name switch - { - "Int32" => "int", - "Long" => "long", - "Double" => "float", - "String" => "string", - "Boolean" => "bool", - _ => name - }; - } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7fac2eb2e..9eb24ec4d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using System.Text.Json; using System.ComponentModel; +using Reqnroll.TestProjectGenerator; namespace CucumberMessages.CompatibilityTests { @@ -173,33 +174,33 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) } [TestMethod] - [DataRow("attachments")] - [DataRow("minimal")] - [DataRow("cdata")] - [DataRow("pending")] - [DataRow("examples-tables")] - [DataRow("hooks")] - [DataRow("data-tables")] - [DataRow("parameter-types")] - [DataRow("skipped")] - [DataRow("undefined")] - [DataRow("unknown-parameter-type")] - [DataRow("rules")] - public void CCKScenarios(string scenarioName) + [DataRow("attachments", "Attachments")] + [DataRow("minimal", "minimal")] + [DataRow("cdata","cdata")] + [DataRow("pending", "Pending steps")] + [DataRow("examples-tables", "Examples Tables")] + [DataRow("hooks", "Hooks")] + [DataRow("data-tables", "Data Tables")] + [DataRow("parameter-types", "Parameter Types")] + [DataRow("skipped", "Skipping scenarios")] + [DataRow("undefined", "Undefined steps")] + [DataRow("unknown-parameter-type", "Parameter Types")] + [DataRow("rules", "Usage of a 'Rule'")] + public void CCKScenarios(string testName, string featureNameText) { AddCucumberMessagePlugIn(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); - scenarioName = scenarioName.Replace("-", "_"); + var featureFileName = testName.Replace("-", "_"); - AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - //AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + //AddBinaryFilesFromResource($"{testName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); - var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + var validator = new CucumberMessagesValidator(GetActualResults(testName, featureNameText).ToList(), GetExpectedResults(testName, featureFileName).ToList()); validator.ShouldPassBasicStructuralChecks(); validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); @@ -214,10 +215,10 @@ private void AddUtilClassWithFileSystemPath() $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\CCK\"; }} }} "); } - private IEnumerable GetExpectedResults(string scenarioName) + private IEnumerable GetExpectedResults(string testName, string featureFileName) { var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{testName}\\{featureFileName}.feature.ndjson")); foreach (var json in expectedJsonText) { @@ -226,13 +227,16 @@ private IEnumerable GetExpectedResults(string scenarioName) }; } - private IEnumerable GetActualResults(string scenarioName) + private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; - var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); + + // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario + + var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{fileName}.ndjson")); foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 30a48b1bd..eed4580af 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -38,7 +38,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances - .ComparingByValue() + .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() @@ -91,6 +91,56 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(info => info.Path.EndsWith("Language")) + + // Using special logic to compare regular expression strings (ignoring the differences of the regex anchor characters) + .Using>(ctx => + { + var subjects = ctx.Subject; + var expectations = ctx.Expectation; + subjects.Should().HaveSameCount(expectations); + int count = subjects.Count; + for (int i = 0; i < count; i++) + { + string subject = subjects[i]; + string expectation = expectations[i]; + if ((subject.Length > 0 && subject[0] == '^') || (expectation.Length > 0 && expectation[0] == '^') || + (subject.Length > 0 && subject[subject.Length - 1] == '$') || (expectation.Length > 0 && expectation[expectation.Length - 1] == '$')) + { + // If the first or last character is '^' or '$', remove it before comparing + subject = subject.Length > 0 && subject[0] == '^' ? subject.Substring(1) : subject; + subject = subject.Length > 0 && subject[subject.Length - 1] == '$' ? subject.Substring(0, subject.Length - 1) : subject; + expectation = expectation.Length > 0 && expectation[0] == '^' ? expectation.Substring(1) : expectation; + expectation = expectation.Length > 0 && expectation[expectation.Length - 1] == '$' ? expectation.Substring(0, expectation.Length - 1) : expectation; + } + subject.Should().Be(expectation); + } + }) + .When(info => info.Path.EndsWith("RegularExpressions")) + + // Using special logic to ignore ParameterTypeName except when the value is one of the basic types + .Using((ctx) => + { + if (ctx.Expectation == "string" || ctx.Expectation == "int" || ctx.Expectation == "long" || ctx.Expectation == "double" || ctx.Expectation == "float" + || ctx.Expectation == "short" || ctx.Expectation == "byte" || ctx.Expectation == "biginteger") + { + ctx.Subject.Should().Be(ctx.Expectation); + } + // Any other ParameterTypeName should be ignored, including {word} (no .NET equivalent) and custom type names + else + { + 1.Should().Be(1); + } + }) + .When(info => info.Path.EndsWith("ParameterTypeName")) + // Using a custom string comparison to ignore the differences in platform line endings .Using((ctx) => { @@ -100,7 +150,39 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() + .When(info => info.Path.EndsWith("Description") || info.Path.EndsWith("Text") || info.Path.EndsWith("Data")) + + // The list of hooks should contain at least as many items as the list of expected hooks + // Because Reqnroll does not support Tag Expressions, these are represented in RnR as multiple Hooks or multiple Tags on Hooks Binding methods + // which result in multiple Hook messages. + .Using>(ctx => + { + if (ctx.SelectedNode.IsRoot) + { + var actualList = ctx.Subject; + var expectedList = ctx.Expectation; + + if (expectedList == null || !expectedList.Any()) + { + return; // If expected is null or empty, we don't need to check anything + } + + actualList.Should().NotBeNull(); + actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, + "actual collection should have at least as many items as expected"); + + // Impossible to compare individual Hook messages (Ids aren't comparable, the Source references aren't compatible, + // and the Scope tags won't line up because the CCK uses tag expressions and RnR does not support them) +/* + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } +*/ } + }) + .WhenTypeIs>() // A bit of trickery here to tell FluentAssertions that Timestamps are always equal // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) @@ -171,28 +253,6 @@ public void ResultShouldPassAllComparisonTests() } } - - private void TestCasesShouldBeComparable() - { - CompareMessageType(); - } - - private void StepDefinitionsShouldBeComparable() - { - CompareMessageType(); - } - - private void PicklesShouldBeComparable() - { - CompareMessageType(); - } - - private void GherkinDocumentShouldBeComparable() - { - CompareMessageType(); - - } - private void CompareMessageType() { if (!expecteds_elementsByType.ContainsKey(typeof(T))) @@ -212,47 +272,9 @@ private void CompareMessageType() expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; actual.Should().BeEquivalentTo(expected, options => options - .Using>(ctx => - { - if (ctx.SelectedNode.IsRoot) - { - var actualList = ctx.Subject; - var expectedList = ctx.Expectation; - - if (expectedList == null || !expectedList.Any()) - { - return; // If expected is null or empty, we don't need to check anything - } - - actualList.Should().NotBeNull(); - actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, - "actual collection should have at least as many items as expected"); - - foreach (var expectedItem in expectedList) - { - actualList.Should().Contain(actualItem => - AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, - "actual collection should contain an item equivalent to {0}", expectedItem); - } - } - }) - .WhenTypeIs>() - // Using a custom string comparison that deals with ISO langauge codes when the property name ends with "Language" - .Using(ctx => - { - var actual = ctx.Subject.Split("-")[0]; - var expected = ctx.Expectation.Split("-")[0]; - actual.Should().Be(expected); - }) - .When(inf => inf.Path.EndsWith("Language")) .WithTracing()); } - private void SourceContentShouldBeIdentical() - { - CompareMessageType(); - } - public void ResultShouldPassBasicSanityChecks() { EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); @@ -271,25 +293,29 @@ public void ShouldPassBasicStructuralChecks() { var actual = actualEnvelopes; var expected = expectedEnvelopes; - actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); - // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) - foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) + using (new AssertionScope()) { - if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the actual but not in the expected."); - } - if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the expected but not in the actual."); - } - if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); + + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { - actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); } - if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - actuals_elementsByType[messageType].Count().Should().BeGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 8ec3541da..1cd13c029 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -21,17 +21,21 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) // Uri is not always comparable (eg, CCK refers to source file paths in typescript) // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references + // Exception: Exceptions are not comparable // Duration: time values are not comparable + // UseForSnippets: Reqnroll defaults to false always regadless of what is in the CCK // Start: Start refers to a column position in source code, which may not be comparable across platforms. // FileName: CCK does not provide the file name of attachments but Reqnroll does // ProtocolVersion, Implementation, Runtime, Cpu, Os, Ci: These properties of the Meta message are not comparable across platforms. // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) - private List PropertiesToSkip = new List() { "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", - "Duration", "Start", "FileName", "Message", "Type", "StackTrace", + private List PropertiesToSkip = new List() { + "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Exception", + "Duration", "Start", "FileName", "Message", "Type", "StackTrace", "UseForSnippets", "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "TestCaseStartedId","TestStepId", "TestCaseId", "WorkerId", - "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci"}; + "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci" + }; public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) { From b06135a8f46c99bcae347b42898a6690a993d571 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:05:12 -0500 Subject: [PATCH 054/218] Hooks now working. Fixed BASE64 encoding of svg file in CCK. Separate treatment of how hook-related TestStepFinished messages are handled. --- .../CucumberMessageFactory.cs | 14 +++-- .../CucumberMessagePublisher.cs | 13 +---- .../CucumberMessagesValidator.cs | 54 ++++++++++++------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 0ef49c3c9..baed74f65 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; @@ -89,7 +90,7 @@ internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBin internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { var regex = stepTransform.Regex.ToString(); - var name = stepTransform.Name; + var name = stepTransform.Name ?? stepTransform.Method.ReturnType.Name; var result = new ParameterType ( name, @@ -343,8 +344,15 @@ private static string GenerateSignature(IBinding stepDefinition) } public static string Base64EncodeFile(string filePath) { - byte[] fileBytes = File.ReadAllBytes(filePath); - return Convert.ToBase64String(fileBytes); + if (Path.GetExtension(filePath) == ".png" || Path.GetExtension(filePath) == ".jpg") + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } + // else assume its a text file + string text = File.ReadAllText(filePath); + text = text.Replace("\r\n", "\n"); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index b6f18124c..e9be13919 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -1,20 +1,9 @@ using Reqnroll.BoDi; using Reqnroll.Events; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Io.Cucumber.Messages; -using Io.Cucumber.Messages.Types; using Reqnroll.Tracing; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; -using Reqnroll.Time; -using Cucumber.Messages; -using Reqnroll.Bindings; -using System.Reflection; using System.Collections.Concurrent; -using System.Linq; using System.Diagnostics; namespace Reqnroll.CucumberMessages @@ -28,7 +17,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - // Debugger.Launch(); + Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index eed4580af..a64d5dfcb 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -17,8 +17,8 @@ public class CucumberMessagesValidator private Dictionary> expecteds_IDsByType = new(); private Dictionary> actuals_elementsByType = new(); private Dictionary> expecteds_elementsByType = new(); - private Dictionary> actuals_elementsByID = new(); - private Dictionary> expecteds_elementsByID = new(); + private Dictionary actuals_elementsByID = new(); + private Dictionary expecteds_elementsByID = new(); private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; // Envelope types - these are the top level types in CucumberMessages @@ -173,14 +173,15 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable - AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, - "actual collection should contain an item equivalent to {0}", expectedItem); - } -*/ } + /* + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } + */ + } }) .WhenTypeIs>() @@ -195,7 +196,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) + private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) { var xrefBuilder = new CrossReferenceBuilder(msg => { @@ -222,13 +223,9 @@ private static void InsertIntoIDsByType(object msg, Dictionary> elementsByID) + private static void InsertIntoElementsById(object msg, Dictionary elementsByID) { - if (!elementsByID.ContainsKey(msg.Id())) - { - elementsByID.Add(msg.Id(), new HashSet()); - } - elementsByID[msg.Id()].Add(msg); + elementsByID.Add(msg.Id(), msg); } private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) @@ -271,8 +268,27 @@ private void CompareMessageType() expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; - actual.Should().BeEquivalentTo(expected, options => options - .WithTracing()); + if (!(typeof(T) == typeof(TestStepFinished))) + { + actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); + } + else + { + // For TestStepFinished, we will separate out those related to hooks; + // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions + // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses + // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. + // if it has a hookId, it is a hook step + + var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + + actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); + + actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); + } } public void ResultShouldPassBasicSanityChecks() From bfcfe767121ff4b5ef34a75da76f910d4cafade4 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:16:40 -0500 Subject: [PATCH 055/218] Added stack-traces scenario from CCK. FluentAssertion structural comparisons working for Cdata, hooks, minimal, parameter-types, pending, stack-traces, undefined, and unknown-parameter-type. The remaining won't work until handling of DataTables is fixed. Next: ID walking to test referential integrity. --- .../CucumberMessageFactory.cs | 6 ++- .../FeatureEventProcessor.cs | 47 ++++++++++++------- .../CCK/pending/pending.cs | 12 ++--- .../CCK/stack-traces/stack_traces.cs | 19 ++++++++ .../CCK/stack-traces/stack_traces.feature | 9 ++++ .../stack-traces/stack_traces.feature.ndjson | 12 +++++ .../CucumberCompatibilityTests.cs | 3 +- ...CucumberMessages.CompatibilityTests.csproj | 6 +++ .../CucumberMessagesValidator.cs | 10 ++++ 9 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index baed74f65..d77a5b22f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -86,6 +86,10 @@ internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBin var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); return stepDefinitionPattern; } + internal static UndefinedParameterType ToUndefinedParameterType(string expression, string paramName, IIdGenerator iDGenerator) + { + return new UndefinedParameterType(expression, paramName); + } internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { @@ -251,7 +255,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat return status switch { ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, - ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.UNDEFINED, ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 8a8338527..710b29ab5 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; namespace Reqnroll.CucumberMessages { @@ -106,29 +107,39 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv } var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - if (bindingRegistry.IsValid) + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) { - foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) { - var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); - yield return Envelope.Create(parameterType); + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); + yield return Envelope.Create(undefinedParameterType); } - foreach (var binding in bindingRegistry.GetStepDefinitions()) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); + } - yield return Envelope.Create(stepDefinition); - } + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); - foreach (var hookBinding in bindingRegistry.GetHooks()) - { - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); - var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - HookDefinitionsByPattern.TryAdd(hookId, hook.Id); - yield return Envelope.Create(hook); - } + yield return Envelope.Create(stepDefinition); + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + HookDefinitionsByPattern.TryAdd(hookId, hook.Id); + yield return Envelope.Create(hook); } yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs index 6d2fb8755..9ca0959fe 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs @@ -10,12 +10,6 @@ namespace CucumberMessages.CompatibilityTests.CCK.pending [Binding] internal class Pending { - [Given("an unimplemented pending step")] - public void GivenAnUnimplementedPendingStep() - { - throw new PendingStepException(); - } - [Given("an implemented non-pending step")] public void GivenAnImplementedNonPendingStep() { @@ -27,5 +21,11 @@ public void GivenAnImplementedStepThatIsSkipped() { throw new ApplicationException("This step should not have been executed"); } + + [Given("an unimplemented pending step")] + public void GivenAnUnimplementedPendingStep() + { + throw new PendingStepException(); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs new file mode 100644 index 000000000..c2f9d72c1 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.stack_traces +{ + [Binding] + internal class stack_traces + { + [When(@"a step throws an exception")] + public void WhenAStepThrowsAnException() + { + throw new Exception("BOOM"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature new file mode 100644 index 000000000..587d50589 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature @@ -0,0 +1,9 @@ +Feature: Stack traces + Stack traces can help you diagnose the source of a bug. + Cucumber provides helpful stack traces that includes the stack frames from the + Gherkin document and remove uninteresting frames by default + + The first line of the stack trace will contain a reference to the feature file. + + Scenario: A failing step + When a step throws an exception diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson new file mode 100644 index 000000000..4bbcea535 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Stack traces\n Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.\n\n Scenario: A failing step\n When a step throws an exception\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/stack-traces/stack-traces.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":8},"name":"A failing step","steps":[{"id":"1","keyword":"When ","keywordType":"Action","location":{"column":5,"line":9},"text":"a step throws an exception"}],"tags":[]}}],"description":" Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Stack traces","tags":[]},"uri":"samples/stack-traces/stack-traces.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"A failing step","steps":[{"astNodeIds":["1"],"id":"3","text":"a step throws an exception","type":"Action"}],"tags":[],"uri":"samples/stack-traces/stack-traces.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"a step throws an exception","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/stack-traces/stack-traces.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"BOOM","type":"Error"},"message":"BOOM\nsamples/stack-traces/stack-traces.feature:9","status":"FAILED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 9eb24ec4d..85316410c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -184,7 +184,8 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("parameter-types", "Parameter Types")] [DataRow("skipped", "Skipping scenarios")] [DataRow("undefined", "Undefined steps")] - [DataRow("unknown-parameter-type", "Parameter Types")] + [DataRow("unknown-parameter-type", "Unknown Parameter Types")] + [DataRow("stack-traces", "Stack traces")] [DataRow("rules", "Usage of a 'Rule'")] public void CCKScenarios(string testName, string featureNameText) { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 6dcc8cd5e..1a8f594c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -20,6 +20,7 @@ + @@ -45,6 +46,8 @@ + + @@ -103,6 +106,9 @@ Always + + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index a64d5dfcb..6a97cc728 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -173,6 +173,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable actual, IEnumerable>() + // Groups are nested self-referential objects inside of StepMatchArgument(s). Other Cucumber implementations support a more sophisticated + // version of this structure in which multiple regex capture groups are conveyed inside of a single StepMatchArgument + // For Reqnroll, we will only compare the outermost Group; the only property we care about is the Value. + .Using((ctx) => + { + ctx.Subject.Value.Should().Be(ctx.Expectation.Value); + }) + .WhenTypeIs() + // A bit of trickery here to tell FluentAssertions that Timestamps are always equal // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) .Using(ctx => 1.Should().Be(1)) From 0ddb463f0582cd028a241e888e3b7935743f1ca0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:19:08 -0500 Subject: [PATCH 056/218] Added Ambiguous scenario and corrected error in the Factory in how test step results statuses were mapped. --- .../CucumberMessageFactory.cs | 2 +- .../CCK/ambiguous/ambiguous.cs | 22 +++++++++++++++++++ .../CCK/ambiguous/ambiguous.feature | 9 ++++++++ .../CCK/ambiguous/ambiguous.feature.ndjson | 16 ++++++++++++++ .../CucumberCompatibilityTests.cs | 10 ++++++++- ...CucumberMessages.CompatibilityTests.csproj | 6 +++++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index d77a5b22f..c55847927 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -255,7 +255,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat return status switch { ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, - ScenarioExecutionStatus.BindingError => TestStepResultStatus.UNDEFINED, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs new file mode 100644 index 000000000..6529a6a92 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs @@ -0,0 +1,22 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.ambiguous +{ + [Binding] + internal class Ambiguous + { + [Given(@"a step that matches more than one step binding")] + public void FirstMatchingStep() { } + + [Given(@"a step that matches more than one step binding")] + public void SecondMatchingStep() { } + + [Then(@"this step gets skipped because of the prior ambiguous step")] + public void ThirdSkippedStep() { } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature new file mode 100644 index 000000000..0ba800aa1 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature @@ -0,0 +1,9 @@ +Feature: ambiguous + +This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions + +NOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite + +Scenario: Ambiguous + Given a step that matches more than one step binding + Then this step gets skipped because of the prior ambiguous step diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson new file mode 100644 index 000000000..d4afb6c48 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson @@ -0,0 +1,16 @@ +{"meta":{"protocolVersion":"26.0.0","implementation":{"name":"Reqnroll","version":"2.1.1-local+46152309beb5ca95501132a597284bc53db62081"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","data":"Feature: ambiguous\r\n\r\nThis feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite\r\n\r\nScenario: Ambiguous\r\n\tGiven a step that matches more than one step binding\r\n\tThen this step gets skipped because of the prior ambiguous step\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"ambiguous","description":"This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite","children":[{"scenario":{"location":{"line":7,"column":1},"tags":[],"keyword":"Scenario","name":"Ambiguous","description":"","steps":[{"location":{"line":8,"column":2},"keyword":"Given ","keywordType":"Context","text":"a step that matches more than one step binding","id":"0"},{"location":{"line":9,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"this step gets skipped because of the prior ambiguous step","id":"1"}],"examples":[],"id":"2"}}]},"comments":[]}} +{"pickle":{"id":"5","uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","name":"Ambiguous","language":"en-US","steps":[{"astNodeIds":["0"],"id":"3","type":"Context","text":"a step that matches more than one step binding"},{"astNodeIds":["1"],"id":"4","type":"Outcome","text":"this step gets skipped because of the prior ambiguous step"}],"tags":[],"astNodeIds":["2"]}} +{"stepDefinition":{"id":"6","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"FirstMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"7","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"SecondMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"8","pattern":{"source":"this step gets skipped because of the prior ambiguous step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"ThirdSkippedStep","methodParameterTypes":[]}}}} +{"testRunStarted":{"timestamp":{"seconds":1726222400,"nanos":625585200}}} +{"testCase":{"id":"9","pickleId":"5","testSteps":[{"id":"11","pickleStepId":"3","stepDefinitionIds":["6","7"],"stepMatchArgumentsLists":[]},{"id":"12","pickleStepId":"4","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"10","testCaseId":"9","timestamp":{"seconds":1726222400,"nanos":664903000}}} +{"testStepStarted":{"testCaseStartedId":"10","testStepId":"11","timestamp":{"seconds":1726222400,"nanos":666997500}}} +{"testStepFinished":{"testCaseStartedId":"10","testStepId":"11","testStepResult":{"duration":{"seconds":0,"nanos":7889700},"message":"","status":"AMBIGUOUS"},"timestamp":{"seconds":1726222400,"nanos":674887200}}} +{"testStepStarted":{"testCaseStartedId":"10","testStepId":"12","timestamp":{"seconds":1726222400,"nanos":679031100}}} +{"testStepFinished":{"testCaseStartedId":"10","testStepId":"12","testStepResult":{"duration":{"seconds":0,"nanos":817600},"message":"","status":"SKIPPED"},"timestamp":{"seconds":1726222400,"nanos":679848700}}} +{"testCaseFinished":{"testCaseStartedId":"10","timestamp":{"seconds":1726222400,"nanos":688807200},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"seconds":1726222400,"nanos":697037200}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 85316410c..8ec163a94 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -176,7 +176,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [TestMethod] [DataRow("attachments", "Attachments")] [DataRow("minimal", "minimal")] - [DataRow("cdata","cdata")] + [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] [DataRow("examples-tables", "Examples Tables")] [DataRow("hooks", "Hooks")] @@ -209,6 +209,14 @@ public void CCKScenarios(string testName, string featureNameText) ConfirmAllTestsRan(null); } + [TestMethod] + [DataRow("ambiguous", "ambiguous")] + public void NonCCKScenarios(string testName, string featureNameText) + { + CCKScenarios(testName, featureNameText); + } + + private void AddUtilClassWithFileSystemPath() { string location = AppContext.BaseDirectory; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 1a8f594c0..aea923ac8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,6 +10,7 @@ + @@ -26,6 +27,8 @@ + + @@ -56,8 +59,11 @@ + + + Always From 7d0491cd8cef086d142698de078493063d17390b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:19:23 -0500 Subject: [PATCH 057/218] Added referential integrity validations on Actuals. --- .../CucumberCompatibilityTests.cs | 2 +- .../CucumberMessagesValidator.cs | 360 +++++++++++------- 2 files changed, 217 insertions(+), 145 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 8ec163a94..9a63cc6e9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -203,8 +203,8 @@ public void CCKScenarios(string testName, string featureNameText) var validator = new CucumberMessagesValidator(GetActualResults(testName, featureNameText).ToList(), GetExpectedResults(testName, featureFileName).ToList()); validator.ShouldPassBasicStructuralChecks(); - validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); + validator.ResultShouldPassSanityChecks(); ConfirmAllTestsRan(null); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 6a97cc728..ef0821001 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -36,6 +36,215 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) + { + var xrefBuilder = new CrossReferenceBuilder(msg => + { + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + }); + foreach (var message in messages) + { + var msg = message.Content(); + CucumberMessageVisitor.Accept(xrefBuilder, msg); + } + } + private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) + { + if (!IDsByType.ContainsKey(msg.GetType())) + { + IDsByType.Add(msg.GetType(), new HashSet()); + } + IDsByType[msg.GetType()].Add(msg.Id()); + } + + private static void InsertIntoElementsById(object msg, Dictionary elementsByID) + { + elementsByID.Add(msg.Id(), msg); + } + + private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) + { + if (!elementsByType.ContainsKey(msg.GetType())) + { + elementsByType.Add(msg.GetType(), new HashSet()); + } + elementsByType[msg.GetType()].Add(msg); + } + + public void ResultShouldPassAllComparisonTests() + { + var method = typeof(CucumberMessagesValidator).GetMethod(nameof(CompareMessageType), BindingFlags.NonPublic | BindingFlags.Instance); + using (new AssertionScope()) + { + foreach (Type t in EnvelopeTypes) + { + var genMethod = method!.MakeGenericMethod(t); + genMethod.Invoke(this, null); + } + } + } + + private void CompareMessageType() + { + if (!expecteds_elementsByType.ContainsKey(typeof(T))) + return; + + HashSet? actuals; + List actual; + List expected; + + if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) + { + actual = actuals.OfType().ToList(); + } + else + actual = new List(); + + expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; + + if (!(typeof(T) == typeof(TestStepFinished))) + { + actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); + } + else + { + // For TestStepFinished, we will separate out those related to hooks; + // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions + // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses + // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. + // if it has a hookId, it is a hook step + + var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + + actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); + + actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); + } + } + + public void ResultShouldPassSanityChecks() + { + EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + TestExecutionStepsShouldProperlyReferenceTestCases(); + TestExecutionMessagesShouldProperlyNest(); + ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + } + + private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() + { + } + + private void TestExecutionStepsShouldProperlyReferenceTestCases() + { + var testCaseIds = actuals_elementsByType[typeof(TestCase)].OfType().Select(tc => tc.Id).ToList(); + + var testCaseStarteds = actuals_elementsByType[typeof(TestCaseStarted)].OfType().ToList(); + testCaseIds.Should().Contain(id => testCaseStarteds.Any(tcs => tcs.TestCaseId == id), "a test case should be referenced by a test case started message"); + + var testCaseStartedIds = testCaseStarteds.Select(tcs => tcs.Id).ToList(); + + var testCaseFinisheds = actuals_elementsByType[typeof(TestCaseFinished)].OfType().ToList(); + var testStepStarteds = actuals_elementsByType[typeof(TestStepStarted)].OfType().ToList(); + var testStepFinisheds = actuals_elementsByType[typeof(TestStepFinished)].OfType().ToList(); + + testCaseStartedIds.Should().Contain(id => testStepStarteds.Any(tss => tss.TestCaseStartedId == id), "a test case started should be referenced by at least one test step started message"); + testCaseStartedIds.Should().Contain(id => testStepFinisheds.Any(tsf => tsf.TestCaseStartedId == id), "a test case started should be referenced by at least one test step finished message"); + testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); + } + + private void TestExecutionMessagesShouldProperlyNest() + { + //walk sequence of messages, using stacks/lists/sets to keep track of nesting + } + + private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook() + { + var testCases = actuals_elementsByType[typeof(TestCase)].OfType(); + foreach (var testCase in testCases) + { + var pickle = testCase.PickleId; + actuals_elementsByID.Should().ContainKey(pickle, "a pickle should be referenced by the test case"); + + var steps = testCase.TestSteps.OfType(); + foreach (var step in steps) + { + if (step.HookId != null) + actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); + + if (step.PickleStepId != null) + actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); + + if (step.StepDefinitionIds != null && step.StepDefinitionIds.Count > 0) + { + foreach (var stepDefinitionId in step.StepDefinitionIds) + actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); + } + } + } + } + + private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() + { + var pickles = actuals_elementsByType[typeof(Pickle)].OfType(); + foreach (var pickle in pickles) + { + var testCases = actuals_elementsByType[typeof(TestCase)].OfType(); + testCases.Should().Contain(tc => tc.PickleId == pickle.Id, "a pickle should be referenced by a test case"); + + var pickleSteps = pickle.Steps.OfType(); + foreach (var pickleStep in pickleSteps) + { + var testSteps = actuals_elementsByType[typeof(TestStep)].OfType(); + testSteps.Should().Contain(ts => ts.PickleStepId == pickleStep.Id, "a pickle step should be referenced by a test step"); + } + } + } + + public void ShouldPassBasicStructuralChecks() + { + var actual = actualEnvelopes; + var expected = expectedEnvelopes; + + using (new AssertionScope()) + { + actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); + + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) + { + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); + } + } + } + + private void ArrangeGlobalFluentAssertionOptions() + { AssertionOptions.AssertEquivalencyUsing(options => options // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances .ComparingByMembers() @@ -143,13 +352,13 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable((ctx) => - { - var subject = ctx.Subject ?? string.Empty; - var expectation = ctx.Expectation ?? string.Empty; - subject = subject.Replace("\r\n", "\n"); - expectation = expectation.Replace("\r\n", "\n"); - subject.Should().Be(expectation); - }) + { + var subject = ctx.Subject ?? string.Empty; + var expectation = ctx.Expectation ?? string.Empty; + subject = subject.Replace("\r\n", "\n"); + expectation = expectation.Replace("\r\n", "\n"); + subject.Should().Be(expectation); + }) .When(info => info.Path.EndsWith("Description") || info.Path.EndsWith("Text") || info.Path.EndsWith("Data")) // The list of hooks should contain at least as many items as the list of expected hooks @@ -206,143 +415,6 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) - { - var xrefBuilder = new CrossReferenceBuilder(msg => - { - InsertIntoElementsByType(msg, elementsByType); - - if (msg.HasId()) - { - InsertIntoElementsById(msg, elementsByID); - InsertIntoIDsByType(msg, IDsByType); - } - }); - foreach (var message in messages) - { - var msg = message.Content(); - CucumberMessageVisitor.Accept(xrefBuilder, msg); - } - } - private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) - { - if (!IDsByType.ContainsKey(msg.GetType())) - { - IDsByType.Add(msg.GetType(), new HashSet()); - } - IDsByType[msg.GetType()].Add(msg.Id()); - } - - private static void InsertIntoElementsById(object msg, Dictionary elementsByID) - { - elementsByID.Add(msg.Id(), msg); - } - - private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) - { - if (!elementsByType.ContainsKey(msg.GetType())) - { - elementsByType.Add(msg.GetType(), new HashSet()); - } - elementsByType[msg.GetType()].Add(msg); - } - public void ResultShouldPassAllComparisonTests() - { - var method = typeof(CucumberMessagesValidator).GetMethod(nameof(CompareMessageType), BindingFlags.NonPublic | BindingFlags.Instance); - using (new AssertionScope()) - { - foreach (Type t in EnvelopeTypes) - { - var genMethod = method!.MakeGenericMethod(t); - genMethod.Invoke(this, null); - } - } - } - - private void CompareMessageType() - { - if (!expecteds_elementsByType.ContainsKey(typeof(T))) - return; - - HashSet? actuals; - List actual; - List expected; - - if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) - { - actual = actuals.OfType().ToList(); - } - else - actual = new List(); - - expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; - - if (!(typeof(T) == typeof(TestStepFinished))) - { - actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); - } - else - { - // For TestStepFinished, we will separate out those related to hooks; - // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions - // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses - // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. - // if it has a hookId, it is a hook step - - var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); - var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); - var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); - var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); - - actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); - - actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); - } - } - - public void ResultShouldPassBasicSanityChecks() - { - EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); - EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); - } - - private void EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook() - { - } - - private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() - { - } - - public void ShouldPassBasicStructuralChecks() - { - var actual = actualEnvelopes; - var expected = expectedEnvelopes; - - using (new AssertionScope()) - { - actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); - - // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) - foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) - { - if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the actual but not in the expected."); - } - if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the expected but not in the actual."); - } - if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - { - actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); - } - if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); - } - } - } } } \ No newline at end of file From 65faa43cfcdf0f71865792001a105cb080860590 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:44:29 -0500 Subject: [PATCH 058/218] Validation test suite functionally complete. --- .../CucumberMessagesValidator.cs | 112 ++++++++++++++++-- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index ef0821001..7bcb01a15 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -136,15 +136,48 @@ private void CompareMessageType() public void ResultShouldPassSanityChecks() { - EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); - EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); - TestExecutionStepsShouldProperlyReferenceTestCases(); - TestExecutionMessagesShouldProperlyNest(); - ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + using (new AssertionScope()) + { + EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + TestExecutionStepsShouldProperlyReferenceTestCases(); + TestExecutionMessagesShouldProperlyNest(); + ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + } } private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() { + // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message + + var actualTestStepStarted_TestStepIds = actuals_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); + var expectedTestStepStarteds_TestStepIds = expecteds_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); + + // Making the assumption here that the order of TestStepStarted messages is the same in both Actual and Expected + // pair these up, and walk back to the pickle step text and compare + + actualTestStepStarted_TestStepIds + .Zip(expectedTestStepStarteds_TestStepIds, (a, e) => (a, e)) + .ToList() + .ForEach(t => + { + actuals_elementsByID[t.a].Should().BeAssignableTo(); ; + var actualTS = actuals_elementsByID[t.a] as TestStep; + expecteds_elementsByID[t.e].Should().BeAssignableTo(); ; + var expectedTS = expecteds_elementsByID[t.e] as TestStep; + if (actualTS!.PickleStepId != null && expectedTS!.PickleStepId != null) + { + actuals_elementsByID[actualTS.PickleStepId].Should().BeAssignableTo(); ; + var actualPickleStep = actuals_elementsByID[actualTS.PickleStepId] as PickleStep; + expecteds_elementsByID[expectedTS.PickleStepId].Should().BeAssignableTo(); ; + var expectedPickleStep = expecteds_elementsByID[expectedTS.PickleStepId] as PickleStep; + actualPickleStep!.Text.Should().Be(expectedPickleStep!.Text, $"expecting the text of the pickle step {actualPickleStep.Id} to match that of {expectedPickleStep.Id}"); + } + else + { // confirm that both are null or not null, if one is null, throw an exception + actualTS.PickleStepId.Should().Be(expectedTS!.PickleStepId, "expecting both PickleStepIds to be null or not null"); + } + }); } private void TestExecutionStepsShouldProperlyReferenceTestCases() @@ -167,7 +200,68 @@ private void TestExecutionStepsShouldProperlyReferenceTestCases() private void TestExecutionMessagesShouldProperlyNest() { - //walk sequence of messages, using stacks/lists/sets to keep track of nesting + var ClosedIDs = new List(); + var OpenTestCaseStartedIDs = new List(); + var OpenTestStepIds = new List(); + var numberOfEnvelopes = actualEnvelopes.Count(); + var testRunStartedSeenAtEnvelopeIndex = numberOfEnvelopes + 1; + var testRunFinishedSeenAtEnvelopeIndex = -1; + int currentIndex = 0; + foreach (object msg in actualEnvelopes.Select(e => e.Content())) + { + switch (msg) + { + case TestRunStarted testRunStarted: + testRunStartedSeenAtEnvelopeIndex = currentIndex; + if (testRunFinishedSeenAtEnvelopeIndex != -1) + testRunStartedSeenAtEnvelopeIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestRunStarted events must be before TestRunFinished event"); + break; + case TestRunFinished testRunFinished: + testRunFinishedSeenAtEnvelopeIndex = currentIndex; + testRunFinishedSeenAtEnvelopeIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestRunFinished events must be after TestRunStarted event"); + testRunFinishedSeenAtEnvelopeIndex.Should().Be(numberOfEnvelopes - 1, "TestRunFinished events must be the last event"); + break; + case TestCaseStarted testCaseStarted: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestCaseStarted events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestCaseStarted events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testCaseStarted.Id, "a test case should not be Started twice"); + OpenTestCaseStartedIDs.Add(testCaseStarted.Id); + break; + case TestCaseFinished testCaseFinished: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestCaseFinished events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestCaseFinished events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testCaseFinished.TestCaseStartedId, "a test case should not be Finished twice"); + OpenTestCaseStartedIDs.Should().Contain(testCaseFinished.TestCaseStartedId, "a test case should be Started and active before it is Finished"); + OpenTestCaseStartedIDs.Remove(testCaseFinished.TestCaseStartedId); + ClosedIDs.Add(testCaseFinished.TestCaseStartedId); + OpenTestCaseStartedIDs.Remove(testCaseFinished.TestCaseStartedId); + break; + case TestStepStarted testStepStarted: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestStepStarted events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestStepStarted events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testStepStarted.TestCaseStartedId, "a TestStepStarted event must refer to an active test case"); + OpenTestCaseStartedIDs.Should().Contain(testStepStarted.TestCaseStartedId, "a TestStepStarted event must refer to an active test case"); + OpenTestStepIds.Add(testStepStarted.TestStepId); + break; + case TestStepFinished testStepFinished: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestStepFinished events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestStepFinished events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testStepFinished.TestCaseStartedId, "a TestStepFinished event must refer to an active test case"); + ClosedIDs.Should().NotContain(testStepFinished.TestStepId, "a TestStepFinished event must refer to an active test step"); + OpenTestCaseStartedIDs.Should().Contain(testStepFinished.TestCaseStartedId, "a TestStepFinished event must refer to an active test case"); + OpenTestStepIds.Should().Contain(testStepFinished.TestStepId, "a TestStepFinished event must refer to an active test step"); + ClosedIDs.Add(testStepFinished.TestStepId); + OpenTestStepIds.Remove(testStepFinished.TestStepId); + break; + default: + break; + } + currentIndex++; + } } private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook() @@ -182,15 +276,15 @@ private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOr foreach (var step in steps) { if (step.HookId != null) - actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); + actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); if (step.PickleStepId != null) - actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); + actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); if (step.StepDefinitionIds != null && step.StepDefinitionIds.Count > 0) { foreach (var stepDefinitionId in step.StepDefinitionIds) - actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); + actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); } } } From 2648b5cf7bd60474ffdee718fef5d00816be42fa Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:17:20 -0500 Subject: [PATCH 059/218] Revert "Given default Gherkin.Ast.Locations of (0,0) to generated Table elements (headers, rows, cells)." This reverts commit d9ada246b7b1a2a5bd027febe61942e1c784a393. --- .../Transformation/IncludeExternalDataTransformation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs index 3441f3d5d..c649d032b 100644 --- a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs +++ b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs @@ -85,7 +85,7 @@ private Scenario GetTransformedScenario(Scenario scenario, ExternalDataSpecifica { var exampleRecords = specification.GetExampleRecords(examplesHeaderNames); var exampleRows = exampleRecords.Items - .Select(rec => new Gherkin.Ast.TableRow(new Location(0, 0), exampleRecords.Header.Select(h => new TableCell(new Location(0, 0), rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) + .Select(rec => new Gherkin.Ast.TableRow(null, exampleRecords.Header.Select(h => new TableCell(null, rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) .ToArray(); var examplesBlock = CreateExamplesBlock(exampleRecords.Header, exampleRows, examplesKeyword); @@ -108,8 +108,8 @@ private Examples CreateExamplesBlock(string[] headerNames, Gherkin.Ast.TableRow[ { keyword ??= "External Examples"; var name = "External Examples"; - var tableHeader = new Gherkin.Ast.TableRow(new Location(0, 0), headerNames.Select(h => new TableCell(new Location(0, 0), h)).ToArray()); - return new Examples(new Tag[0], new Location(0, 0), keyword, name, "", tableHeader, exampleRows); + var tableHeader = new Gherkin.Ast.TableRow(null, headerNames.Select(h => new TableCell(null, h)).ToArray()); + return new Examples(new Tag[0], null, keyword, name, "", tableHeader, exampleRows); } } } From 4e923df02704de12ca2019b79b6ab14e77b65807 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:18:08 -0500 Subject: [PATCH 060/218] Modified CucumberMessagesConverter to include transformation of the AST to patch in missing Location elements before the AST is transformed to the Cucumber GherkinDocument. This duplicates the GherkinDocumentVisitor and ScenarioTransformation classes from the External Plugin (pending further refactoring). --- Reqnroll.Parser/CucumberMessagesConverter.cs | 4 +- Reqnroll.Parser/GherkinDocumentVisitor.cs | 142 ++++++++++++++++++ ...chMissingLocationElementsTransformation.cs | 51 +++++++ Reqnroll.Parser/ScenarioTransformation.cs | 122 +++++++++++++++ .../CucumberMessagePublisher.cs | 2 +- .../CucumberCompatibilityTests.cs | 2 +- 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 Reqnroll.Parser/GherkinDocumentVisitor.cs create mode 100644 Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs create mode 100644 Reqnroll.Parser/ScenarioTransformation.cs diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index ec9e26af7..9369631cd 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -15,9 +15,11 @@ public CucumberMessagesConverter(IIdGenerator idGenerator) } public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) { + var NullLocationPatcher = new PatchMissingLocationElementsTransformation(); + var gherkinDocumentWithLocation = NullLocationPatcher.TransformDocument(gherkinDocument); var converter = new AstMessagesConverter(_idGenerator); var location = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)); - return converter.ConvertGherkinDocumentToEventArgs(gherkinDocument, location); + return converter.ConvertGherkinDocumentToEventArgs(gherkinDocumentWithLocation, location); } public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) diff --git a/Reqnroll.Parser/GherkinDocumentVisitor.cs b/Reqnroll.Parser/GherkinDocumentVisitor.cs new file mode 100644 index 000000000..4be6eccaa --- /dev/null +++ b/Reqnroll.Parser/GherkinDocumentVisitor.cs @@ -0,0 +1,142 @@ +using System; +using Gherkin.Ast; + +namespace Reqnroll.Parser +{ + abstract class GherkinDocumentVisitor + { + protected virtual void AcceptDocument(ReqnrollDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var featureChild in feature.Children) + { + if (featureChild is Rule rule) AcceptRule(rule); + else if (featureChild is Background background) AcceptBackground(background); + else if (featureChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (featureChild is Scenario scenario) AcceptScenario(scenario); + } + OnFeatureVisited(feature); + } + + protected virtual void AcceptStep(Step step) + { + OnStepVisited(step); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + OnScenarioVisited(scenario); + } + + protected virtual void AcceptScenarioOutline(ScenarioOutline scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var step in scenarioOutline.Steps) + { + AcceptStep(step); + } + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var ruleChild in rule.Children) + { + if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (ruleChild is Scenario scenario) AcceptScenario(scenario); + } + OnRuleVisited(rule); + } + + protected virtual void OnDocumentVisiting(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + } +} diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs new file mode 100644 index 000000000..0107efcd7 --- /dev/null +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -0,0 +1,51 @@ +using Gherkin.Ast; +using Reqnroll.ExternalData.ReqnrollPlugin.Transformation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.Parser +{ + internal class PatchMissingLocationElementsTransformation : ScenarioTransformation + { + protected override Scenario GetTransformedScenario(Scenario scenario) + { + return null; + } + + protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline) + { + if (scenarioOutline.Examples == null || !scenarioOutline.Examples.Any()) + return null; + + var exampleTables = scenarioOutline.Examples; + List transformedExamples = new List(); + + transformedExamples.AddRange(exampleTables.Select(e => PatchLocations(e))); + return new ScenarioOutline( + scenarioOutline.Tags.ToArray(), + PatchLocation(scenarioOutline.Location), + scenarioOutline.Keyword, + scenarioOutline.Name, + scenarioOutline.Description, + scenarioOutline.Steps.ToArray(), + transformedExamples.ToArray()); + } + + private Examples PatchLocations(Examples e) + { + var headerCells = e.TableHeader.Cells; + var tableHeader = new Gherkin.Ast.TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new Gherkin.Ast.TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); + var rows = e.TableBody.Select(r => new Gherkin.Ast.TableRow(PatchLocation(r.Location), r.Cells.Select(c => new Gherkin.Ast.TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); + return new Examples(e.Tags.ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); + } + + private static Location PatchLocation(Location l) + { + return l ?? new Location(0, 0); + } + + + } +} diff --git a/Reqnroll.Parser/ScenarioTransformation.cs b/Reqnroll.Parser/ScenarioTransformation.cs new file mode 100644 index 000000000..627577d78 --- /dev/null +++ b/Reqnroll.Parser/ScenarioTransformation.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gherkin.Ast; + +namespace Reqnroll.Parser +{ + abstract class ScenarioTransformation : GherkinDocumentVisitor + { + protected ReqnrollDocument _sourceDocument; + private ReqnrollDocument _transformedDocument; + private ReqnrollFeature _transformedFeature; + private bool _hasTransformedScenarioInFeature = false; + private bool _hasTransformedScenarioInCurrentRule = false; + private readonly List _featureChildren = new(); + private readonly List _ruleChildren = new(); + private List _currentChildren; + + public ReqnrollDocument TransformDocument(ReqnrollDocument document) + { + Reset(); + AcceptDocument(document); + return _transformedDocument ?? document; + } + + private void Reset() + { + _sourceDocument = null; + _transformedDocument = null; + _transformedFeature = null; + _featureChildren.Clear(); + _ruleChildren.Clear(); + _hasTransformedScenarioInFeature = false; + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _featureChildren; + } + + protected abstract Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline); + protected abstract Scenario GetTransformedScenario(Scenario scenario); + + protected override void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + var transformedScenarioOutline = GetTransformedScenarioOutline(scenarioOutline); + OnScenarioVisitedInternal(scenarioOutline, transformedScenarioOutline); + } + + protected override void OnScenarioVisited(Scenario scenario) + { + var transformedScenario = GetTransformedScenario(scenario); + OnScenarioVisitedInternal(scenario, transformedScenario); + } + + private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedScenario) + { + if (transformedScenario == null) + { + _currentChildren.Add(scenario); + return; + } + + _hasTransformedScenarioInFeature = true; + _hasTransformedScenarioInCurrentRule = true; + _currentChildren.Add(transformedScenario); + } + + protected override void OnBackgroundVisited(Background background) + { + _featureChildren.Add(background); + } + + protected override void OnRuleVisiting(Rule rule) + { + _ruleChildren.Clear(); + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _ruleChildren; + } + + protected override void OnRuleVisited(Rule rule) + { + _currentChildren = _featureChildren; + if (_hasTransformedScenarioInCurrentRule) + { + var transformedRule = new Rule( + rule.Tags?.ToArray() ?? Array.Empty(), + rule.Location, + rule.Keyword, + rule.Name, + rule.Description, + _ruleChildren.ToArray()); + _featureChildren.Add(transformedRule); + } + else + { + _featureChildren.Add(rule); + } + } + + protected override void OnFeatureVisited(Feature feature) + { + if (_hasTransformedScenarioInFeature) + _transformedFeature = new ReqnrollFeature( + feature.Tags?.ToArray() ?? Array.Empty(), + feature.Location, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + _featureChildren.ToArray()); + } + + protected override void OnDocumentVisiting(ReqnrollDocument document) + { + _sourceDocument = document; + } + + protected override void OnDocumentVisited(ReqnrollDocument document) + { + if (_transformedFeature != null) + _transformedDocument = new ReqnrollDocument(_transformedFeature, document.Comments.ToArray(), document.DocumentLocation); + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index e9be13919..7bd7a086f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -17,7 +17,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - Debugger.Launch(); + //Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 9a63cc6e9..3505f4d02 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -59,7 +59,7 @@ public void CucumberMessagesInteropWithExternalData() { // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages AddCucumberMessagePlugIn(); - _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.2.0-local"); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest From 8fb61c4477711e3999543d98a0dc46487665d799 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:59:17 -0500 Subject: [PATCH 061/218] Fixed: HookTestStep should have argument properties that are null. Fixed: copy/paste error of using statement in Location Element transformer --- Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs | 1 - Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 0107efcd7..87daa06ab 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -1,5 +1,4 @@ using Gherkin.Ast; -using Reqnroll.ExternalData.ReqnrollPlugin.Transformation; using System; using System.Collections.Generic; using System.Linq; diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index c55847927..49913a9dc 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -190,8 +190,8 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) hookId, hookStepState.TestStepID, null, - new List(), - new List()); + null, + null); } internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) { From d9b5d1ee617c9eb719e7127425e0e409d36b7304 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:23:23 -0500 Subject: [PATCH 062/218] Enhanced BindingException with sub-class AmbiguousBindingException to carry along the candidate BindingMatches that cause the ambiguity. These are used in the Step tracker and message factory to populate the array of StepDef Ids that cause the ambiguity. --- .../CucumberMessageFactory.cs | 5 +- .../ScenarioStepProcessor.cs | 8 +++ Reqnroll/ErrorHandling/BindingException.cs | 49 +++++++++++++++++++ Reqnroll/ErrorHandling/ErrorProvider.cs | 10 ++-- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 49913a9dc..4afbb55ab 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -122,7 +122,8 @@ private static SourceReference ToSourceRef(IBinding binding) internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { - bool bound = stepState.StepDefinitionId != null; + bool bound = stepState.Bound; + bool ambiguous = stepState.Ambiguous; var args = stepState.StepArguments .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) @@ -132,7 +133,7 @@ internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, null, stepState.TestStepID, stepState.PickleStepID, - bound ? new List { stepState.StepDefinitionId } : new List(), + bound ? new List { stepState.StepDefinitionId } : ambiguous ? new List(stepState.AmbiguousStepDefinitions) : new List(), bound ? new List { new StepMatchArgumentsList(args) } : new List() ); diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 7e6b53aca..7a779844a 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Reqnroll.CucumberMessages { @@ -26,6 +27,8 @@ public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base( public bool Bound { get; set; } public string CanonicalizedStepPattern { get; set; } public string StepDefinitionId { get; private set; } + public IEnumerable AmbiguousStepDefinitions { get; set; } + public bool Ambiguous { get { return AmbiguousStepDefinitions != null && AmbiguousStepDefinitions.Count() > 0;} } public IStepDefinitionBinding StepDefinition { get; set; } public List StepArguments { get; set; } @@ -64,6 +67,11 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) if (Status == ScenarioExecutionStatus.TestError && stepFinishedEvent.ScenarioContext.TestError != null) { Exception = stepFinishedEvent.ScenarioContext.TestError; + if (Exception is AmbiguousBindingException) + { + AmbiguousStepDefinitions = new List(((AmbiguousBindingException)Exception).Matches.Select(m => + FindStepDefIDByStepPattern(CucumberMessageFactory.CanonicalizeStepDefinitionPattern(m.StepBinding)))); + } } var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); diff --git a/Reqnroll/ErrorHandling/BindingException.cs b/Reqnroll/ErrorHandling/BindingException.cs index 308708cd6..3bb5fe4cd 100644 --- a/Reqnroll/ErrorHandling/BindingException.cs +++ b/Reqnroll/ErrorHandling/BindingException.cs @@ -1,4 +1,7 @@ +using Reqnroll.Bindings; using System; +using System.Collections; +using System.Collections.Generic; using System.Runtime.Serialization; // the exceptions are part of the public API, keep them in Reqnroll namespace @@ -25,4 +28,50 @@ protected BindingException( { } } + + [Serializable] + public class AmbiguousBindingException : BindingException + { + public IEnumerable Matches { get; private set; } + + public AmbiguousBindingException() + { + } + + public AmbiguousBindingException(string message) : base(message) + { + } + + public AmbiguousBindingException(string message, Exception inner) : base(message, inner) + { + } + + public AmbiguousBindingException(string message, IEnumerable matches) : base(message) + { + Matches = new List(matches); + } + + protected AmbiguousBindingException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + Matches = (List)info.GetValue("Matches", typeof(List)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + base.GetObjectData(info, context); + info.AddValue("Matches", Matches); + } + } } \ No newline at end of file diff --git a/Reqnroll/ErrorHandling/ErrorProvider.cs b/Reqnroll/ErrorHandling/ErrorProvider.cs index ce6731d3f..b1dc709cb 100644 --- a/Reqnroll/ErrorHandling/ErrorProvider.cs +++ b/Reqnroll/ErrorHandling/ErrorProvider.cs @@ -59,17 +59,19 @@ public Exception GetParameterCountError(BindingMatch match, int expectedParamete public Exception GetAmbiguousMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( - $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + return new AmbiguousBindingException( + $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetAmbiguousBecauseParamCheckMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( + return new AmbiguousBindingException( "Multiple step definitions found, but none of them have matching parameter count and type for step " - + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetNoMatchBecauseOfScopeFilterError(List matches, StepInstance stepInstance) From 3c6e111c7e01ce5ddb38689386284e2167da55b6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:22:50 -0500 Subject: [PATCH 063/218] Modified how step arguments are handled to prevent step DataTable and DocString from being rendered as StepMatchArguments. This allows for the DataTables CCK scenario to pass. --- Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 7a779844a..d13f26267 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -74,9 +74,10 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + var IsInputDataTableOrDocString = stepFinishedEvent.StepContext.StepInfo.Table != null || stepFinishedEvent.StepContext.StepInfo.MultilineText != null; var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); - StepArguments = Bound ? + StepArguments = Bound && !IsInputDataTableOrDocString ? argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); From f4f4a0b27e8538a0764f483a557fcf8377825bb3 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:53:08 -0500 Subject: [PATCH 064/218] Large (partial) refactor. Modified code gen so that pickleID is provided to each test method at runtime (as a parameter for row tests, hard-coded by code-gen for regular tests). PickleStepIds are retrieved at run-time (given the PickleId and sequence of steps taken). The PickleID is stored in ScenarioInfo and the pickleStepId is provided to the Given/When/Then. These are wired into the relevant ExecutionEvents. --- .../Generation/ScenarioPartHelper.cs | 6 +- .../Generation/UnitTestFeatureGenerator.cs | 17 +++- .../Generation/UnitTestMethodGenerator.cs | 85 ++++++++++++++++--- .../FeatureLevelCucumberMessages.cs | 11 ++- Reqnroll/CucumberMesssages/PickleJar.cs | 65 ++++++++++++++ Reqnroll/ITestRunner.cs | 10 +-- .../Infrastructure/ITestExecutionEngine.cs | 2 +- .../Infrastructure/TestExecutionEngine.cs | 4 +- Reqnroll/ScenarioInfo.cs | 8 +- Reqnroll/StepInfo.cs | 4 +- Reqnroll/TestRunner.cs | 20 ++--- .../CucumberCompatibilityTestBase.cs | 10 +-- .../CucumberCompatibilityTests.cs | 53 ++++++++++++ 13 files changed, 250 insertions(+), 45 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/PickleJar.cs diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 307b7d8a6..c3f215e05 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using Gherkin.Ast; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Parser; @@ -11,6 +12,8 @@ namespace Reqnroll.Generator.Generation { public class ScenarioPartHelper { + private const string BRINE = "PickleJar"; + private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly CodeDomHelper _codeDomHelper; private int _tableCounter; @@ -92,7 +95,8 @@ public void GenerateStep(TestClassGenerationContext generationContext, List())); return; } // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object @@ -245,17 +257,20 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); - // Create a FeatureLevelCucumberMessages object and add it to featureInfo var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), new CodePrimitiveExpression(featureSourceMessageString), new CodePrimitiveExpression(featureGherkinDocumentMessageString), new CodePrimitiveExpression(featurePickleMessagesString)); + CucumberMessagesInitializeMethod.Statements.Add( new CodeAssignStatement( new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), featureLevelCucumberMessagesExpression)); + ///At runtime, pull the PickleStepIds from the pickleJar for the given runtime pickleID; partition them out by Background, RuleBackground, and Scenario steps + ///keep an index of which step is being generated and use that to index in to the PickleStepIds at runtime ... eg "stepIds[codeprimitive(stepIndex)]" + // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( null, diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 252c6ca34..8e8136eed 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -5,6 +5,8 @@ using System.Linq; using Gherkin.Ast; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; +using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; @@ -15,6 +17,9 @@ namespace Reqnroll.Generator.Generation { public class UnitTestMethodGenerator { + private const string PICKLEJAR = "PICKLEJAR"; + private const string PICKLEID_PARAMETER_NAME = "@pickleId"; + private const string PICKLEID_VARIABLE_NAME = "m_pickleId"; private const string IGNORE_TAG = "@Ignore"; private const string TESTRUNNER_FIELD = "testRunner"; private readonly CodeDomHelper _codeDomHelper; @@ -22,6 +27,8 @@ public class UnitTestMethodGenerator private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; + private PickleJar _pickleJar; + public UnitTestMethodGenerator(IUnitTestGeneratorProvider unitTestGeneratorProvider, IDecoratorRegistry decoratorRegistry, CodeDomHelper codeDomHelper, ScenarioPartHelper scenarioPartHelper, ReqnrollConfiguration reqnrollConfiguration) { @@ -39,13 +46,14 @@ IEnumerable GetScenarioDefinitionsOfRule(IEnume .Where(child => child is not Background) .Select(sd => new ScenarioDefinitionInFeatureFile(sd, feature, rule)); - return + return GetScenarioDefinitionsOfRule(feature.Children, null) .Concat(feature.Children.OfType().SelectMany(rule => GetScenarioDefinitionsOfRule(rule.Children, rule))); } public void CreateUnitTests(ReqnrollFeature feature, TestClassGenerationContext generationContext) { + _pickleJar = generationContext.CustomData[PICKLEJAR] as PickleJar; foreach (var scenarioDefinition in GetScenarioDefinitions(feature)) { CreateUnitTest(generationContext, scenarioDefinition); @@ -66,6 +74,7 @@ private void CreateUnitTest(TestClassGenerationContext generationContext, Scenar else { GenerateTest(generationContext, scenarioDefinitionInFeatureFile); + _pickleJar.NextPickle(); } } @@ -87,7 +96,7 @@ private void GenerateScenarioOutlineTest(TestClassGenerationContext generationCo GenerateScenarioOutlineExamplesAsIndividualMethods(scenarioOutline, generationContext, scenarioOutlineTestMethod, paramToIdentifier); } - GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier); + GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier, true); } private void GenerateTest(TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile) @@ -129,7 +138,9 @@ private void GenerateTestBody( TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile, CodeMemberMethod testMethod, - CodeExpression additionalTagsExpression = null, ParameterSubstitution paramToIdentifier = null) + CodeExpression additionalTagsExpression = null, + ParameterSubstitution paramToIdentifier = null, + bool pickleIdIncludedInParameters = false) { var scenarioDefinition = scenarioDefinitionInFeatureFile.ScenarioDefinition; var feature = scenarioDefinitionInFeatureFile.Feature; @@ -190,6 +201,9 @@ private void GenerateTestBody( AddVariableForArguments(testMethod, paramToIdentifier); + AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, generationContext.CustomData[PICKLEJAR] as PickleJar); + AddVariableForPickleStepSequence(testMethod); + testMethod.Statements.Add( new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(ScenarioInfo)), "scenarioInfo", new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(ScenarioInfo)), @@ -197,7 +211,8 @@ private void GenerateTestBody( new CodePrimitiveExpression(scenarioDefinition.Description), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), - inheritedTagsExpression))); + inheritedTagsExpression, + new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -206,6 +221,41 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } + private void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + { + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), PICKLEID_VARIABLE_NAME, + pickleIdIncludedInParameters ? + new CodeVariableReferenceExpression(PICKLEID_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleJar.CurrentPickleId)); + testMethod.Statements.Add(pickleIdVariable); + } + + private void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) + { + // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME, + // Right side of the assignment (property access chain) + + new CodeMethodInvokeExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(TESTRUNNER_FIELD), + "FeatureContext" + ), + "FeatureInfo" + ), + "FeatureCucumberMessages" + ), + "PickleJar" + ), + "PickleStepSequenceFor", + new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)) + ); + + testMethod.Statements.Add(pickleStepSequence); + } private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { var tagVariable = new CodeVariableDeclarationStatement(typeof(string[]), GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME, tagsExpression); @@ -272,6 +322,9 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex foreach (var scenarioStep in scenario.Steps) { _scenarioPartHelper.GenerateStep(generationContext, statementsWhenScenarioIsExecuted, scenarioStep, paramToIdentifier); + statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(new CodeMethodInvokeExpression( + new CodeVariableReferenceExpression(PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME), + "NextStep"))); } var tagsOfScenarioVariableReferenceExpression = new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME); @@ -322,7 +375,7 @@ internal void GenerateScenarioCleanupMethodCall(TestClassGenerationContext gener testMethod.Statements.Add(expression); } - + private CodeMethodInvokeExpression CreateTestRunnerSkipScenarioCall() { return new CodeMethodInvokeExpression( @@ -362,7 +415,9 @@ private void GenerateScenarioOutlineExamplesAsIndividualMethods( foreach (var example in exampleSet.TableBody.Select((r, i) => new { Row = r, Index = i })) { var variantName = useFirstColumnAsName ? example.Row.Cells.First().Value : string.Format("Variant {0}", example.Index); - GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, exampleSet.Tags.ToArray(), variantName); + var currentPickleId = _pickleJar.CurrentPickleId; + GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, currentPickleId, exampleSet.Tags.ToArray(), variantName); + _pickleJar.NextPickle(); } exampleSetIndex++; @@ -377,8 +432,11 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex { foreach (var row in examples.TableBody) { - var arguments = row.Cells.Select(c => c.Value); + var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleJar.CurrentPickleId }); + _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); + + _pickleJar.NextPickle(); } } } @@ -434,12 +492,12 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont testMethod.Name = string.Format(GeneratorConstants.TEST_NAME_FORMAT, scenarioOutline.Name.ToIdentifier()); _codeDomHelper.MarkCodeMemberMethodAsAsync(testMethod); - + foreach (var pair in paramToIdentifier) { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), @"@pickleId")); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } @@ -452,16 +510,17 @@ private void GenerateScenarioOutlineTestVariant( string exampleSetTitle, string exampleSetIdentifier, Gherkin.Ast.TableRow row, + string pickleId, Tag[] exampleSetTags, string variantName) { var testMethod = CreateTestMethod(generationContext, scenarioOutline, exampleSetTags, variantName, exampleSetIdentifier); - + //call test implementation with the params var argumentExpressions = row.Cells.Select(paramCell => new CodePrimitiveExpression(paramCell.Value)).Cast().ToList(); - + argumentExpressions.Add(new CodePrimitiveExpression(pickleId)); argumentExpressions.Add(_scenarioPartHelper.GetStringArrayExpression(exampleSetTags)); - + var statements = new List(); using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, scenarioOutline.Location)) @@ -475,7 +534,7 @@ private void GenerateScenarioOutlineTestVariant( statements.Add(new CodeExpressionStatement(callTestMethodExpression)); } - + testMethod.Statements.AddRange(statements.ToArray()); //_linePragmaHandler.AddLineDirectiveHidden(testMethod.Statements); diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs index 5a48bc4b3..07a15b569 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -1,4 +1,5 @@ -using System; +using Reqnroll.CucumberMesssages; +using System; using System.Collections.Generic; using System.Text; @@ -11,10 +12,12 @@ public FeatureLevelCucumberMessages(string source, string gkerkinDocument, strin Source = source; GherkinDocument = gkerkinDocument; Pickles = pickles; + PickleJar = new PickleJar(pickles); } - public string Source { get; set; } - public string GherkinDocument { get; set; } - public string Pickles { get; set; } + public string Source { get; } + public string GherkinDocument { get; } + public string Pickles { get; } + public PickleJar PickleJar { get; } } } diff --git a/Reqnroll/CucumberMesssages/PickleJar.cs b/Reqnroll/CucumberMesssages/PickleJar.cs new file mode 100644 index 000000000..f8bd2e5a9 --- /dev/null +++ b/Reqnroll/CucumberMesssages/PickleJar.cs @@ -0,0 +1,65 @@ +using Gherkin.CucumberMessages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public class PickleJar + { + public const string PICKLEJAR_VARIABLE_NAME = "m_pickleJar"; + + public int _PickleCounter = 0; + public IEnumerable Pickles { get; set; } + + //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) + //{ } + public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } + public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } + + public PickleJar(IEnumerable pickles, int pickleCounter, int pickleStepCounter) + { + Pickles = pickles; + _PickleCounter = pickleCounter; + } + + public string CurrentPickleId { get { return Pickles.ElementAt(_PickleCounter).Id; } } + public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } + + public IEnumerable PickleStepIdsFor(string pickleId) + { + return Pickles.Where(p => p.Id == pickleId).SelectMany(p => p.Steps.Select(s => s.Id)).ToArray(); + } + public PickleStepSequence PickleStepSequenceFor(string pickleId) + { + return new PickleStepSequence(Pickles.Where(p => p.Id == pickleId).First()); + } + + public void NextPickle() + { + _PickleCounter++; + } + } + + + public class PickleStepSequence + { + public static string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; + public Pickle CurrentPickle { get; } + + private int _PickleStepCounter; + + public PickleStepSequence(Gherkin.CucumberMessages.Types.Pickle pickle) + { + CurrentPickle = pickle; + _PickleStepCounter = 0; + } + public void NextStep() + { + _PickleStepCounter++; + } + public string CurrentPickleStepId { get { return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; } } + + } +} diff --git a/Reqnroll/ITestRunner.cs b/Reqnroll/ITestRunner.cs index e949a177f..6c7537e06 100644 --- a/Reqnroll/ITestRunner.cs +++ b/Reqnroll/ITestRunner.cs @@ -30,11 +30,11 @@ public interface ITestRunner void SkipScenario(); - Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); + Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); void Pending(); } diff --git a/Reqnroll/Infrastructure/ITestExecutionEngine.cs b/Reqnroll/Infrastructure/ITestExecutionEngine.cs index fdb15e429..e3686428c 100644 --- a/Reqnroll/Infrastructure/ITestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/ITestExecutionEngine.cs @@ -22,7 +22,7 @@ public interface ITestExecutionEngine void OnScenarioSkipped(); - Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg); + Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId); void Pending(); } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index aca42938a..aeb94c22f 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -622,12 +622,12 @@ private async Task ConvertArg(object value, IBindingType typeToConvertTo #region Given-When-Then - public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg) + public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId = null) { StepDefinitionType stepDefinitionType = stepDefinitionKeyword == StepDefinitionKeyword.And || stepDefinitionKeyword == StepDefinitionKeyword.But ? GetCurrentBindingType() : (StepDefinitionType) stepDefinitionKeyword; - _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg)); + _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); try diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 4abd241a0..0247f7e54 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -36,13 +36,19 @@ public class ScenarioInfo /// public string Description { get; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null) + /// + /// The PickleId of the Scenario when exported as a Cucumber Message "pickle". + /// + public string PickleId { get; } + + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null) { Title = title; Description = description; Tags = tags ?? Array.Empty(); Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); + PickleId = pickleId; } } } \ No newline at end of file diff --git a/Reqnroll/StepInfo.cs b/Reqnroll/StepInfo.cs index 3cbb3bc87..75b435e52 100644 --- a/Reqnroll/StepInfo.cs +++ b/Reqnroll/StepInfo.cs @@ -12,13 +12,15 @@ public class StepInfo public BindingMatch BindingMatch { get; set; } public StepInstance StepInstance { get; set; } + public string PickleStepId { get; } - public StepInfo(StepDefinitionType stepDefinitionType, string text, Table table, string multilineText) + public StepInfo(StepDefinitionType stepDefinitionType, string text, Table table, string multilineText, string pickleStepId = null) { StepDefinitionType = stepDefinitionType; Text = text; Table = table; MultilineText = multilineText; + PickleStepId = pickleStepId; } diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index 82c5bff7a..dbbe282dc 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -73,29 +73,29 @@ public async Task OnTestRunEndAsync() await _executionEngine.OnTestRunEndAsync(); } - public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg, pickleStepId); } public void Pending() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 0fddf4667..4f7fa2d1c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -11,6 +11,10 @@ namespace CucumberMessages.CompatibilityTests { public class CucumberCompatibilityTestBase : SystemTestBase { + protected override void TestCleanup() + { + // TEMPORARY: this is in place so that SystemTestBase.TestCleanup does not run (which deletes the generated code) + } protected void AddCucumberMessagePlugIn() { @@ -47,12 +51,6 @@ protected IEnumerable GetTestBinaryFileNames(string scenarioName, string .Select(rn => rn.Substring(prefixToRemove.Length)); } - protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix, Assembly? assembly) - { - var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); - return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); - } - protected void CucumberMessagesAddConfigurationFile(string configFileName) { var configFileContent = File.ReadAllText(configFileName); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 3505f4d02..d9cb9cece 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -9,6 +9,8 @@ using System.Text.Json; using System.ComponentModel; using Reqnroll.TestProjectGenerator; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Reqnroll.TestProjectGenerator.Driver; namespace CucumberMessages.CompatibilityTests { @@ -54,6 +56,57 @@ public void SmokeTest() ShouldAllScenariosPass(); } + [TestMethod] + public void SmokeOutlineTest() + { + AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] + public void SmokeOutlineTestAsMethods() + { + var _configurationFileDriver = GetServiceSafe(); + _configurationFileDriver.SetIsRowTestsAllowed(false); + + AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test As Methods + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] public void CucumberMessagesInteropWithExternalData() { From 147b34573f864bd948eb97f4a8da4f704242fcf1 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:11:51 -0500 Subject: [PATCH 065/218] Refactored scenario tracking into TestCase tracking --- .../FileSinkPlugin.cs | 59 +++- ...mberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- .../Generation/ScenarioPartHelper.cs | 2 +- .../Generation/UnitTestFeatureGenerator.cs | 1 - .../Generation/UnitTestMethodGenerator.cs | 2 +- .../AttachmentAddedEventWrapper.cs | 0 .../CucumberMessagEnumConverter.cs | 0 .../CucumberMessageBroker.cs | 16 +- .../CucumberMessageExtensions.cs | 0 .../CucumberMessageFactory.cs | 40 +-- .../CucumberMessagePublisher.cs | 150 ++++----- .../CucumberMessageTransformer.cs | 0 .../CucumberMessageVisitor.cs | 0 .../CucumberMessage_TraversalVisitorBase.cs | 0 .../FeatureLevelCucumberMessages.cs | 2 +- Reqnroll/CucumberMessages/FeatureTracker.cs | 104 +++++++ .../FileExtensionToMIMETypeMap.cs | 0 .../HookStepProcessor.cs | 13 +- .../ICucumberMessagePublisher.cs | 0 .../ICucumberMessageSink.cs | 0 .../ICucumberMessageVisitor.cs | 0 .../IStepProcessor.cs | 0 .../IdGeneratorFactory.cs | 0 .../NdjsonSerializer.cs | 0 .../OutputAddedEventWrapper.cs | 0 .../PickleJar.cs | 2 +- .../ReqnrollCucumberMessage.cs | 0 .../StepProcessorBase.cs | 8 +- .../TestCaseCucumberMessageTracker.cs | 291 ++++++++++++++++++ .../TestStepProcessor.cs} | 22 +- .../FeatureEventProcessor.cs | 259 ---------------- .../ScenarioEventProcessor.cs | 205 ------------ Reqnroll/Events/ExecutionEvent.cs | 19 +- Reqnroll/FeatureInfo.cs | 3 +- .../Infrastructure/ReqnrollOutputHelper.cs | 12 +- Reqnroll/Reqnroll.csproj | 2 +- .../CucumberCompatibilityTests.cs | 8 +- ...CucumberMessages.CompatibilityTests.csproj | 2 +- 38 files changed, 567 insertions(+), 657 deletions(-) rename Reqnroll/{CucumberMesssages => CucumberMessages}/AttachmentAddedEventWrapper.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessagEnumConverter.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageBroker.cs (76%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageExtensions.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageFactory.cs (86%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessagePublisher.cs (50%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageTransformer.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageVisitor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessage_TraversalVisitorBase.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/FeatureLevelCucumberMessages.cs (94%) create mode 100644 Reqnroll/CucumberMessages/FeatureTracker.cs rename Reqnroll/{CucumberMesssages => CucumberMessages}/FileExtensionToMIMETypeMap.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/HookStepProcessor.cs (58%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessagePublisher.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessageSink.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessageVisitor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/IStepProcessor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/IdGeneratorFactory.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/NdjsonSerializer.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/OutputAddedEventWrapper.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/PickleJar.cs (98%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ReqnrollCucumberMessage.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/StepProcessorBase.cs (56%) create mode 100644 Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs rename Reqnroll/{CucumberMesssages/ScenarioStepProcessor.cs => CucumberMessages/TestStepProcessor.cs} (77%) delete mode 100644 Reqnroll/CucumberMesssages/FeatureEventProcessor.cs delete mode 100644 Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index f2602a1d4..d95de1804 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -10,6 +10,8 @@ using Reqnroll.Events; using System.Collections.Concurrent; using System.Text.Json; +using Reqnroll.Tracing; +using Reqnroll.BoDi; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] @@ -28,25 +30,24 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private readonly ConcurrentDictionary fileStreams = new(); private FileSinkConfiguration? configuration; private string baseDirectory = ""; + private Lazy? traceListener; + private ITraceListener? trace => traceListener?.Value; + private IObjectContainer? objectContainer; public FileSinkPlugin() { + traceListener = new Lazy(() => objectContainer!.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) - return; - - baseDirectory = ProcessConfiguration(configuration); - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + objectContainer = args.ObjectContainer; testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; @@ -61,23 +62,26 @@ private string ProcessConfiguration(FileSinkConfiguration configuration) var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); if (!Directory.Exists(basePath)) { - lock(_lock) + lock (_lock) { if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath); } } - + trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); return basePath; } else { - return Assembly.GetExecutingAssembly().Location; + var location = Assembly.GetExecutingAssembly().Location; + trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); + return location; } } private void CloseFileSink(TestRunFinishedEvent @event) { + trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; @@ -85,11 +89,23 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + return; + } + + baseDirectory = ProcessConfiguration(configuration); + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } public void Publish(ReqnrollCucumberMessage message) { + var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; + trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -102,16 +118,17 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } } - private bool disposedValue; private string Serialize(Envelope message) { @@ -119,26 +136,34 @@ private string Serialize(Envelope message) } private async Task Write(string featureName, string cucumberMessage) { - - if (!fileStreams.ContainsKey(featureName)) + try { - lock (_lock) + if (!fileStreams.ContainsKey(featureName)) { - if (!fileStreams.ContainsKey(featureName)) + lock (_lock) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } } } + trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); + } + catch (System.Exception ex) + { + trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); } - await fileStreams[featureName].WriteLineAsync(cucumberMessage); } private void CloseFeatureStream(string featureName) { + trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); fileStreams[featureName].Close(); fileStreams.TryRemove(featureName, out var _); } - + private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 4df9424dd..3b49d3ad1 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,7 +10,7 @@ - + diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index c3f215e05..3dd6839a8 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -4,7 +4,7 @@ using System.Text.RegularExpressions; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Generator.CodeDom; using Reqnroll.Parser; diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 51fec3125..1142bba12 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -7,7 +7,6 @@ using Gherkin.CucumberMessages; using Reqnroll.Configuration; using Reqnroll.CucumberMessages; -using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 8e8136eed..0d8794649 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs similarity index 100% rename from Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs similarity index 76% rename from Reqnroll/CucumberMesssages/CucumberMessageBroker.cs rename to Reqnroll/CucumberMessages/CucumberMessageBroker.cs index 485667467..7b4804104 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs @@ -16,7 +16,7 @@ public interface ICucumberMessageBroker { bool Enabled { get; } void Complete(string cucumberMessageSource); - void Publish(ReqnrollCucumberMessage message); + void Publish(ReqnrollCucumberMessage featureMessages); } public class CucumberMessageBroker : ICucumberMessageBroker @@ -25,22 +25,20 @@ public class CucumberMessageBroker : ICucumberMessageBroker public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; - //private ITraceListener _traceListener; + private Lazy> RegisteredSinks; public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; + RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); } public void Publish(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); - //TODO: find a way to populate this list a single time - var registeredSinks = _objectContainer.ResolveAll().ToList(); - - foreach (var sink in registeredSinks) + foreach (var sink in RegisteredSinks.Value) { - _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); + _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}: {message.Envelope.Content()}"); sink.Publish(message); } @@ -49,8 +47,6 @@ public void Publish(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public void Complete(string cucumberMessageSource) { - var registeredSinks = _objectContainer.ResolveAll().ToList(); - var _traceListener = _objectContainer.Resolve(); var completionMessage = new ReqnrollCucumberMessage @@ -58,7 +54,7 @@ public void Complete(string cucumberMessageSource) CucumberMessageSource = cucumberMessageSource }; - foreach (var sink in registeredSinks) + foreach (var sink in RegisteredSinks.Value) { _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMessages/CucumberMessageExtensions.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs rename to Reqnroll/CucumberMessages/CucumberMessageExtensions.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/CucumberMessageFactory.cs similarity index 86% rename from Reqnroll/CucumberMesssages/CucumberMessageFactory.cs rename to Reqnroll/CucumberMessages/CucumberMessageFactory.cs index 4afbb55ab..b0a1a33ec 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageFactory.cs @@ -19,25 +19,25 @@ namespace Reqnroll.CucumberMessages { internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(FeatureEventProcessor featureState, FeatureStartedEvent featureStartedEvent) + public static TestRunStarted ToTestRunStarted(FeatureStartedEvent featureStartedEvent) { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } - public static TestRunFinished ToTestRunFinished(FeatureEventProcessor featureState, FeatureFinishedEvent featureFinishedEvent) + public static TestRunFinished ToTestRunFinished(bool testRunStatus, FeatureFinishedEvent testRunFinishedEvent) { - return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); + return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(testRunFinishedEvent.Timestamp), null); } - internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); - foreach (var stepState in scenarioState.Steps) + foreach (var stepState in testCaseTracker.Steps) { switch (stepState) { - case ScenarioStepProcessor _: - var testStep = CucumberMessageFactory.ToPickleTestStep(scenarioState, stepState as ScenarioStepProcessor); + case TestStepProcessor _: + var testStep = CucumberMessageFactory.ToPickleTestStep(testCaseTracker, stepState as TestStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: @@ -50,19 +50,19 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar } var testCase = new TestCase ( - scenarioState.TestCaseID, - scenarioState.PickleID, + testCaseTracker.TestCaseId, + testCaseTracker.PickleId, testSteps ); return testCase; } - internal static TestCaseStarted ToTestCaseStarted(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCaseStarted ToTestCaseStarted(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { - return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); + return new TestCaseStarted(0, testCaseTracker.TestCaseStartedId, testCaseTracker.TestCaseId, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); } - internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + internal static TestCaseFinished ToTestCaseFinished(TestCaseCucumberMessageTracker testCaseTracker, ScenarioFinishedEvent scenarioFinishedEvent) { - return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); + return new TestCaseFinished(testCaseTracker.TestCaseStartedId, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { @@ -120,7 +120,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) + internal static TestStep ToPickleTestStep(TestCaseCucumberMessageTracker tracker, TestStepProcessor stepState) { bool bound = stepState.Bound; bool ambiguous = stepState.Ambiguous; @@ -150,7 +150,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); } - internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(TestStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -158,7 +158,7 @@ internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepStat Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(ScenarioStepProcessor stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(TestStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, @@ -185,7 +185,7 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) { // find the Hook message at the Feature level var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); - var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; + var hookId = hookStepState.ParentTestCase.StepDefinitionsByPattern[hookCacheKey]; return new TestStep( hookId, @@ -204,7 +204,7 @@ internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepPr return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } - internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, AttachmentAddedEventWrapper attachmentAddedEventWrapper) { return new Attachment( Base64EncodeFile(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), @@ -216,7 +216,7 @@ internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProc attachmentAddedEventWrapper.TestCaseStepID, null); } - internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, OutputAddedEventWrapper outputAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, OutputAddedEventWrapper outputAddedEventWrapper) { return new Attachment( outputAddedEventWrapper.OutputAddedEvent.Text, @@ -340,7 +340,7 @@ public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding st public static string CanonicalizeHookBinding(IHookBinding hookBinding) { string signature = GenerateSignature(hookBinding); - return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + return $"{hookBinding.Method.Type.FullName}.{hookBinding.Method.Name}({signature})"; } private static string GenerateSignature(IBinding stepDefinition) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs similarity index 50% rename from Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 7bd7a086f..4ea98657f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -5,6 +5,9 @@ using Reqnroll.UnitTestProvider; using System.Collections.Concurrent; using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMessages { @@ -12,7 +15,8 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private ConcurrentDictionary featureProcessorsByFeatureName = new(); + private ConcurrentDictionary StartedFeatures = new(); + private ConcurrentDictionary testCaseTrackersById = new(); bool Enabled = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) @@ -46,6 +50,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { + var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; @@ -53,81 +58,75 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) // if true, then it checks with the broker to confirm that a listener/sink has been registered Enabled = broker.Enabled; if (!Enabled) - return; - - var featureEnabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - - var featureProc = new FeatureEventProcessor { - Name = featureName, - Enabled = featureEnabled - }; + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: Broker is disabled for {featureName}."); + return; + } - // todo: need a lock around this - if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) + if (StartedFeatures.ContainsKey(featureName)) { - // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureProcessorsByFeatureName[featureName]; - featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well - - // None of the rest of this method should be executed + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName} already started"); return; } - var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); - - if (!featureEnabled) - return; - - ProcessEvent(featureStartedEvent, featureName); + var ft = new FeatureTracker(featureStartedEvent); + if (StartedFeatures.TryAdd(featureName, ft)) + { + foreach (var msg in ft.StaticMessages) + { + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + } } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { + var traceListener = objectContainer.Resolve(); if (!Enabled) + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Broker is disabled for {featureFinishedEvent.FeatureContext.FeatureInfo.Title}."); return; - - + } var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureProcessor = featureProcessorsByFeatureName[featureName]; + var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); - lock (featureProcessor) + // IF all TestCaseCucumberMessageTrackers are done, then send the messages to the CucumberMessageBroker + if (featureTestCases.All(tc => tc.Finished)) { - // Remove the worker thread marker for this thread - featureProcessor.workerThreadMarkers.TryPop(out int result); - - // Check if there are other threads still working on this feature - if (featureProcessor.workerThreadMarkers.TryPeek(out result)) - { - // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet - return; - } - featureProcessor.Finished = true; + var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); + var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + broker.Complete(featureName); } - - - if (!featureProcessor.Enabled) - return; - - ProcessEvent(featureFinishedEvent, featureName); - - foreach (var message in featureProcessor.Messages) + else { - broker.Publish(message); + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Error: {featureTestCases.Count(tc => !tc.Finished)} test cases not marked as finished for Feature {featureName}. TestRunFinished event will not be sent."); } - broker.Complete(featureName); + // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? + } private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { if (!Enabled) return; - - + var traceListener = objectContainer.Resolve(); var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(scenarioStartedEvent, featureName); + var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); + testCaseTrackersById.TryAdd(id, tccmt); + tccmt.ProcessEvent(scenarioStartedEvent); + } + else + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker not available"); + throw new ApplicationException("FeatureTracker not available"); + } } private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) @@ -135,9 +134,13 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished if (!Enabled) return; + var tccmt = testCaseTrackersById[scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(scenarioFinishedEvent); - var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(scenarioFinishedEvent, featureName); + foreach (var msg in tccmt.TestCaseCucumberMessages()) + { + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + } } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) @@ -145,9 +148,8 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) if (!Enabled) return; - - var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(stepStartedEvent, featureName); + var tccmt = testCaseTrackersById[stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(stepStartedEvent); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) @@ -155,9 +157,8 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (!Enabled) return; - - var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(stepFinishedEvent, featureName); + var tccmt = testCaseTrackersById[stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(stepFinishedEvent); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) @@ -165,19 +166,17 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - - var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookBindingStartedEvent, featureName); + var tccmt = testCaseTrackersById[hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(hookBindingStartedEvent); } - private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { if (!Enabled) return; - - var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookBindingEvent, featureName); + var tccmt = testCaseTrackersById[hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(hookBindingFinishedEvent); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) @@ -185,9 +184,8 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (!Enabled) return; - - var featureName = attachmentAddedEvent.FeatureName; - ProcessEvent(attachmentAddedEvent, featureName); + var tccmt = testCaseTrackersById[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(attachmentAddedEvent); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) @@ -195,22 +193,8 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (!Enabled) return; - - ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); - } - - - private void ProcessEvent(ExecutionEvent anEvent, string featureName) - { - if (!Enabled) - return; - - - var featureProcessor = featureProcessorsByFeatureName[featureName]; - if (!featureProcessor.Enabled) - return; - - featureProcessor.ProcessEvent(anEvent); + var tccmt = testCaseTrackersById[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(outputAddedEvent); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/CucumberMessageTransformer.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs rename to Reqnroll/CucumberMessages/CucumberMessageTransformer.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/CucumberMessageVisitor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/CucumberMessageVisitor.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs rename to Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs similarity index 94% rename from Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs rename to Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs index 07a15b569..12fb81ac1 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs @@ -1,4 +1,4 @@ -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using System; using System.Collections.Generic; using System.Text; diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs new file mode 100644 index 000000000..0b6ac7f37 --- /dev/null +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -0,0 +1,104 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Reqnroll.CucumberMessages +{ + public class FeatureTracker + { + internal IEnumerable StaticMessages; + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + public IIdGenerator IDGenerator { get; set; } + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + internal Dictionary StepDefinitionsByPattern = new(); + public string FeatureName { get; set; } + public bool Enabled { get; private set; } + + public FeatureTracker(FeatureStartedEvent featureStartedEvent) + { + FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + Enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles == null ? false : true; + PreProcessEvent(featureStartedEvent); + } + internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) + { + // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow + StaticMessages = GenerateStaticMessages(featureStartedEvent).ToList(); + } + private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) + { + yield return CucumberMessageFactory.ToMeta(featureStartedEvent); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + yield return Envelope.Create(messageSource); + + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + yield return Envelope.Create(gherkinDocument); + + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + string lastID = ExtractLastID(pickles); + IDGenerator = IdGeneratorFactory.Create(lastID); + + foreach (var pickle in pickles) + { + yield return Envelope.Create(pickle); + } + + var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) + { + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) + { + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); + yield return Envelope.Create(undefinedParameterType); + } + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + + yield return Envelope.Create(stepDefinition); + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + StepDefinitionsByPattern.Add(hookId, hook.Id); + yield return Envelope.Create(hook); + } + + yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(featureStartedEvent)); + } + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs similarity index 100% rename from Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs rename to Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMessages/HookStepProcessor.cs similarity index 58% rename from Reqnroll/CucumberMesssages/HookStepProcessor.cs rename to Reqnroll/CucumberMessages/HookStepProcessor.cs index 0754bde6f..80a7fc77d 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMessages/HookStepProcessor.cs @@ -9,24 +9,21 @@ public class HookStepProcessor : StepProcessorBase { public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } - public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public HookStepProcessor(TestCaseCucumberMessageTracker tracker) : base(tracker) { } - public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEvent) + public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { - TestStepID = parentScenario.IdGenerator.GetNewId(); - HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(stepFinishedEvent.HookBinding); - return Enumerable.Empty(); + TestStepID = ParentTestCase.IDGenerator.GetNewId(); + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); } - public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + public void ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; Exception = hookFinishedEvent.HookException; Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; - - return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/ICucumberMessageSink.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessageSink.cs rename to Reqnroll/CucumberMessages/ICucumberMessageSink.cs diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMessages/IStepProcessor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/IStepProcessor.cs rename to Reqnroll/CucumberMessages/IStepProcessor.cs diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/IdGeneratorFactory.cs similarity index 100% rename from Reqnroll/CucumberMesssages/IdGeneratorFactory.cs rename to Reqnroll/CucumberMessages/IdGeneratorFactory.cs diff --git a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/NdjsonSerializer.cs similarity index 100% rename from Reqnroll/CucumberMesssages/NdjsonSerializer.cs rename to Reqnroll/CucumberMessages/NdjsonSerializer.cs diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs similarity index 100% rename from Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs diff --git a/Reqnroll/CucumberMesssages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs similarity index 98% rename from Reqnroll/CucumberMesssages/PickleJar.cs rename to Reqnroll/CucumberMessages/PickleJar.cs index f8bd2e5a9..df6b70b02 100644 --- a/Reqnroll/CucumberMesssages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class PickleJar { diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs rename to Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMessages/StepProcessorBase.cs similarity index 56% rename from Reqnroll/CucumberMesssages/StepProcessorBase.cs rename to Reqnroll/CucumberMessages/StepProcessorBase.cs index 3638404b0..c1d3ec601 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMessages/StepProcessorBase.cs @@ -5,16 +5,16 @@ namespace Reqnroll.CucumberMessages public class StepProcessorBase : IStepProcessor { public string TestStepID { get; set; } - public string TestCaseStartedID => parentScenario.TestCaseStartedID; + public string TestCaseStartedID => ParentTestCase.TestCaseStartedId; public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } public Exception Exception { get; set; } - public ScenarioEventProcessor parentScenario; + public TestCaseCucumberMessageTracker ParentTestCase; - public StepProcessorBase(ScenarioEventProcessor parentScenario) + public StepProcessorBase(TestCaseCucumberMessageTracker parentScenario) { - this.parentScenario = parentScenario; + ParentTestCase = parentScenario; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs new file mode 100644 index 000000000..faeb018d5 --- /dev/null +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -0,0 +1,291 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; + +namespace Reqnroll.CucumberMessages +{ + /// + /// This class is used to track the execution of Test Cases + /// There will be one instance of this class per gherkin Pickle/TestCase. It will track info from both Feature-level and Scenario-level Execution Events for a single Test Case + /// + public class TestCaseCucumberMessageTracker + { + public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) + { + FeatureName = featureTracker.FeatureName; + Enabled = featureTracker.Enabled; + IDGenerator = featureTracker.IDGenerator; + StepDefinitionsByPattern = featureTracker.StepDefinitionsByPattern; + } + + // Feature FeatureInfo and Pickle ID make up a unique identifier for tracking execution of Test Cases + public string FeatureName { get; set; } + public string PickleId { get; set; } = String.Empty; + public string TestCaseTrackerId { get { return FeatureName + PickleId; } } + public string TestCaseId { get; set; } + public string TestCaseStartedId { get; private set; } + + // When this class is first created (on FeatureStarted), it will not yet be assigned a Scenario/Pickle; + // When a Scenario is started, the Publisher will assign the Scenario to the first UnAssigned TestCaseCucumberMessageTracker it finds + // This property will indicate that state + public bool IsUnassigned { get { return PickleId == String.Empty; } } + + public bool Enabled { get; set; } //This will be false if the feature could not be pickled + + public bool Finished { get; set; } + + + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + public IIdGenerator IDGenerator { get; set; } + + public Dictionary StepsById { get; private set; } = new(); + public Dictionary StepsByEvent { get; private set; } = new(); + public List Steps + { + get + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); + } + } + public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } + + + // This queue holds ExecutionEvents that will be processed in stage 2 + private Queue _events = new(); + + internal IEnumerable StaticMessages; + + // During Post-Processing, this is used to track the most recent TestStepStarted event so that Attachments and Output events can be associated with it + private TestStepStarted mostRecentTestStepStarted; + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + internal Dictionary StepDefinitionsByPattern = new(); + + // Processing of events is handled in two stages. + // Stage 1: As events are recieved, critical information needed right away is extracted and stored in the TestCaseCucumberMessageTracker + // The event is then stored in a queue for processing in stage 2 + // Stage 2: When TestRunFinished is recieved, the messages are processed to generate Cucumber Messages and then sent in a single batch to the broker + internal void ProcessEvent(ExecutionEvent anEvent) + { + _events.Enqueue(anEvent); + if (Enabled) InvokePreProcessEvent(anEvent); + } + private void InvokePreProcessEvent(ExecutionEvent anEvent) + { + switch (anEvent) + { + case FeatureStartedEvent featureStartedEvent: + PreProcessEvent(featureStartedEvent); + break; + case FeatureFinishedEvent featureFinishedEvent: + PreProcessEvent(featureFinishedEvent); + break; + case ScenarioStartedEvent scenarioStartedEvent: + PreProcessEvent(scenarioStartedEvent); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + PreProcessEvent(scenarioFinishedEvent); + break; + case StepStartedEvent stepStartedEvent: + PreProcessEvent(stepStartedEvent); + break; + case StepFinishedEvent stepFinishedEvent: + PreProcessEvent(stepFinishedEvent); + break; + case HookBindingStartedEvent hookBindingStartedEvent: + PreProcessEvent(hookBindingStartedEvent); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + PreProcessEvent(hookBindingFinishedEvent); + break; + case AttachmentAddedEvent attachmentAddedEvent: + PreProcessEvent(attachmentAddedEvent); + break; + case OutputAddedEvent outputAddedEvent: + PreProcessEvent(outputAddedEvent); + break; + default: + throw new NotImplementedException($"Event type {anEvent.GetType().Name} is not supported."); + } + } + + public IEnumerable TestCaseCucumberMessages() + { + // Stage 2 + return _events.Select(e => InvokePostProcessEvent(e)).SelectMany(x => x); + } + + private IEnumerable InvokePostProcessEvent(ExecutionEvent anEvent) + { + return anEvent switch + { + FeatureStartedEvent featureStartedEvent => PostProcessEvent(featureStartedEvent), + FeatureFinishedEvent featureFinishedEvent => PostProcessEvent(featureFinishedEvent), + ScenarioStartedEvent scenarioStartedEvent => PostProcessEvent(scenarioStartedEvent), + ScenarioFinishedEvent scenarioFinishedEvent => PostProcessEvent(scenarioFinishedEvent), + StepStartedEvent stepStartedEvent => PostProcessEvent(stepStartedEvent), + StepFinishedEvent stepFinishedEvent => PostProcessEvent(stepFinishedEvent), + HookBindingStartedEvent hookBindingStartedEvent => PostProcessEvent(hookBindingStartedEvent), + HookBindingFinishedEvent hookBindingFinishedEvent => PostProcessEvent(hookBindingFinishedEvent), + AttachmentAddedEvent attachmentAddedEvent => PostProcessEvent(attachmentAddedEvent), + AttachmentAddedEventWrapper attachmentAddedEventWrapper => PostProcessEvent(attachmentAddedEventWrapper), + OutputAddedEvent outputAddedEvent => PostProcessEvent(outputAddedEvent), + OutputAddedEventWrapper outputAddedEventWrapper => PostProcessEvent(outputAddedEventWrapper), + _ => throw new NotImplementedException($"Event type {anEvent.GetType().Name} is not supported."), + }; + } + internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) + { + } + + internal IEnumerable PostProcessEvent(FeatureStartedEvent featureStartedEvent) + { + return Enumerable.Empty(); + } + + internal void PreProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + } + internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + return Enumerable.Empty(); + } + + internal void PreProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + PickleId = scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = TestCaseTrackerId; + TestCaseId = IDGenerator.GetNewId(); + TestCaseStartedId = IDGenerator.GetNewId(); + } + internal IEnumerable PostProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); + var TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); + yield return Envelope.Create(TestCase); + yield return Envelope.Create(TestCaseStarted); + } + + internal void PreProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + Finished = true; + } + internal IEnumerable PostProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + ScenarioExecutionStatus = scenarioFinishedEvent.ScenarioContext.ScenarioExecutionStatus; + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); + } + + internal void PreProcessEvent(StepStartedEvent stepStartedEvent) + { + var stepState = new TestStepProcessor(this); + + stepState.ProcessEvent(stepStartedEvent); + StepsById.Add(stepState.PickleStepID, stepState); + StepsByEvent.Add(stepStartedEvent, stepState); + } + internal IEnumerable PostProcessEvent(StepStartedEvent stepStartedEvent) + { + var stepState = StepsById[stepStartedEvent.StepContext.StepInfo.PickleStepId]; + var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as TestStepProcessor, stepStartedEvent); + mostRecentTestStepStarted = stepStarted; + yield return Envelope.Create(stepStarted); + } + internal void PreProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; + stepState.ProcessEvent(stepFinishedEvent); + StepsByEvent.Add(stepFinishedEvent, stepState); + } + internal IEnumerable PostProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepState as TestStepProcessor, stepFinishedEvent)); + } + + internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) + return; + var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingStartedEvent); + StepsByEvent.Add(hookBindingStartedEvent, step); + + } + internal IEnumerable PostProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; + var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); + mostRecentTestStepStarted = hookStepStarted; + yield return Envelope.Create(hookStepStarted); + } + + internal void PreProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return; + + var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); + step.ProcessEvent(hookBindingFinishedEvent); + StepsByEvent.Add(hookBindingFinishedEvent, step); + } + internal IEnumerable PostProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + var hookStepProcessor = FindMatchingHookStartedEvent(hookBindingFinishedEvent); + + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); + } + internal void PreProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, ""); + _events.Enqueue(attachmentExecutionEventWrapper); + } + internal IEnumerable PostProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + return Enumerable.Empty(); + } + internal IEnumerable PostProcessEvent(AttachmentAddedEventWrapper attachmentAddedEventWrapper) + { + attachmentAddedEventWrapper.TestCaseStepID = mostRecentTestStepStarted.TestStepId; + attachmentAddedEventWrapper.TestCaseStartedID = mostRecentTestStepStarted.TestCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); + + } + internal void PreProcessEvent(OutputAddedEvent outputAddedEvent) + { + var pickleStepId = ""; + var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); + _events.Enqueue(outputExecutionEventWrapper); + } + + internal IEnumerable PostProcessEvent(OutputAddedEvent outputAddedEvent) + { + return Enumerable.Empty(); + } + + internal IEnumerable PostProcessEvent(OutputAddedEventWrapper outputAddedEventWrapper) + { + outputAddedEventWrapper.TestCaseStepID = mostRecentTestStepStarted.TestStepId; ; + outputAddedEventWrapper.TestCaseStartedID = mostRecentTestStepStarted.TestCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); + } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); + } + + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMessages/TestStepProcessor.cs similarity index 77% rename from Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs rename to Reqnroll/CucumberMessages/TestStepProcessor.cs index d13f26267..dce2ad41c 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMessages/TestStepProcessor.cs @@ -15,11 +15,11 @@ public class StepArgument public string Type; } - public class ScenarioStepProcessor : StepProcessorBase + public class TestStepProcessor : StepProcessorBase { private StepStartedEvent stepStartedEvent; - public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public TestStepProcessor(TestCaseCucumberMessageTracker parentTracker) : base(parentTracker) { } @@ -33,24 +33,19 @@ public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base( public List StepArguments { get; set; } - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + internal void ProcessEvent(StepStartedEvent stepStartedEvent) { this.stepStartedEvent = stepStartedEvent; - TestStepID = parentScenario.IdGenerator.GetNewId(); - return Enumerable.Empty(); + TestStepID = ParentTestCase.IDGenerator.GetNewId(); + PickleStepID = stepStartedEvent.StepContext.StepInfo.PickleStepId; } private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) { - return parentScenario.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + return ParentTestCase.StepDefinitionsByPattern[canonicalizedStepPattern]; } - private string FindPickleStepIDByStepText(string stepText) - { - return parentScenario.FeatureState.PicklesByScenarioName[parentScenario.Name].Steps.Where(st => st.Text == stepText).First().Id; - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + internal void ProcessEvent(StepFinishedEvent stepFinishedEvent) { var bindingMatch = stepFinishedEvent.StepContext?.StepInfo?.BindingMatch; Bound = !(bindingMatch == null || bindingMatch == BindingMatch.NonMatching); @@ -59,8 +54,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) CanonicalizedStepPattern = Bound ? CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition) : ""; StepDefinitionId = Bound ? FindStepDefIDByStepPattern(CanonicalizedStepPattern) : null; - PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); - Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; Status = stepFinishedEvent.StepContext.Status; @@ -81,7 +74,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); - return Enumerable.Empty(); } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs deleted file mode 100644 index 710b29ab5..000000000 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ /dev/null @@ -1,259 +0,0 @@ -using Gherkin.CucumberMessages; -using Io.Cucumber.Messages.Types; -using Reqnroll.Bindings; -using Reqnroll.Events; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Reqnroll.CucumberMessages -{ - public class FeatureEventProcessor - { - public string Name { get; set; } - public bool Enabled { get; set; } //This will be false if the feature could not be pickled - - // These two flags are used to avoid duplicate messages being sent when Scenarios within Features are run concurrently - // and multiple FeatureStartedEvent and FeatureFinishedEvent events are fired - public bool Started { get; set; } - public bool Finished { get; set; } - - public bool Success - { - get - { - return Enabled && Finished && ScenarioName2ScenarioProcessorMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); - } - } - - // ID Generator to use when generating IDs for TestCase messages and beyond - // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID - // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. - public IIdGenerator IDGenerator { get; set; } - - //Lookup tables - // - // These three dictionaries hold the mapping of steps, hooks, and pickles to their IDs - // These should only be produced by the first FeatureStartedEvent that this FeatureEventProcessor receives (it might receive multiple if the scenario is run concurrently) - // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once - public ConcurrentDictionary StepDefinitionsByPattern = new(); - public ConcurrentDictionary HookDefinitionsByPattern = new(); - - //TODO: fix this; there will be multiple Pickles with the same scenario name when executing Example table rows - public ConcurrentDictionary PicklesByScenarioName = new(); - - //TODO: Fix this for thread-safety; there will be multiple active Scenarios with the same name when executing Example table rows in parallel - // Scenario event processors by scenario name; - public Dictionary ScenarioName2ScenarioProcessorMap = new(); - - // The list of Cucumber Messages that are ready to be sent to the broker for distribution to consumers - public ConcurrentQueue Messages = new(); - - // A set of markers that represent the worker threads that are currently processing events for this feature. - // Once the last worker thread marker is removed, the Messages are then sent to the broker - public ConcurrentStack workerThreadMarkers = new(); - - internal void ProcessEvent(ExecutionEvent anEvent) - { - foreach (Envelope e in DispatchEvent(anEvent)) - { - Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = Name, Envelope = e }); - } - } - private IEnumerable DispatchEvent(ExecutionEvent anEvent) - { - return anEvent switch - { - FeatureStartedEvent featureStartedEvent => ProcessEvent(featureStartedEvent), - FeatureFinishedEvent featureFinishedEvent => ProcessEvent(featureFinishedEvent), - ScenarioStartedEvent scenarioStartedEvent => ProcessEvent(scenarioStartedEvent), - ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), - StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), - StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), - HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), - HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), - AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), - OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), - _ => throw new NotImplementedException(), - }; - } - - internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) - { - yield return CucumberMessageFactory.ToMeta(featureStartedEvent); - - Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - yield return Envelope.Create(messageSource); - - - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - yield return Envelope.Create(gherkinDocument); - - - var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); - var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); - - string lastID = ExtractLastID(pickles); - IDGenerator = IdGeneratorFactory.Create(lastID); - - foreach (var pickle in pickles) - { - PicklesByScenarioName.TryAdd(pickle.Name, pickle); - yield return Envelope.Create(pickle); - } - - var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - - foreach (var stepTransform in bindingRegistry.GetStepTransformations()) - { - var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); - yield return Envelope.Create(parameterType); - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) - { - var errmsg = binding.ErrorMessage; - if (errmsg.Contains("Undefined parameter type")) - { - var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; - var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); - yield return Envelope.Create(undefinedParameterType); - } - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); - - yield return Envelope.Create(stepDefinition); - } - - foreach (var hookBinding in bindingRegistry.GetHooks()) - { - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); - var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - HookDefinitionsByPattern.TryAdd(hookId, hook.Id); - yield return Envelope.Create(hook); - } - - yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); - - } - - internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) - { - yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); - } - - private string GenerateScenarioKey(ScenarioInfo scenarioInfo) - { - var scenarioArguments = new List(); - foreach (string v in scenarioInfo.Arguments.Values) - { - scenarioArguments.Add(v); - } - return scenarioInfo.Title - + scenarioArguments - + scenarioInfo.CombinedTags; - } - internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) - { - var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = new ScenarioEventProcessor(scenarioStartedEvent.ScenarioContext, this); - ScenarioName2ScenarioProcessorMap.Add(scenarioName, scenarioEP); - - foreach (var e in scenarioEP.ProcessEvent(scenarioStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) - { - var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(scenarioFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) - { - var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(stepStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) - { - var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(stepFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedEvent) - { - var scenarioName = hookStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - var scenarioName = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookBindingFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) - { - var scenarioName = attachmentAddedEvent.ScenarioName; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(attachmentAddedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) - { - var scenarioName = outputAddedEvent.ScenarioName; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioEP.ProcessEvent(outputAddedEvent)) - { - yield return e; - } - } - - private string ExtractLastID(List pickles) - { - return pickles.Last().Id; - } - - } -} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs deleted file mode 100644 index d20816a1e..000000000 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ /dev/null @@ -1,205 +0,0 @@ -using Cucumber.Messages; -using Gherkin.CucumberMessages; -using Io.Cucumber.Messages.Types; -using Reqnroll.Bindings; -using Reqnroll.Events; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Reqnroll.CucumberMessages -{ - - public class ScenarioEventProcessor - { - internal readonly IIdGenerator IdGenerator; - internal readonly FeatureEventProcessor FeatureState; - - public string TestCaseStartedID; - public string Name { get; set; } - public string TestCaseID { get; set; } - public string PickleID { get; set; } - public Pickle Pickle { get; set; } - private TestCase TestCase; - - private TestCaseStarted TestCaseStarted; - - // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages - private Queue _events = new(); - - public Dictionary StepsByEvent { get; private set; } = new(); - public List Steps - { - get - { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); - } - } - public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } - - public ScenarioEventProcessor(IScenarioContext context, FeatureEventProcessor featureState) - { - IdGenerator = featureState.IDGenerator; - FeatureState = featureState; - - Name = context.ScenarioInfo.Title; - TestCaseID = IdGenerator.GetNewId(); - Pickle = featureState.PicklesByScenarioName[Name]; - PickleID = featureState.PicklesByScenarioName[Name].Id; - } - - internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) - { - TestCaseStartedID = IdGenerator.GetNewId(); - _events.Enqueue(scenarioStartedEvent); - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - _events.Enqueue(hookBindingStartedEvent); - var step = new HookStepProcessor(this); - step.ProcessEvent(hookBindingStartedEvent); - StepsByEvent.Add(hookBindingStartedEvent, step); - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - - _events.Enqueue(hookBindingFinishedEvent); - var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); - step.ProcessEvent(hookBindingFinishedEvent); - StepsByEvent.Add(hookBindingFinishedEvent, step); - return Enumerable.Empty(); - } - - private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); - } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) - { - _events.Enqueue(stepStartedEvent); - - var stepState = new ScenarioStepProcessor(this); - StepsByEvent.Add(stepStartedEvent, stepState); - stepState.ProcessEvent(stepStartedEvent); - - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) - { - _events.Enqueue(stepFinishedEvent); - var stepState = FindMatchingStepStartEvent(stepFinishedEvent); - stepState.ProcessEvent(stepFinishedEvent); - StepsByEvent.Add(stepFinishedEvent, stepState); - - return Enumerable.Empty(); - } - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) - { - //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = ""; - - var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); - _events.Enqueue(attachmentExecutionEventWrapper); - - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) - { - //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); - //var pickleStepId = matchingPickleStep.Id; - - var pickleStepId = ""; - var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); - _events.Enqueue(outputExecutionEventWrapper); - - return Enumerable.Empty(); - } - - private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) - { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent)(kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); - } - - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) - { - _events.Enqueue(scenarioFinishedEvent); - TestStepStarted mostRecentTestStepStarted = null; - - while (_events.Count > 0) - { - var executionEvent = _events.Dequeue(); - - switch (executionEvent) - { - case ScenarioStartedEvent scenarioStartedEvent: - TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); - TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); - yield return Envelope.Create(TestCase); - yield return Envelope.Create(TestCaseStarted); - break; - case ScenarioFinishedEvent scenarioFinished: - ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); - break; - case StepStartedEvent stepStartedEvent: - var stepState = StepsByEvent[stepStartedEvent]; - var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent); - mostRecentTestStepStarted = stepStarted; - yield return Envelope.Create(stepStarted); - break; - case StepFinishedEvent stepFinishedEvent: - var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); - break; - case HookBindingStartedEvent hookBindingStartedEvent: - var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; - var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); - mostRecentTestStepStarted = hookStepStarted; - yield return Envelope.Create(hookStepStarted); - break; - case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepProcessor = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); - break; - case AttachmentAddedEventWrapper attachmentAddedEventWrapper: - // find the TestCaseStepId and testCaseStartedId - var testStepID = mostRecentTestStepStarted.TestStepId; - var testCaseStartedId = TestCaseStarted.Id; - attachmentAddedEventWrapper.TestCaseStepID = testStepID; - attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; - yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); - break; - case OutputAddedEventWrapper outputAddedEventWrapper: - // find the TestCaseStepId and testCaseStartedId - testStepID = mostRecentTestStepStarted.TestStepId; - testCaseStartedId = TestCaseStarted.Id; - outputAddedEventWrapper.TestCaseStepID = testStepID; - outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; - yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); - break; - // add more cases for other event types - default: - throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); - } - } - - } - } -} \ No newline at end of file diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index a3f2a0003..e211c8524 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -211,7 +211,7 @@ public interface IExecutionOutputEvent public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string Text { get; } - public string FeatureName { get; } + public FeatureInfo FeatureInfo { get; } public string ScenarioName { get; } public string StepText { get; } @@ -220,31 +220,24 @@ public OutputAddedEvent(string text) Text = text; } - public OutputAddedEvent(string text, string featureName, string scenarioName, string stepText) : this(text) + public OutputAddedEvent(string text, FeatureInfo featureInfo) : this(text) { - FeatureName = featureName; - ScenarioName = scenarioName; - StepText = stepText; + FeatureInfo = featureInfo; } } public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } - public string FeatureName { get; } - public string ScenarioName { get; } - public string StepText { get; } - + public FeatureInfo FeatureInfo { get; } public AttachmentAddedEvent(string filePath) { FilePath = filePath; } - public AttachmentAddedEvent(string filePath, string featureName, string scenarioName, string stepText) : this(filePath) + public AttachmentAddedEvent(string filePath, FeatureInfo featureInfo) : this(filePath) { - FeatureName = featureName; - ScenarioName = scenarioName; - StepText = stepText; + FeatureInfo = featureInfo; } } } diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index 5235ec7c7..42a157b3c 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -17,9 +17,10 @@ public class FeatureInfo public CultureInfo Language { get; private set; } - // TODO: Add this to the constructor and update all the related tests // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } + // This holds the unique identifier for the tracker instance that is being used to generate cucumber messages for this Test Case + public string CucumberMessages_TestCaseTrackerId { get; set; } public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index 604d08d88..477bed81e 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -20,11 +20,9 @@ public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecuti public void WriteLine(string message) { - var featureName = contextManager.FeatureContext.FeatureInfo?.Title; - var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext?.StepInfo?.Text; + var featureInfo = contextManager.FeatureContext.FeatureInfo; - _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); + _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureInfo)); _traceListener.WriteToolOutput(message); } @@ -35,10 +33,8 @@ public void WriteLine(string format, params object[] args) public void AddAttachment(string filePath) { - var featureName = contextManager.FeatureContext.FeatureInfo?.Title; - var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext?.StepInfo?.Text; - _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); + var featureInfo = contextManager.FeatureContext.FeatureInfo; + _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureInfo)); _reqnrollAttachmentHandler.AddAttachment(filePath); } } diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index d38ef61af..3071a72fc 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,7 @@ - + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d9cb9cece..f7c6a601b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -42,12 +42,8 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test - @some-tag - Scenario: Log JSON - When the following string is attached as "application/json": - ``` - {"message": "The big question", "foo": "bar"} - ``` + Scenario: Eating Cukes + When I eat 5 cukes """); AddPassingStepBinding("When"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index aea923ac8..4f29059ea 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -132,7 +132,7 @@ - + From ca147142d2b9c22112bd864d88ba8c1315aeaa8a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:08:56 -0500 Subject: [PATCH 066/218] Most tests passing. Need to clean up Specs. --- .../CucumberMessage_TraversalVisitorBase.cs | 38 +++++++++---------- Reqnroll/CucumberMessages/FeatureTracker.cs | 4 +- Reqnroll/Reqnroll.nuspec | 1 + .../CCK/examples-tables/examples_tables.cs | 2 +- .../CucumberCompatibilityTests.cs | 6 ++- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs index 0bb4ac6d7..a337956af 100644 --- a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs @@ -40,7 +40,7 @@ public virtual void Visit(GherkinDocument gherkinDocument) public virtual void Visit(Feature feature) { OnVisiting(feature); - foreach (var featureChild in feature.Children) + foreach (var featureChild in feature.Children ?? new List()) { Accept(featureChild); } @@ -62,11 +62,11 @@ public virtual void Visit(FeatureChild featureChild) public virtual void Visit(Rule rule) { OnVisiting(rule); - foreach (var ruleChild in rule.Children) + foreach (var ruleChild in rule.Children ?? new List()) { Accept(ruleChild); } - foreach (var tag in rule.Tags) + foreach (var tag in rule.Tags ?? new List()) { Accept(tag); } @@ -87,7 +87,7 @@ public virtual void Visit(Background background) { OnVisiting(background); Accept(background.Location); - foreach (var step in background.Steps) + foreach (var step in background.Steps ?? new List()) { Accept(step); } @@ -98,15 +98,15 @@ public virtual void Visit(Scenario scenario) { OnVisiting(scenario); Accept(scenario.Location); - foreach (var tag in scenario.Tags) + foreach (var tag in scenario.Tags ?? new List()) { Accept(tag); } - foreach (var step in scenario.Steps) + foreach (var step in scenario.Steps ?? new List()) { Accept(step); } - foreach (var example in scenario.Examples) + foreach (var example in scenario.Examples ?? new List()) { Accept(example); } @@ -117,12 +117,12 @@ public virtual void Visit(Examples examples) { OnVisiting(examples); Accept(examples.Location); - foreach (var tag in examples.Tags) + foreach (var tag in examples.Tags ?? new List()) { Accept(tag); } Accept(examples.TableHeader); - foreach (var tableRow in examples.TableBody) + foreach (var tableRow in examples.TableBody ?? new List()) { Accept(tableRow); } @@ -142,7 +142,7 @@ public virtual void Visit(TableRow tableRow) { OnVisiting(tableRow); Accept(tableRow.Location); - foreach (var tableCell in tableRow.Cells) + foreach (var tableCell in tableRow.Cells ?? new List()) { Accept(tableCell); } @@ -166,11 +166,11 @@ public virtual void Visit(Tag tag) public virtual void Visit(Pickle pickle) { OnVisiting(pickle); - foreach (var pickleStep in pickle.Steps) + foreach (var pickleStep in pickle.Steps ?? new List()) { Accept(pickleStep); } - foreach (var tag in pickle.Tags) + foreach (var tag in pickle.Tags ?? new List()) { Accept(tag); } @@ -197,7 +197,7 @@ public virtual void Visit(PickleStepArgument pickleStepArgument) public virtual void Visit(PickleTable pickleTable) { OnVisiting(pickleTable); - foreach (var pickleTableRow in pickleTable.Rows) + foreach (var pickleTableRow in pickleTable.Rows ?? new List()) { Accept(pickleTableRow); } @@ -207,7 +207,7 @@ public virtual void Visit(PickleTable pickleTable) public virtual void Visit(PickleTableRow pickleTableRow) { OnVisiting(pickleTableRow); - foreach (var pickleTableCell in pickleTableRow.Cells) + foreach (var pickleTableCell in pickleTableRow.Cells ?? new List()) { Accept(pickleTableCell); } @@ -229,7 +229,7 @@ public virtual void Visit(PickleTag pickleTag) public virtual void Visit(TestCase testCase) { OnVisiting(testCase); - foreach (var step in testCase.TestSteps) + foreach (var step in testCase.TestSteps ?? new List()) { Accept(step); } @@ -253,7 +253,7 @@ public virtual void Visit(TestCaseFinished testCaseFinished) public virtual void Visit(TestStep testStep) { OnVisiting(testStep); - foreach (var argumentList in testStep.StepMatchArgumentsLists) + foreach (var argumentList in testStep.StepMatchArgumentsLists ?? new List()) { Accept(argumentList); } @@ -385,7 +385,7 @@ public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) { OnVisiting(dataTable); Accept(dataTable.Location); - foreach (var row in dataTable.Rows) + foreach (var row in dataTable.Rows ?? new List()) { Accept(row); } @@ -402,7 +402,7 @@ public virtual void Visit(DocString docString) public virtual void Visit(Group group) { OnVisiting(group); - foreach (var child in group.Children) + foreach (var child in group.Children ?? new List()) { Accept(child); } @@ -456,7 +456,7 @@ public virtual void Visit(StepMatchArgument stepMatchArgument) public virtual void Visit(StepMatchArgumentsList stepMatchArgumentsList) { OnVisiting(stepMatchArgumentsList); - foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments) + foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments ?? new List()) { Accept(stepMatchArgument); } diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs index 0b6ac7f37..18517816d 100644 --- a/Reqnroll/CucumberMessages/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -80,7 +80,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + if (!StepDefinitionsByPattern.ContainsKey(pattern)) StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); } @@ -89,7 +89,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - StepDefinitionsByPattern.Add(hookId, hook.Id); + if (!StepDefinitionsByPattern.ContainsKey(hookId)) StepDefinitionsByPattern.Add(hookId, hook.Id); yield return Envelope.Create(hook); } diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec index b98dddb40..742ab84f2 100644 --- a/Reqnroll/Reqnroll.nuspec +++ b/Reqnroll/Reqnroll.nuspec @@ -20,6 +20,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs index a87217d3e..74d5c6cbc 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs @@ -31,7 +31,7 @@ public void WhenIEatCucumbers(int p0) _count -= p0; } - [Then("I should have {int} cuc umbers")] + [Then("I should have {int} cucumbers")] public void ThenIShouldHaveCucumbers(int p0) { if (_count != p0) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index f7c6a601b..4a2bd13aa 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -235,7 +235,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("undefined", "Undefined steps")] [DataRow("unknown-parameter-type", "Unknown Parameter Types")] [DataRow("stack-traces", "Stack traces")] - [DataRow("rules", "Usage of a 'Rule'")] + [DataRow("rules", "Usage of a `Rule`")] public void CCKScenarios(string testName, string featureNameText) { AddCucumberMessagePlugIn(); @@ -255,7 +255,9 @@ public void CCKScenarios(string testName, string featureNameText) validator.ResultShouldPassAllComparisonTests(); validator.ResultShouldPassSanityChecks(); - ConfirmAllTestsRan(null); + + // This is necessary b/c the System Test framework doesn't understand Rules and can't determine the number of expected tests + ConfirmAllTestsRan(testName == "rules" ? 3 : null); } [TestMethod] From ced75d67b79fdaa14de7490b46ce3c6947c49eb5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:49:49 -0500 Subject: [PATCH 067/218] Added support for Background steps. Minor refactoring to UnitTestMethodGenerator and ScenarioPartHelper - moved pickle generation to SPH. Add new non-CCK scenario for Backgrounds --- .../Generation/GeneratorConstants.cs | 4 ++ .../Generation/ScenarioPartHelper.cs | 61 +++++++++++++++++-- .../Generation/UnitTestMethodGenerator.cs | 48 +++------------ Reqnroll/CucumberMessages/PickleJar.cs | 1 - .../CCK/background/background.cs | 47 ++++++++++++++ .../CCK/background/background.feature | 17 ++++++ .../CCK/background/background.feature.ndjson | 36 +++++++++++ .../CucumberCompatibilityTests.cs | 1 + ...CucumberMessages.CompatibilityTests.csproj | 6 ++ .../CucumberExpressionIntegrationTests.cs | 2 +- 10 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 3a9a6b081..18f58e348 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -18,5 +18,9 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; + public const string PICKLEID_PARAMETER_NAME = "@pickleId"; + public const string PICKLEID_VARIABLE_NAME = "m_pickleId"; + public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; + public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "pickleStepSequence"; } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 3dd6839a8..4c852e9c0 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -12,8 +12,6 @@ namespace Reqnroll.Generator.Generation { public class ScenarioPartHelper { - private const string BRINE = "PickleJar"; - private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly CodeDomHelper _codeDomHelper; private int _tableCounter; @@ -37,6 +35,7 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) backgroundMethod.Attributes = MemberAttributes.Public; backgroundMethod.Name = GeneratorConstants.BACKGROUND_NAME; + backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); _codeDomHelper.MarkCodeMemberMethodAsAsync(backgroundMethod); @@ -44,13 +43,14 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, background.Location)) { } + AddVariableForPickleStepSequenceFromMethodParameter(backgroundMethod); foreach (var step in background.Steps) { GenerateStep(generationContext, statements, step, null); } backgroundMethod.Statements.AddRange(statements.ToArray()); - + } #region Rule Background Support @@ -96,9 +96,9 @@ public void GenerateStep(TestClassGenerationContext generationContext, List {new CodePrimitiveExpression(formatText)}; + var formatArguments = new List { new CodePrimitiveExpression(formatText) }; formatArguments.AddRange(arguments.Select(id => new CodeVariableReferenceExpression(id))); return new CodeMethodInvokeExpression( @@ -230,5 +234,50 @@ private CodeExpression GetSubstitutedString(string text, ParameterSubstitution p "Format", formatArguments.ToArray()); } + public void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + { + // string m_pickleId = pickleJar.CurrentPickleId; or + // string m_pickleId = @pickleId; + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEID_VARIABLE_NAME, + pickleIdIncludedInParameters ? + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleJar.CurrentPickleId)); + testMethod.Statements.Add(pickleIdVariable); + } + + public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod testMethod) + { + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); + + testMethod.Statements.Add(pickleStepSequence); + } + + public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMethod) + { + // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + // Right side of the assignment (property access chain) + + new CodeMethodInvokeExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(GeneratorConstants.TESTRUNNER_FIELD), + "FeatureContext" + ), + "FeatureInfo" + ), + "FeatureCucumberMessages" + ), + "PickleJar" + ), + "PickleStepSequenceFor", + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME)) + ); + + testMethod.Statements.Add(pickleStepSequence); + } } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 0d8794649..0dde9c14b 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -18,8 +18,6 @@ namespace Reqnroll.Generator.Generation public class UnitTestMethodGenerator { private const string PICKLEJAR = "PICKLEJAR"; - private const string PICKLEID_PARAMETER_NAME = "@pickleId"; - private const string PICKLEID_VARIABLE_NAME = "m_pickleId"; private const string IGNORE_TAG = "@Ignore"; private const string TESTRUNNER_FIELD = "testRunner"; private readonly CodeDomHelper _codeDomHelper; @@ -212,7 +210,7 @@ private void GenerateTestBody( new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), inheritedTagsExpression, - new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)))); + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -221,40 +219,14 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } - private void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + internal void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) { - var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), PICKLEID_VARIABLE_NAME, - pickleIdIncludedInParameters ? - new CodeVariableReferenceExpression(PICKLEID_PARAMETER_NAME) : - new CodePrimitiveExpression(pickleJar.CurrentPickleId)); - testMethod.Statements.Add(pickleIdVariable); + _scenarioPartHelper.AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, pickleJar); } - private void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) + internal void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) { - // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME, - // Right side of the assignment (property access chain) - - new CodeMethodInvokeExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodeVariableReferenceExpression(TESTRUNNER_FIELD), - "FeatureContext" - ), - "FeatureInfo" - ), - "FeatureCucumberMessages" - ), - "PickleJar" - ), - "PickleStepSequenceFor", - new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)) - ); - - testMethod.Statements.Add(pickleStepSequence); + _scenarioPartHelper.AddVariableForPickleStepSequenceForPickleId(testMethod); } private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { @@ -310,7 +282,8 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex { var backgroundMethodCallExpression = new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), - generationContext.FeatureBackgroundMethod.Name); + generationContext.FeatureBackgroundMethod.Name, + new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)); _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(backgroundMethodCallExpression); statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(backgroundMethodCallExpression)); @@ -322,9 +295,6 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex foreach (var scenarioStep in scenario.Steps) { _scenarioPartHelper.GenerateStep(generationContext, statementsWhenScenarioIsExecuted, scenarioStep, paramToIdentifier); - statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(new CodeMethodInvokeExpression( - new CodeVariableReferenceExpression(PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME), - "NextStep"))); } var tagsOfScenarioVariableReferenceExpression = new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME); @@ -433,7 +403,7 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex foreach (var row in examples.TableBody) { var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleJar.CurrentPickleId }); - + _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); _pickleJar.NextPickle(); @@ -497,7 +467,7 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), @"@pickleId")); + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), GeneratorConstants.PICKLEID_PARAMETER_NAME)); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } diff --git a/Reqnroll/CucumberMessages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs index df6b70b02..e6f230cd9 100644 --- a/Reqnroll/CucumberMessages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -45,7 +45,6 @@ public void NextPickle() public class PickleStepSequence { - public static string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; public Pickle CurrentPickle { get; } private int _PickleStepCounter; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs new file mode 100644 index 000000000..d4a5f29d3 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs @@ -0,0 +1,47 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.background +{ + [Binding] + internal class Background + { + private Dictionary accounts = new Dictionary(); + private int total = 0; + + [Given(@"I have ${int} in my {word} account")] + public void AddMoneyToAccount(int amount, string account) + { + accounts[account] = amount; + } + + [When("the accounts are combined")] + public void CombineAccounts() + { + total = accounts.Sum(x => x.Value); + } + + [Then(@"I have ${int}")] + public void CheckTotalBalance(int amount) + { + if(total != amount) throw new ApplicationException("Total balance should be " + amount); + } + + [When(@"I transfer ${int} from {word} to {word}")] + public void TransferMoney(int amount, string from, string to) + { + accounts[from] -= amount; + accounts[to] += amount; + } + + [Then(@"My {word} account has a balance of ${int}")] + public void CheckAccountBalance(string account, int balance) + { + if(accounts[account] != balance) throw new ApplicationException($"Account: {account} balance should be " + balance); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature new file mode 100644 index 000000000..41424d2e9 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature @@ -0,0 +1,17 @@ +Feature: background + +Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature + +Background: +# set up bank account balance +Given I have $500 in my checking account +And I have $200 in my savings account + +Scenario: Combined Balance + When the accounts are combined + Then I have $700 + +Scenario: Transfer Money + When I transfer $150 from savings to checking + Then My checking account has a balance of $650 + And My savings account has a balance of $50 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson new file mode 100644 index 000000000..f44c8ac17 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+86e9495e4e6815ab377bdbc6ea2eebd6f89a0537"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} +{"pickle":{"id":"14","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"10","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"11","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"12","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"13","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} +{"pickle":{"id":"20","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"15","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"16","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"17","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"18","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"19","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} +{"stepDefinition":{"id":"21","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} +{"stepDefinition":{"id":"22","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"23","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} +{"stepDefinition":{"id":"24","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} +{"stepDefinition":{"id":"25","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} +{"testRunStarted":{"timestamp":{"seconds":1727192229,"nanos":420947400}}} +{"testCase":{"id":"26","pickleId":"14","testSteps":[{"id":"28","pickleStepId":"10","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"11","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"26","timestamp":{"seconds":1727192229,"nanos":452538900}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"28","timestamp":{"seconds":1727192229,"nanos":455045500}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"28","testStepResult":{"duration":{"seconds":0,"nanos":19933400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":474978900}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"29","timestamp":{"seconds":1727192229,"nanos":476934500}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"29","testStepResult":{"duration":{"seconds":0,"nanos":698600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":477633100}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"30","timestamp":{"seconds":1727192229,"nanos":478014600}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"30","testStepResult":{"duration":{"seconds":0,"nanos":1699400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":479714000}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"31","timestamp":{"seconds":1727192229,"nanos":479992900}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"31","testStepResult":{"duration":{"seconds":0,"nanos":551600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":480544500}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"seconds":1727192229,"nanos":482528500},"willBeRetried":false}} +{"testCase":{"id":"32","pickleId":"20","testSteps":[{"id":"34","pickleStepId":"15","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"17","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"18","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"38","pickleStepId":"19","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"33","testCaseId":"32","timestamp":{"seconds":1727192229,"nanos":495596400}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"34","timestamp":{"seconds":1727192229,"nanos":495781000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"34","testStepResult":{"duration":{"seconds":0,"nanos":278500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496059500}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"35","timestamp":{"seconds":1727192229,"nanos":496095600}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"35","testStepResult":{"duration":{"seconds":0,"nanos":83500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496179100}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"36","timestamp":{"seconds":1727192229,"nanos":496202200}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"36","testStepResult":{"duration":{"seconds":0,"nanos":685000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496887200}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"37","timestamp":{"seconds":1727192229,"nanos":496910500}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"37","testStepResult":{"duration":{"seconds":0,"nanos":512200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497422700}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"38","timestamp":{"seconds":1727192229,"nanos":497441700}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"38","testStepResult":{"duration":{"seconds":0,"nanos":194200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497635900}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":1727192229,"nanos":497750600},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"seconds":1727192229,"nanos":499781200}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 4a2bd13aa..7be0f6340 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -262,6 +262,7 @@ public void CCKScenarios(string testName, string featureNameText) [TestMethod] [DataRow("ambiguous", "ambiguous")] + [DataRow("background", "background")] public void NonCCKScenarios(string testName, string featureNameText) { CCKScenarios(testName, featureNameText); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 4f29059ea..0813739d3 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -12,6 +12,7 @@ + @@ -31,6 +32,8 @@ + + @@ -67,6 +70,9 @@ Always + + + diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 4c140d512..2f4511a66 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -186,7 +186,7 @@ private async Task PerformStepExecution(string methodName, strin await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo("en-US"), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); await engine.OnScenarioStartAsync(); engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null)); - await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); + await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null, "stubPickleStepId"); var contextManager = testThreadContainer.Resolve(); contextManager.ScenarioContext.ScenarioExecutionStatus.Should().Be(ScenarioExecutionStatus.OK, $"should not fail with '{contextManager.ScenarioContext.TestError?.Message}'"); From cde5c36341c59527ecf1cf969eca54e10e3c2dd1 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:32:25 -0500 Subject: [PATCH 068/218] Moved FileSink to be a built-in plug-in --- Reqnroll.sln | 10 - .../CucumberMessages-config-schema.json | 34 +++ .../CucumberMessages/FileSinkConfiguration.cs | 35 ++++ Reqnroll/CucumberMessages/FileSinkPlugin.cs | 195 ++++++++++++++++++ .../DefaultDependencyProvider.cs | 2 + 5 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 Reqnroll/CucumberMessages/CucumberMessages-config-schema.json create mode 100644 Reqnroll/CucumberMessages/FileSinkConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/FileSinkPlugin.cs diff --git a/Reqnroll.sln b/Reqnroll.sln index e0e020419..fe8c30ca0 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,12 +116,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.CompatibilityTests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.CompatibilityTests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Plugins\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{58B078D5-37A2-416B-91DA-E5E6BD510062}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -252,10 +248,6 @@ Global {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.Build.0 = Debug|Any CPU {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.ActiveCfg = Release|Any CPU {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.Build.0 = Release|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -294,9 +286,7 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {5072F73C-8CDD-4B44-B3F8-4212F65C3708} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} - {58B078D5-37A2-416B-91DA-E5E6BD510062} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json new file mode 100644 index 000000000..f7fcd8f17 --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "filSinkEnabled": { + "type": "boolean", + "description": "Indicates whether the program is enabled or not" + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether this destination setting is enabled" + }, + "basePath": { + "type": "string", + "description": "The base path for output files" + }, + "outputDirectory": { + "type": "string", + "description": "The directory path where output should go" + } + }, + "required": [ "enabled", "basePath", "outputDirectory" ] + }, + "minItems": 1, + "description": "Array of destination settings" + } + }, + "required": [ "programEnabled", "destinations" ] +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs new file mode 100644 index 000000000..7a56f4233 --- /dev/null +++ b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs new file mode 100644 index 000000000..40baea678 --- /dev/null +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -0,0 +1,195 @@ +#nullable enable + +using Reqnroll.CucumberMessages; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using Io.Cucumber.Messages.Types; +using System.Reflection; +using Reqnroll.Events; +using System.Collections.Concurrent; +using System.Text.Json; +using Reqnroll.Tracing; +using Reqnroll.BoDi; +using System; +using System.Threading.Tasks; +using System.IO; +using System.Linq; + + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin + { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private Task? fileWritingTask; + + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) + private object _lock = new(); + private readonly BlockingCollection postedMessages = new(); + private readonly ConcurrentDictionary fileStreams = new(); + private FileSinkConfiguration? configuration; + private string baseDirectory = ""; + private Lazy? traceListener; + private ITraceListener? trace => traceListener?.Value; + private IObjectContainer? objectContainer; + + public FileSinkPlugin() + { + traceListener = new Lazy(() => objectContainer!.Resolve()); + } + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + objectContainer = args.ObjectContainer; + testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); + testThreadExecutionEventPublisher.AddHandler(CloseFileSink); + }; + } + + private string ProcessConfiguration(FileSinkConfiguration configuration) + { + var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeDestination != null) + { + var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); + if (!Directory.Exists(basePath)) + { + lock (_lock) + { + if (!Directory.Exists(basePath)) + Directory.CreateDirectory(basePath); + } + } + trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); + return basePath; + } + else + { + var location = Assembly.GetExecutingAssembly().Location; + trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); + return location; + } + } + + private void CloseFileSink(TestRunFinishedEvent @event) + { + trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); + postedMessages.CompleteAdding(); + fileWritingTask?.Wait(); + fileWritingTask = null; + } + + private void LaunchFileSink(TestRunStartedEvent testRunStarted) + { + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + return; + } + + baseDirectory = ProcessConfiguration(configuration); + + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + } + + public void Publish(ReqnrollCucumberMessage message) + { + var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; + trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + postedMessages.Add(message); + } + + private async Task ConsumeAndWriteToFiles() + { + foreach (var message in postedMessages.GetConsumingEnumerable()) + { + var featureName = message.CucumberMessageSource; + + if (message.Envelope != null) + { + var cm = Serialize(message.Envelope); + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + await Write(featureName, cm); + } + else + { + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + CloseFeatureStream(featureName); + } + } + } + + + private string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + private async Task Write(string featureName, string cucumberMessage) + { + try + { + if (!fileStreams.ContainsKey(featureName)) + { + lock (_lock) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + } + } + trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); + } + catch (System.Exception ex) + { + trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); + } + } + + private void CloseFeatureStream(string featureName) + { + trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); + fileStreams[featureName].Close(); + fileStreams.TryRemove(featureName, out var _); + } + private bool disposedValue; + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CloseFileSink(new TestRunFinishedEvent()); + postedMessages.Dispose(); + foreach (var stream in fileStreams.Values) + { + stream.Close(); + stream.Dispose(); + }; + fileStreams.Clear(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 61d0c283b..93c80f693 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -17,6 +17,7 @@ using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; using Reqnroll.CucumberMessages; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; namespace Reqnroll.Infrastructure { @@ -104,6 +105,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); + container.RegisterTypeAs("FileSinkPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) From 52d5e1006faf78d6bea1b3fbd7adb2e85f76e14b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:50:32 -0500 Subject: [PATCH 069/218] FileSink Configuration supports environment variable overrides for the storage location of output and a flag to enable or disable execution. --- .../CucumberMessages/FileSinkConfiguration.cs | 51 +++++++++++- Reqnroll/CucumberMessages/FileSinkPlugin.cs | 48 ++++------- .../unknown_parameter_type.feature | 2 +- .../unknown_parameter_type.feature.ndjson | 2 +- .../CucumberCompatibilityTestBase.cs | 8 +- .../CucumberCompatibilityTests.cs | 81 +++++++++---------- 6 files changed, 110 insertions(+), 82 deletions(-) diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs index 7a56f4233..65e5d61c2 100644 --- a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs +++ b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs @@ -1,21 +1,66 @@ -using System; +using Reqnroll.Time; +using Reqnroll.Tracing; +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - internal class FileSinkConfiguration + public class FileSinkConfiguration { + internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; + private object _lock = new(); + public bool FileSinkEnabled { get; set; } public List Destinations { get; set; } public FileSinkConfiguration() : this(true) { } public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) { FileSinkEnabled = fileSinkEnabled; Destinations = destinations; } + public string ConfiguredOutputDirectory(ITraceListener trace) + { + string outputDirectory; + string configuredOutputDirectory = string.Empty; + string defaultOutputDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string environmentVariableOutputDirectory = Environment.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var activeConfiguredDestination = Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); + } + + outputDirectory = defaultOutputDirectory; + if (!String.IsNullOrEmpty(configuredOutputDirectory)) + outputDirectory = configuredOutputDirectory; + if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) + outputDirectory = environmentVariableOutputDirectory; + + string logEntry; + if (outputDirectory == defaultOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; + else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; + else logEntry = $"FileSinkPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; + + trace?.WriteTestOutput(logEntry); + if (!Directory.Exists(outputDirectory)) + { + lock (_lock) + { + if (!Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + } + } + return outputDirectory; + } + } public class Destination diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs index 40baea678..75b9e016a 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -21,12 +21,13 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; + private object _lock = new(); //Thread safe collections to hold: // 1. Inbound Cucumber Messages - BlockingCollection // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - private object _lock = new(); private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private FileSinkConfiguration? configuration; @@ -53,33 +54,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; } - - private string ProcessConfiguration(FileSinkConfiguration configuration) - { - var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeDestination != null) - { - var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); - if (!Directory.Exists(basePath)) - { - lock (_lock) - { - if (!Directory.Exists(basePath)) - Directory.CreateDirectory(basePath); - } - } - trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); - return basePath; - } - else - { - var location = Assembly.GetExecutingAssembly().Location; - trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); - return location; - } - } - private void CloseFileSink(TestRunFinishedEvent @event) { trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); @@ -90,16 +64,24 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) + bool environmentEnabled = "true".Equals(Environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE), StringComparison.InvariantCultureIgnoreCase); + bool environmentLocationSpecified = !String.IsNullOrEmpty(FileSinkConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + } + else if (environmentEnabled && environmentLocationSpecified) + configuration = new FileSinkConfiguration(true); + else configuration = new FileSinkConfiguration(false); + if (!configuration.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } - baseDirectory = ProcessConfiguration(configuration); + baseDirectory = configuration.ConfiguredOutputDirectory(trace); + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature index 4ce05c974..59e2d6201 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature @@ -1,4 +1,4 @@ -Feature: Parameter Types +Feature: Unknown Parameter Types Cucumber will generate an error message if a step definition registers an unknown parameter type, but the suite will run. diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson index cc9886734..ab4bb0bef 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson @@ -1,5 +1,5 @@ {"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} -{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"source":{"data":"Feature: Unknown Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"1","keyword":"Scenario","location":{"column":3,"line":5},"name":"undefined parameter type","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":6},"text":"CDG is closed because of a strike"}],"tags":[]}}],"description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"pickle":{"astNodeIds":["1"],"id":"3","language":"en","name":"undefined parameter type","steps":[{"astNodeIds":["0"],"id":"2","text":"CDG is closed because of a strike","type":"Context"}],"tags":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"undefinedParameterType":{"expression":"{airport} is closed because of a strike","name":"airport"}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 4f7fa2d1c..a9c7cac2c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -16,11 +16,15 @@ protected override void TestCleanup() // TEMPORARY: this is in place so that SystemTestBase.TestCleanup does not run (which deletes the generated code) } - protected void AddCucumberMessagePlugIn() + protected void EnableCucumberMessages() { - _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.1-local"); + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); } + protected void DisableCucumberMessages() + { + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "false"); + } protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) { var bindingCLassFileContent = _testFileManager.GetTestFileContent(fileName, prefix, assemblyToLoadFrom); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7be0f6340..a0465bd33 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -11,17 +11,22 @@ using Reqnroll.TestProjectGenerator; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Reqnroll.TestProjectGenerator.Driver; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { + private void ResetCucumberMessages() + { + DisableCucumberMessages(); + } [TestMethod] public void NullTest() { + ResetCucumberMessages(); // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else - AddFeatureFile(""" Feature: Cucumber Messages Null Test Scenario: Eating Cukes @@ -37,7 +42,8 @@ When I eat 5 cukes [TestMethod] public void SmokeTest() { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -55,7 +61,8 @@ When I eat 5 cukes [TestMethod] public void SmokeOutlineTest() { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -78,10 +85,11 @@ public void SmokeOutlineTest() [TestMethod] public void SmokeOutlineTestAsMethods() { + ResetCucumberMessages(); var _configurationFileDriver = GetServiceSafe(); _configurationFileDriver.SetIsRowTestsAllowed(false); - AddCucumberMessagePlugIn(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -106,8 +114,9 @@ public void SmokeOutlineTestAsMethods() [TestMethod] public void CucumberMessagesInteropWithExternalData() { + ResetCucumberMessages(); // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages - AddCucumberMessagePlugIn(); + EnableCucumberMessages(); _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -218,27 +227,30 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) """); ExecuteTests(); - ShouldAllScenariosPass(); + ShouldAllScenariosPass(3); } [TestMethod] - [DataRow("attachments", "Attachments")] [DataRow("minimal", "minimal")] [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] [DataRow("examples-tables", "Examples Tables")] - [DataRow("hooks", "Hooks")] [DataRow("data-tables", "Data Tables")] + [DataRow("hooks", "Hooks")] [DataRow("parameter-types", "Parameter Types")] - [DataRow("skipped", "Skipping scenarios")] [DataRow("undefined", "Undefined steps")] - [DataRow("unknown-parameter-type", "Unknown Parameter Types")] [DataRow("stack-traces", "Stack traces")] [DataRow("rules", "Usage of a `Rule`")] + // These CCK scenario examples produce Cucumber Messages that are materially compliant with the CCK. + // The messages produced match the CCK expected messages, with exceptions for things + // that are not material to the CCK spec (such as IDs don't have to be generated in the same order, timestamps don't have to match, etc.) + // The rules for what must match and what is allowed to not match are built in to a series of custom FluentAssertion validation rules + // (located in the CucumberMessagesValidator class) public void CCKScenarios(string testName, string featureNameText) { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); @@ -255,7 +267,6 @@ public void CCKScenarios(string testName, string featureNameText) validator.ResultShouldPassAllComparisonTests(); validator.ResultShouldPassSanityChecks(); - // This is necessary b/c the System Test framework doesn't understand Rules and can't determine the number of expected tests ConfirmAllTestsRan(testName == "rules" ? 3 : null); } @@ -263,11 +274,23 @@ public void CCKScenarios(string testName, string featureNameText) [TestMethod] [DataRow("ambiguous", "ambiguous")] [DataRow("background", "background")] + // These tests are not (yet) within the CCK but are included here to round out the testing. The expected results were generated by the CucumberMessages plugin. + // Once the CCK includes these scenarios, the expected results should come from the CCK repo. public void NonCCKScenarios(string testName, string featureNameText) { CCKScenarios(testName, featureNameText); } + [TestMethod] + [DataRow("attachments", "Attachments")] + [DataRow("skipped", "Skipping scenarios")] + [DataRow("unknown-parameter-type", "Unknown Parameter Types")] + // These scenarios are from the CCK, but Reqnroll cannot provide a compliant implementation. This is usually the result of differences in behavior or support of Gherkin features. + // When these scenarios are run, expect them to fail. + public void NonCompliantCCKScenarios(string testName, string featureNameText) + { + CCKScenarios(testName, featureNameText); + } private void AddUtilClassWithFileSystemPath() { @@ -291,43 +314,17 @@ private IEnumerable GetExpectedResults(string testName, string feature private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; - var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; + + var resultLocation = config!.ConfiguredOutputDirectory(null); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario - var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{fileName}.ndjson")); + var actualJsonText = File.ReadAllLines(Path.Combine(resultLocation, $"{fileName}.ndjson")); foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } } - internal class FileSinkConfiguration - { - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } } \ No newline at end of file From d10accb586038d4dd749980b4d6ae201b82a5927 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:58:02 -0500 Subject: [PATCH 070/218] FileSink now waits to register as a Sink until it is confirmed that it is Enabled. --- Reqnroll/CucumberMessages/CucumberMessageBroker.cs | 2 +- Reqnroll/CucumberMessages/FileSinkPlugin.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs index 7b4804104..5d25eff32 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs @@ -23,7 +23,7 @@ public class CucumberMessageBroker : ICucumberMessageBroker { private IObjectContainer _objectContainer; - public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; + public bool Enabled => RegisteredSinks.Value.ToList().Count > 0; private Lazy> RegisteredSinks; diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs index 75b9e016a..d8da19f4f 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -34,22 +34,20 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private string baseDirectory = ""; private Lazy? traceListener; private ITraceListener? trace => traceListener?.Value; - private IObjectContainer? objectContainer; + private IObjectContainer? testThreadObjectContainer; public FileSinkPlugin() { - traceListener = new Lazy(() => objectContainer!.Resolve()); + traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - objectContainer = args.ObjectContainer; + testThreadObjectContainer = args.ObjectContainer; testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; @@ -83,6 +81,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + testThreadObjectContainer!.RegisterInstanceAs(this); } public void Publish(ReqnrollCucumberMessage message) From ce8376ec29c50686eaf7ef55be1917a83dcd8e14 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:06:07 -0500 Subject: [PATCH 071/218] Removed local nuget location from nuget.config --- Reqnroll/Reqnroll.csproj | 2 +- nuget.config | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 3071a72fc..ab13e29f0 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,6 @@ - @@ -27,6 +26,7 @@ + diff --git a/nuget.config b/nuget.config index c8c559d96..6076bdca3 100644 --- a/nuget.config +++ b/nuget.config @@ -4,6 +4,5 @@ - \ No newline at end of file From b005f26943b22799abe4ab429f1395d90592a063 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:03:09 -0500 Subject: [PATCH 072/218] Refactored configuration to use Profiles. Fixed regressions in RuntimeTests.AnalyticsProviderTests --- .../CucumberMessages/CucumberConfiguration.cs | 70 ++++++++++++++++ .../CucumberMessages-config-schema.json | 14 ++-- .../CucumberOutputConfiguration.cs | 40 ++++++++++ ...{FileSinkPlugin.cs => FileOutputPlugin.cs} | 49 +++++++----- .../CucumberMessages/FileSinkConfiguration.cs | 80 ------------------- .../EnvironmentInfoProvider.cs | 9 ++- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 1 + .../CucumberCompatibilityTests.cs | 11 ++- .../CucumberMessages.configuration.json | 4 +- .../Analytics/AnalyticsEventProviderTests.cs | 28 +++---- .../ErrorProviderTests.cs | 2 +- .../ReqnrollOutputHelperTests.cs | 4 + 13 files changed, 182 insertions(+), 132 deletions(-) create mode 100644 Reqnroll/CucumberMessages/CucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs rename Reqnroll/CucumberMessages/{FileSinkPlugin.cs => FileOutputPlugin.cs} (68%) delete mode 100644 Reqnroll/CucumberMessages/FileSinkConfiguration.cs diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs new file mode 100644 index 000000000..b7d36932e --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -0,0 +1,70 @@ +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Tracing; +using System; +using System.IO; +using System.Linq; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class CucumberConfiguration + { + internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; + internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + private ITraceListener _trace; + private IEnvironmentWrapper _environmentWrapper; + private object _lock = new(); + + private CucumberOutputConfiguration outputConfiguration; + public bool Enabled => outputConfiguration != null ? outputConfiguration.FileOutputEnabled : false; + + public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) + { + _trace = traceListener; + _environmentWrapper = environmentWrapper; + } + + public string ConfigureOutputDirectory(CucumberOutputConfiguration config) + { + outputConfiguration = config; + string outputDirectory; + string configuredOutputDirectory = string.Empty; + string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); + var outDirValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var profileValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); + string environmentVariableOutputDirectory = outDirValue is Success ? ((Success)outDirValue).Result : null; + string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + + var activeConfiguredDestination = config.Destinations.Where(d => d.ProfileName == profileName).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); + } + + outputDirectory = defaultOutputDirectory; + if (!String.IsNullOrEmpty(configuredOutputDirectory)) + outputDirectory = configuredOutputDirectory; + if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) + outputDirectory = environmentVariableOutputDirectory; + + string logEntry; + if (outputDirectory == defaultOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; + else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; + else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; + + _trace?.WriteTestOutput(logEntry); + if (!Directory.Exists(outputDirectory)) + { + lock (_lock) + { + if (!Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + } + } + return outputDirectory; + } + + } +} + diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json index f7fcd8f17..b6f6b7919 100644 --- a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json +++ b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "filSinkEnabled": { + "fileOutputEnabled": { "type": "boolean", "description": "Indicates whether the program is enabled or not" }, @@ -11,9 +11,9 @@ "items": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "description": "Indicates whether this destination setting is enabled" + "profileName": { + "type": "string", + "description": "Provides a description for this destination (ex: 'dev' or 'prod')" }, "basePath": { "type": "string", @@ -21,14 +21,14 @@ }, "outputDirectory": { "type": "string", - "description": "The directory path where output should go" + "description": "The subdirectory of the base path where output should go" } }, - "required": [ "enabled", "basePath", "outputDirectory" ] + "required": [ "profileName", "basePath", "outputDirectory" ] }, "minItems": 1, "description": "Array of destination settings" } }, - "required": [ "programEnabled", "destinations" ] + "required": [ "fileOutputEnabled", "destinations" ] } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs new file mode 100644 index 000000000..e6c9499e3 --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs @@ -0,0 +1,40 @@ +using Reqnroll.Time; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class CucumberOutputConfiguration + { + + public bool FileOutputEnabled { get; set; } + public List Destinations { get; set; } + + public CucumberOutputConfiguration() : this(true) { } + public CucumberOutputConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public CucumberOutputConfiguration(bool fileSinkEnabled, List destinations) + { + FileOutputEnabled = fileSinkEnabled; + Destinations = destinations; + } + + } + + public class Destination + { + public string ProfileName { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(string profileName, string basePath, string outputDirectory) + { + ProfileName = String.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs similarity index 68% rename from Reqnroll/CucumberMessages/FileSinkPlugin.cs rename to Reqnroll/CucumberMessages/FileOutputPlugin.cs index d8da19f4f..5a025dc8b 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -14,13 +14,15 @@ using System.Threading.Tasks; using System.IO; using System.Linq; +using Reqnroll.EnvironmentAccess; +using Reqnroll.CommonModels; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin + public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; private object _lock = new(); @@ -30,13 +32,12 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); - private FileSinkConfiguration? configuration; private string baseDirectory = ""; private Lazy? traceListener; private ITraceListener? trace => traceListener?.Value; private IObjectContainer? testThreadObjectContainer; - public FileSinkPlugin() + public FileOutputPlugin() { traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); } @@ -54,7 +55,7 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar } private void CloseFileSink(TestRunFinishedEvent @event) { - trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); + trace?.WriteTestOutput("FileOutputPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; @@ -62,24 +63,32 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - bool environmentEnabled = "true".Equals(Environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE), StringComparison.InvariantCultureIgnoreCase); - bool environmentLocationSpecified = !String.IsNullOrEmpty(FileSinkConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var environment = testThreadObjectContainer!.Resolve(); + var enabledResult = environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "false"; + bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + + bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; + + CucumberOutputConfiguration configurationDTO; if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; } else if (environmentEnabled && environmentLocationSpecified) - configuration = new FileSinkConfiguration(true); - else configuration = new FileSinkConfiguration(false); - if (!configuration.FileSinkEnabled) + configurationDTO = new CucumberOutputConfiguration(true); + else configurationDTO = new CucumberOutputConfiguration(false); + var configuration = new CucumberConfiguration(trace, environment); + + if (!configurationDTO.FileOutputEnabled) { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace?.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } - baseDirectory = configuration.ConfiguredOutputDirectory(trace); + baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); testThreadObjectContainer!.RegisterInstanceAs(this); } @@ -87,7 +96,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) public void Publish(ReqnrollCucumberMessage message) { var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -100,12 +109,12 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } @@ -130,18 +139,18 @@ private async Task Write(string featureName, string cucumberMessage) } } } - trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); await fileStreams[featureName].WriteLineAsync(cucumberMessage); } catch (System.Exception ex) { - trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Exception: {ex.Message}"); } } private void CloseFeatureStream(string featureName) { - trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); + trace?.WriteTestOutput($"FileOutputPlugin CloseFeatureStream. Closing: {featureName}."); fileStreams[featureName].Close(); fileStreams.TryRemove(featureName, out var _); } diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs deleted file mode 100644 index 65e5d61c2..000000000 --- a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Reqnroll.Time; -using Reqnroll.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class FileSinkConfiguration - { - internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; - private object _lock = new(); - - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - public string ConfiguredOutputDirectory(ITraceListener trace) - { - string outputDirectory; - string configuredOutputDirectory = string.Empty; - string defaultOutputDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string environmentVariableOutputDirectory = Environment.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); - var activeConfiguredDestination = Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeConfiguredDestination != null) - { - configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); - } - - outputDirectory = defaultOutputDirectory; - if (!String.IsNullOrEmpty(configuredOutputDirectory)) - outputDirectory = configuredOutputDirectory; - if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) - outputDirectory = environmentVariableOutputDirectory; - - string logEntry; - if (outputDirectory == defaultOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; - else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; - else logEntry = $"FileSinkPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - - trace?.WriteTestOutput(logEntry); - if (!Directory.Exists(outputDirectory)) - { - lock (_lock) - { - if (!Directory.Exists(outputDirectory)) - Directory.CreateDirectory(outputDirectory); - } - } - return outputDirectory; - } - - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs index 6a9a1cc50..417f7eb27 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -9,11 +9,11 @@ namespace Reqnroll.EnvironmentAccess { public class EnvironmentInfoProvider : IEnvironmentInfoProvider { - private readonly IEnvironmentWrapper environmentWrapper; + private IEnvironmentWrapper EnvironmentWrapper { get; set; } public EnvironmentInfoProvider(IEnvironmentWrapper environmentWrapper) { - this.environmentWrapper = environmentWrapper; + EnvironmentWrapper = environmentWrapper; } public string GetOSPlatform() @@ -60,11 +60,12 @@ private readonly Dictionary buildServerTypes { "CI_NAME", "CodeShip" } }; + public string GetBuildServerName() { foreach (var buildServerType in buildServerTypes) { - var envVariable = environmentWrapper.GetEnvironmentVariable(buildServerType.Key); + var envVariable = EnvironmentWrapper.GetEnvironmentVariable(buildServerType.Key); if (envVariable is ISuccess) return buildServerType.Value; } @@ -73,7 +74,7 @@ public string GetBuildServerName() public bool IsRunningInDockerContainer() { - return environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + return EnvironmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; } public string GetReqnrollVersion() diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 93c80f693..0ef2feab0 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -105,7 +105,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); - container.RegisterTypeAs("FileSinkPlugin"); + container.RegisterTypeAs("FileOutputPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index a9c7cac2c..956126f96 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -19,6 +19,7 @@ protected override void TestCleanup() protected void EnableCucumberMessages() { Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE", "LOCAL"); } protected void DisableCucumberMessages() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index a0465bd33..91e055d1d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -12,6 +12,10 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Reqnroll.TestProjectGenerator.Driver; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Moq; +using Reqnroll.Tracing; +using Reqnroll.EnvironmentAccess; +using SpecFlow.Internal.Json; namespace CucumberMessages.CompatibilityTests { @@ -315,9 +319,12 @@ private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var resultLocation = config!.ConfiguredOutputDirectory(null); + var tracerMock = new Mock(); + var env = new EnvironmentWrapper(); + CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); + var resultLocation = configuration.ConfigureOutputDirectory(config); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index cb6aa6ecb..7b600acc6 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -1,8 +1,8 @@ { - "fileSinkEnabled": true, + "fileOutputEnabled": true, "destinations": [ { - "enabled": true, + "profileName": "DEV", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", "outputDirectory": "CucumberMessages" } diff --git a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs index 33e26f385..a16aa82dc 100644 --- a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs @@ -19,15 +19,14 @@ public class AnalyticsEventProviderTests public void Should_return_the_build_server_name_in_Compiling_Event() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); - - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); - - environmentMock + var environmentWrapperMock = new Mock(); + environmentWrapperMock .Setup(m => m.GetEnvironmentVariable("TF_BUILD")) .Returns(new Success("true")); + var environmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoProvider); + var compilingEvent = sut.CreateProjectCompilingEvent(null, null, null, null, null); compilingEvent.BuildServerName.Should().Be("Azure Pipelines"); @@ -37,14 +36,13 @@ public void Should_return_the_build_server_name_in_Compiling_Event() public void Should_return_the_build_server_name_in_Running_Event() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); - - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); - - environmentMock + var environmentWrapperMock = new Mock(); + environmentWrapperMock .Setup(m => m.GetEnvironmentVariable("TEAMCITY_VERSION")) .Returns(new Success("true")); + var environmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoProvider); var compilingEvent = sut.CreateProjectRunningEvent(null); @@ -55,10 +53,10 @@ public void Should_return_the_build_server_name_in_Running_Event() public void Should_return_null_for_the_build_server_name_when_not_detected() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); + var environmentWrapperMock = new Mock(); + var evironmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), evironmentInfoProvider); var compilingEvent = sut.CreateProjectRunningEvent(null); diff --git a/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs b/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs index 85cd63537..616ddb037 100644 --- a/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs @@ -131,7 +131,7 @@ private void GetMatchErrorMethod_should_return_BindingException_containing_full_ var result = GetMatchErrorFunc(errorProvider, bindingMatch, null); result.Should().NotBeNull(); - result.Should().BeOfType(); + result.Should().BeAssignableTo(); result.Message.Should().Be($"{expectedPrefixMessage} '{stepInstanceDescription}': {methodBindingAssemblyName}:{method1BindingTypeFullName}.{methodName}({parameter1Type}), {methodBindingAssemblyName}:{method2BindingTypeFullName}.{methodName}({parameter1Type})"); } diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 8bbc7d653..80993d9fd 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -38,6 +38,10 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); var contextManager = new Mock(); + var featureContext = new Mock(); + var featureInfo = new FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "test feature", null); + featureContext.SetupGet(fc => fc.FeatureInfo).Returns(featureInfo); + contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext.Object as FeatureContext); return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } From 330be60cb7ee976f0ceeeacf3860917a07a50a7a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:01:02 -0500 Subject: [PATCH 073/218] Fixed ReqnrollOutputHelperTest --- Reqnroll/CucumberMessages/CucumberConfiguration.cs | 2 ++ Reqnroll/CucumberMessages/FileOutputPlugin.cs | 5 ++--- .../Infrastructure/ReqnrollOutputHelperTests.cs | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs index b7d36932e..9af3bd1e6 100644 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -11,6 +11,8 @@ public class CucumberConfiguration { internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; + private ITraceListener _trace; private IEnvironmentWrapper _environmentWrapper; private object _lock = new(); diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 5a025dc8b..047e82fbf 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -23,7 +23,6 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; - private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; private object _lock = new(); @@ -64,8 +63,8 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { var environment = testThreadObjectContainer!.Resolve(); - var enabledResult = environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "false"; + var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 80993d9fd..6dbd00b1d 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -1,7 +1,9 @@ using Moq; +using Reqnroll.Configuration; using Reqnroll.Events; using Reqnroll.Infrastructure; using Reqnroll.Tracing; +using System; using Xunit; namespace Reqnroll.RuntimeTests.Infrastructure @@ -38,10 +40,10 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); var contextManager = new Mock(); - var featureContext = new Mock(); var featureInfo = new FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "test feature", null); - featureContext.SetupGet(fc => fc.FeatureInfo).Returns(featureInfo); - contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext.Object as FeatureContext); + var config = new ReqnrollConfiguration(ConfigSource.Json, null, null, null, null, false, MissingOrPendingStepsOutcome.Error, false, false, TimeSpan.FromSeconds(10), Reqnroll.BindingSkeletons.StepDefinitionSkeletonStyle.CucumberExpressionAttribute, null, false, false, new string[] { }, ObsoleteBehavior.Error, false); + var featureContext = new FeatureContext(null, featureInfo, config); + contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext); return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } From feddbeffc64a09e1569d753836309bb4c5db010f Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:16:30 -0500 Subject: [PATCH 074/218] Simplified File Plug In handling of configuration. Fixed a defect that threw an exception when Source document is not available; not a critical error, so changed it to allow it to proceed with Pickle creation. Revised tests to delete previous run .ndjson files. --- Reqnroll.Parser/CucumberMessagesConverter.cs | 4 +- .../CucumberMessagePublisher.cs | 21 +++++----- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 28 ++++++++----- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 19 +++++++++ .../CucumberCompatibilityTests.cs | 40 ++++++++++++------- .../CucumberMessages.configuration.json | 2 +- 7 files changed, 78 insertions(+), 38 deletions(-) diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 9369631cd..808c34bf4 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -24,7 +24,9 @@ public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) { - var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + string sourceText = $"Source Document: {gherkinDocument.SourceFilePath} not found."; + if (File.Exists(gherkinDocument.SourceFilePath)) + sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); return new Source { Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 4ea98657f..9a3baabc4 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -13,16 +13,16 @@ namespace Reqnroll.CucumberMessages { public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { - private ICucumberMessageBroker broker; + private Lazy _brokerFactory; + private ICucumberMessageBroker _broker; private IObjectContainer objectContainer; private ConcurrentDictionary StartedFeatures = new(); private ConcurrentDictionary testCaseTrackersById = new(); bool Enabled = false; - public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) + public CucumberMessagePublisher() { - //Debugger.Launch(); - broker = CucumberMessageBroker; + _brokerFactory = new Lazy(() => objectContainer.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { @@ -50,13 +50,14 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { + _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; // if not, then this is disabled for this feature - // if true, then it checks with the broker to confirm that a listener/sink has been registered - Enabled = broker.Enabled; + // if true, then it checks with the _broker to confirm that a listener/sink has been registered + Enabled = _broker.Enabled; if (!Enabled) { traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: Broker is disabled for {featureName}."); @@ -75,7 +76,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { foreach (var msg in ft.StaticMessages) { - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } @@ -96,8 +97,8 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve { var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - broker.Complete(featureName); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + _broker.Complete(featureName); } else { @@ -139,7 +140,7 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished foreach (var msg in tccmt.TestCaseCucumberMessages()) { - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); } } diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 047e82fbf..a8b6c7a2b 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -16,13 +16,14 @@ using System.Linq; using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; +using System.Diagnostics; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; private object _lock = new(); @@ -32,17 +33,24 @@ public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugi private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private string baseDirectory = ""; - private Lazy? traceListener; - private ITraceListener? trace => traceListener?.Value; + private Lazy traceListener; + private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; + private IObjectContainer? globalObjectContainer; + public FileOutputPlugin() { traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => + { + globalObjectContainer = args.ObjectContainer; + }; runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { @@ -65,19 +73,15 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) var environment = testThreadObjectContainer!.Resolve(); var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; - bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + bool isMessagesEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); - bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; - CucumberOutputConfiguration configurationDTO; if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; } - else if (environmentEnabled && environmentLocationSpecified) - configurationDTO = new CucumberOutputConfiguration(true); - else configurationDTO = new CucumberOutputConfiguration(false); - var configuration = new CucumberConfiguration(trace, environment); + else + configurationDTO = new CucumberOutputConfiguration(isMessagesEnabled); if (!configurationDTO.FileOutputEnabled) { @@ -85,11 +89,12 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) return; } + var configuration = new CucumberConfiguration(trace, environment); baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); - testThreadObjectContainer!.RegisterInstanceAs(this); + globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } public void Publish(ReqnrollCucumberMessage message) @@ -154,6 +159,7 @@ private void CloseFeatureStream(string featureName) fileStreams.TryRemove(featureName, out var _); } private bool disposedValue; + protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 0ef2feab0..21f481c22 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -103,9 +103,9 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); //Support for publishing Cucumber Messages + container.RegisterTypeAs("FileOutputPlugin"); container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); - container.RegisterTypeAs("FileOutputPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 956126f96..f97f78f78 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,10 +1,15 @@ using FluentAssertions; +using Moq; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.EnvironmentAccess; using Reqnroll.SystemTests; +using Reqnroll.Tracing; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace CucumberMessages.CompatibilityTests @@ -61,5 +66,19 @@ protected void CucumberMessagesAddConfigurationFile(string configFileName) var configFileContent = File.ReadAllText(configFileName); _projectsDriver.AddFile(configFileName, configFileContent); } + + protected static string ActualsResultLocationDirectory() + { + var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + var tracerMock = new Mock(); + var env = new EnvironmentWrapper(); + CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); + var resultLocation = configuration.ConfigureOutputDirectory(config); + return resultLocation; + } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 91e055d1d..6ef87429d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -16,16 +16,34 @@ using Reqnroll.Tracing; using Reqnroll.EnvironmentAccess; using SpecFlow.Internal.Json; +using Microsoft.VisualBasic.FileIO; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { - private void ResetCucumberMessages() + private void ResetCucumberMessages(string? fileToDelete = null) { DisableCucumberMessages(); + DeletePreviousMessagesOutput(fileToDelete); } + + private void DeletePreviousMessagesOutput(string? fileToDelete = null) + { + var directory = ActualsResultLocationDirectory(); + + if (fileToDelete != null) + { + var fileToDeletePath = Path.Combine(directory, fileToDelete); + + if (File.Exists(fileToDeletePath)) + { + File.Delete(fileToDeletePath); + } + } + } + [TestMethod] public void NullTest() { @@ -46,7 +64,7 @@ When I eat 5 cukes [TestMethod] public void SmokeTest() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -65,7 +83,7 @@ When I eat 5 cukes [TestMethod] public void SmokeOutlineTest() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Outline Test.ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -89,7 +107,7 @@ public void SmokeOutlineTest() [TestMethod] public void SmokeOutlineTestAsMethods() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Outline Test As Methods.ndjson"); var _configurationFileDriver = GetServiceSafe(); _configurationFileDriver.SetIsRowTestsAllowed(false); @@ -118,7 +136,7 @@ public void SmokeOutlineTestAsMethods() [TestMethod] public void CucumberMessagesInteropWithExternalData() { - ResetCucumberMessages(); + ResetCucumberMessages("External Data from CSV file.ndjson"); // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages EnableCucumberMessages(); _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); @@ -253,7 +271,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) // (located in the CucumberMessagesValidator class) public void CCKScenarios(string testName, string featureNameText) { - ResetCucumberMessages(); + ResetCucumberMessages(featureNameText+".ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); @@ -317,14 +335,7 @@ private IEnumerable GetExpectedResults(string testName, string feature private IEnumerable GetActualResults(string testName, string fileName) { - var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - var tracerMock = new Mock(); - var env = new EnvironmentWrapper(); - CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); - var resultLocation = configuration.ConfigureOutputDirectory(config); + string resultLocation = ActualsResultLocationDirectory(); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario @@ -332,6 +343,7 @@ private IEnumerable GetActualResults(string testName, string fileName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 7b600acc6..22b0b03c8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -2,7 +2,7 @@ "fileOutputEnabled": true, "destinations": [ { - "profileName": "DEV", + "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", "outputDirectory": "CucumberMessages" } From 6a97d9186b6e677f2cab8ee882b0ca19780a951a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:45:15 -0500 Subject: [PATCH 075/218] Modified Generator to globalize references to Reqnroll types related to Messages --- Reqnroll.Generator/Generation/ScenarioPartHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 4c852e9c0..959d3f4f6 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -35,7 +35,7 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) backgroundMethod.Attributes = MemberAttributes.Public; backgroundMethod.Name = GeneratorConstants.BACKGROUND_NAME; - backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); + backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); _codeDomHelper.MarkCodeMemberMethodAsAsync(backgroundMethod); @@ -247,7 +247,7 @@ public void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdInc public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod testMethod) { - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); testMethod.Statements.Add(pickleStepSequence); @@ -256,7 +256,7 @@ public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMethod) { // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, // Right side of the assignment (property access chain) new CodeMethodInvokeExpression( From 0fc4ae11c8a95db3b589d9d7a7229517ddb84ec0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 09:24:43 -0500 Subject: [PATCH 076/218] Updated GeneratorTests to include now required RnRDocumentLocation elements when testing generation. Fixed MissingLocation patcher to include Feature and Scenario locations. Enhanced Publisher to be more careful of handling of Feature-level enablement. --- .../Generation/UnitTestFeatureGenerator.cs | 3 + Reqnroll.Parser/CucumberMessagesConverter.cs | 25 +++++-- Reqnroll.Parser/GherkinDocumentVisitor.cs | 3 +- ...chMissingLocationElementsTransformation.cs | 25 ++++++- ...on.cs => ScenarioTransformationVisitor.cs} | 2 +- .../CucumberMessagePublisher.cs | 73 ++++++++++++------- Reqnroll/CucumberMessages/FeatureTracker.cs | 4 +- .../TestCaseCucumberMessageTracker.cs | 1 + .../CCK/background/background.feature | 8 ++ .../CustomTestGeneratorProviderTest.cs | 2 +- Tests/Reqnroll.GeneratorTests/ParserHelper.cs | 22 +++--- .../TestGeneratorTest.cs | 2 +- .../MsTestGeneratorProviderTests.cs | 2 +- .../MsTestV2GeneratorProviderTests.cs | 2 +- .../NUnit3GeneratorProviderTests.cs | 2 +- .../XUnit2TestGeneratorProviderTests.cs | 2 +- 16 files changed, 125 insertions(+), 53 deletions(-) rename Reqnroll.Parser/{ScenarioTransformation.cs => ScenarioTransformationVisitor.cs} (98%) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 1142bba12..08316e57a 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -1,6 +1,7 @@ using System; using System.CodeDom; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; @@ -47,6 +48,7 @@ public UnitTestFeatureGenerator( public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace) { + Debugger.Launch(); var codeNamespace = CreateNamespace(targetNamespace); var feature = document.ReqnrollFeature; @@ -227,6 +229,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte try { //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + //TODO: make the type of IDGenerator configurable var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 808c34bf4..38be9c877 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -24,15 +24,26 @@ public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) { - string sourceText = $"Source Document: {gherkinDocument.SourceFilePath} not found."; if (File.Exists(gherkinDocument.SourceFilePath)) - sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); - return new Source { - Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), - Data = sourceText, - MediaType = "text/x.cucumber.gherkin+plain" - }; + var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + return new Source + { + Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), + Data = sourceText, + MediaType = "text/x.cucumber.gherkin+plain" + }; + } + else + { + return new Source + { + Uri = "Unknown", + Data = $"Source Document: {gherkinDocument.SourceFilePath} not found.", + MediaType = "text/x.cucumber.gherkin+plain" + }; + + } } public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument) diff --git a/Reqnroll.Parser/GherkinDocumentVisitor.cs b/Reqnroll.Parser/GherkinDocumentVisitor.cs index 4be6eccaa..ed9e6eba1 100644 --- a/Reqnroll.Parser/GherkinDocumentVisitor.cs +++ b/Reqnroll.Parser/GherkinDocumentVisitor.cs @@ -68,7 +68,8 @@ protected virtual void AcceptRule(Rule rule) OnRuleVisiting(rule); foreach (var ruleChild in rule.Children) { - if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + if (ruleChild is Background background) AcceptBackground(background); + else if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); else if (ruleChild is Scenario scenario) AcceptScenario(scenario); } OnRuleVisited(rule); diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 87daa06ab..462cf37f4 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -6,11 +6,32 @@ namespace Reqnroll.Parser { - internal class PatchMissingLocationElementsTransformation : ScenarioTransformation + internal class PatchMissingLocationElementsTransformation : ScenarioTransformationVisitor { + protected override void OnFeatureVisited(Feature feature) + { + var patchedFeatureLocation = PatchLocation(feature.Location); + var patchedFeature = new Feature( + feature.Tags.ToArray(), + patchedFeatureLocation, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + feature.Children.ToArray()); + base.OnFeatureVisited(patchedFeature); + + } protected override Scenario GetTransformedScenario(Scenario scenario) { - return null; + return new Scenario( + scenario.Tags.ToArray(), + PatchLocation(scenario.Location), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.ToArray(), + scenario.Examples.ToArray()); } protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline) diff --git a/Reqnroll.Parser/ScenarioTransformation.cs b/Reqnroll.Parser/ScenarioTransformationVisitor.cs similarity index 98% rename from Reqnroll.Parser/ScenarioTransformation.cs rename to Reqnroll.Parser/ScenarioTransformationVisitor.cs index 627577d78..2d0d76fb8 100644 --- a/Reqnroll.Parser/ScenarioTransformation.cs +++ b/Reqnroll.Parser/ScenarioTransformationVisitor.cs @@ -5,7 +5,7 @@ namespace Reqnroll.Parser { - abstract class ScenarioTransformation : GherkinDocumentVisitor + abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor { protected ReqnrollDocument _sourceDocument; private ReqnrollDocument _transformedDocument; diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 9a3baabc4..03bcb55da 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -22,13 +22,14 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher() { - _brokerFactory = new Lazy(() => objectContainer.Resolve()); + Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; + _brokerFactory = new Lazy(() => objectContainer.Resolve()); var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); }; @@ -72,7 +73,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); var ft = new FeatureTracker(featureStartedEvent); - if (StartedFeatures.TryAdd(featureName, ft)) + if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) { foreach (var msg in ft.StaticMessages) { @@ -90,6 +91,11 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve return; } var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + if (!StartedFeatures.ContainsKey(featureName) || !StartedFeatures[featureName].Enabled) + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: {featureName} was not started or is Disabled."); + return; + } var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); // IF all TestCaseCucumberMessageTrackers are done, then send the messages to the CucumberMessageBroker @@ -115,13 +121,21 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve return; var traceListener = objectContainer.Resolve(); var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - var tccmt = new TestCaseCucumberMessageTracker(featureTracker); - traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); - testCaseTrackersById.TryAdd(id, tccmt); - tccmt.ProcessEvent(scenarioStartedEvent); + if (featureTracker.Enabled) + { + var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); + testCaseTrackersById.TryAdd(id, tccmt); + tccmt.ProcessEvent(scenarioStartedEvent); + } + else + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker is disabled"); + return; + } } else { @@ -134,13 +148,15 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished { if (!Enabled) return; - - var tccmt = testCaseTrackersById[scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(scenarioFinishedEvent); - - foreach (var msg in tccmt.TestCaseCucumberMessages()) + var testCaseTrackerId = scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) { - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + tccmt.ProcessEvent(scenarioFinishedEvent); + + foreach (var msg in tccmt.TestCaseCucumberMessages()) + { + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + } } } @@ -148,9 +164,9 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { if (!Enabled) return; - - var tccmt = testCaseTrackersById[stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(stepStartedEvent); + var testCaseTrackerId = stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(stepStartedEvent); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) @@ -158,8 +174,9 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (!Enabled) return; - var tccmt = testCaseTrackersById[stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(stepFinishedEvent); + var testCaseTrackerId = stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(stepFinishedEvent); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) @@ -167,8 +184,9 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - var tccmt = testCaseTrackersById[hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(hookBindingStartedEvent); + var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(hookBindingStartedEvent); } private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) @@ -176,8 +194,9 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - var tccmt = testCaseTrackersById[hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(hookBindingFinishedEvent); + var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(hookBindingFinishedEvent); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) @@ -185,8 +204,9 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (!Enabled) return; - var tccmt = testCaseTrackersById[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(attachmentAddedEvent); + var testCaseTrackerId = attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(attachmentAddedEvent); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) @@ -194,8 +214,9 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (!Enabled) return; - var tccmt = testCaseTrackersById[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(outputAddedEvent); + var testCaseTrackerId = outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(outputAddedEvent); } } } diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs index 18517816d..689e5715f 100644 --- a/Reqnroll/CucumberMessages/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -24,11 +24,13 @@ public class FeatureTracker public FeatureTracker(FeatureStartedEvent featureStartedEvent) { FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - Enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles == null ? false : true; + var featureHasCucumberMessages = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages != null; + Enabled = (featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null) ? true : false; PreProcessEvent(featureStartedEvent); } internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) { + if (!Enabled) return; // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow StaticMessages = GenerateStaticMessages(featureStartedEvent).ToList(); } diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs index faeb018d5..cb4ae4ae9 100644 --- a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -120,6 +120,7 @@ private void InvokePreProcessEvent(ExecutionEvent anEvent) public IEnumerable TestCaseCucumberMessages() { + if (!Enabled) return Enumerable.Empty(); // Stage 2 return _events.Select(e => InvokePostProcessEvent(e)).SelectMany(x => x); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature index 41424d2e9..3c9ca9e94 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature @@ -15,3 +15,11 @@ Scenario: Transfer Money When I transfer $150 from savings to checking Then My checking account has a balance of $650 And My savings account has a balance of $50 + +Rule: A rule with a background + Background: First Transfer Money + When I transfer $50 from savings to checking + Then My savings account has a balance of $150 + Scenario: total balance unchanged + When the accounts are combined + Then I have $700 diff --git a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs index 07c2fb8fb..dc61a6544 100644 --- a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs +++ b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs @@ -66,7 +66,7 @@ public void GenerateScenarioExampleTests() var parser = new ReqnrollGherkinParser(new CultureInfo("en-US")); using (var reader = new StringReader(SampleFeatureFile)) { - var feature = parser.Parse(reader, null); + var feature = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_location_for_{nameof(GenerateScenarioExampleTests)}")); feature.Should().NotBeNull(); var sampleTestGeneratorProvider = new SimpleTestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp)); diff --git a/Tests/Reqnroll.GeneratorTests/ParserHelper.cs b/Tests/Reqnroll.GeneratorTests/ParserHelper.cs index f3b6a5594..5ead50214 100644 --- a/Tests/Reqnroll.GeneratorTests/ParserHelper.cs +++ b/Tests/Reqnroll.GeneratorTests/ParserHelper.cs @@ -12,35 +12,39 @@ class ParserHelper { public static ReqnrollDocument CreateAnyDocument(string[] tags = null) { - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, null, null, null, null, null); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), null, null, null, null, null); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); } public static Tag[] GetTags(params string[] tags) { - return tags == null ? new Tag[0] : tags.Select(t => new Tag(null, t)).ToArray(); + return tags == null ? new Tag[0] : tags.Select(t => new Tag(new Location(0), t)).ToArray(); } public static ReqnrollDocument CreateDocument(string[] tags = null, string[] scenarioTags = null) { tags = tags ?? new string[0]; - var scenario1 = new Scenario(GetTags(scenarioTags), null, "Scenario", "scenario1 title", "", new Step[0], new Examples[0]); + var scenario1 = new Scenario(GetTags(scenarioTags), new Location(0), "Scenario", "scenario1 title", "", new Step[0], new Examples[0]); - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); } public static ReqnrollDocument CreateDocumentWithScenarioOutline(string[] tags = null, string[] scenarioOutlineTags = null, string[] examplesTags = null) { tags = tags ?? new string[0]; - var scenario1 = new ScenarioOutline(GetTags(scenarioOutlineTags), null, "Scenario Outline", "scenario outline1 title", "", new Step[0], new [] + var scenario1 = new ScenarioOutline(GetTags(scenarioOutlineTags), new Location(0), "Scenario Outline", "scenario outline1 title", "", new Step[0], new [] { new Examples(GetTags(examplesTags), null, "Examples", "examples name", "", new Gherkin.Ast.TableRow(null, new []{ new TableCell(null, "col1"), }), new Gherkin.Ast.TableRow[0]) }); - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); + } + private static ReqnrollDocumentLocation CreateDummyReqnrollLocation() + { + return new ReqnrollDocumentLocation("dummy_location"); } } } diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs index f95c3183f..5b87d2327 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs @@ -63,7 +63,7 @@ public void GenerateScenarioExampleTests() var parser = new ReqnrollGherkinParser(new CultureInfo("en-US")); using (var reader = new StringReader(SampleFeatureFile)) { - var feature = parser.Parse(reader, null); + var feature = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_location_for_{nameof(GenerateScenarioExampleTests)}")); feature.Should().NotBeNull(); var sampleTestGeneratorProvider = new SimpleTestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp)); diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs index 55ee1b379..83acb6449 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs @@ -276,7 +276,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_Reqnroll_Location_for{nameof(MsTestGeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs index 192190bdf..9446ef88c 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs @@ -238,7 +238,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_Location_for_{nameof(MsTestV2GeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs index dce0efc83..f44957d38 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs @@ -349,7 +349,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_ReqnrollLocation_for_{nameof(NUnit3GeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs index d716902bc..9b707ad97 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs @@ -453,7 +453,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_ReqnrollLocation_for_{nameof(XUnit2TestGeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } From 55154cffa61d5be1580591408114fd993cf72be6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:58:12 -0500 Subject: [PATCH 077/218] Fixed bug in LocationPatcher that was obliterating Rule Backgrounds. Added Location fixup to Tags, Features and Scenario elements to better support testability when tests set them as null as a convenience when setting up test data. Added smoke test to include more than one feature per test. --- .../Generation/UnitTestFeatureGenerator.cs | 1 - ...chMissingLocationElementsTransformation.cs | 16 ++-- .../ScenarioTransformationVisitor.cs | 2 +- .../CucumberMessages/CucumberConfiguration.cs | 2 +- .../CucumberMessagePublisher.cs | 4 +- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 40 +++++++-- .../TestCaseCucumberMessageTracker.cs | 4 +- .../CCK/background/background.feature.ndjson | 88 +++++++++++-------- .../CucumberCompatibilityTests.cs | 25 ++++++ ...CucumberMessages.CompatibilityTests.csproj | 4 +- 10 files changed, 127 insertions(+), 59 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 08316e57a..4b13a7f2c 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -48,7 +48,6 @@ public UnitTestFeatureGenerator( public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace) { - Debugger.Launch(); var codeNamespace = CreateNamespace(targetNamespace); var feature = document.ReqnrollFeature; diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 462cf37f4..2569928a0 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -12,7 +12,7 @@ protected override void OnFeatureVisited(Feature feature) { var patchedFeatureLocation = PatchLocation(feature.Location); var patchedFeature = new Feature( - feature.Tags.ToArray(), + feature.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), patchedFeatureLocation, feature.Language, feature.Keyword, @@ -25,12 +25,12 @@ protected override void OnFeatureVisited(Feature feature) protected override Scenario GetTransformedScenario(Scenario scenario) { return new Scenario( - scenario.Tags.ToArray(), + scenario.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(scenario.Location), scenario.Keyword, scenario.Name, scenario.Description, - scenario.Steps.ToArray(), + scenario.Steps.Select( s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), scenario.Examples.ToArray()); } @@ -42,23 +42,23 @@ protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenar var exampleTables = scenarioOutline.Examples; List transformedExamples = new List(); - transformedExamples.AddRange(exampleTables.Select(e => PatchLocations(e))); + transformedExamples.AddRange(exampleTables.Select(e => PatchExamplesLocations(e))); return new ScenarioOutline( - scenarioOutline.Tags.ToArray(), + scenarioOutline.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(scenarioOutline.Location), scenarioOutline.Keyword, scenarioOutline.Name, scenarioOutline.Description, - scenarioOutline.Steps.ToArray(), + scenarioOutline.Steps.Select(s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), transformedExamples.ToArray()); } - private Examples PatchLocations(Examples e) + private Examples PatchExamplesLocations(Examples e) { var headerCells = e.TableHeader.Cells; var tableHeader = new Gherkin.Ast.TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new Gherkin.Ast.TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); var rows = e.TableBody.Select(r => new Gherkin.Ast.TableRow(PatchLocation(r.Location), r.Cells.Select(c => new Gherkin.Ast.TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); - return new Examples(e.Tags.ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); + return new Examples(e.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); } private static Location PatchLocation(Location l) diff --git a/Reqnroll.Parser/ScenarioTransformationVisitor.cs b/Reqnroll.Parser/ScenarioTransformationVisitor.cs index 2d0d76fb8..621732af3 100644 --- a/Reqnroll.Parser/ScenarioTransformationVisitor.cs +++ b/Reqnroll.Parser/ScenarioTransformationVisitor.cs @@ -65,7 +65,7 @@ private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedSc protected override void OnBackgroundVisited(Background background) { - _featureChildren.Add(background); + _currentChildren.Add(background); } protected override void OnRuleVisiting(Rule rule) diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs index 9af3bd1e6..2c7722ffe 100644 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -55,7 +55,7 @@ public string ConfigureOutputDirectory(CucumberOutputConfiguration config) else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - _trace?.WriteTestOutput(logEntry); + _trace!.WriteTestOutput(logEntry); if (!Directory.Exists(outputDirectory)) { lock (_lock) diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 03bcb55da..d927d8961 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -22,7 +22,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher() { - Debugger.Launch(); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { @@ -127,9 +127,9 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve { var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + tccmt.ProcessEvent(scenarioStartedEvent); traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); testCaseTrackersById.TryAdd(id, tccmt); - tccmt.ProcessEvent(scenarioStartedEvent); } else { diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index a8b6c7a2b..9c066f390 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -85,13 +85,13 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) if (!configurationDTO.FileOutputEnabled) { - trace?.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace!.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } var configuration = new CucumberConfiguration(trace, environment); baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - + trace!.WriteTestOutput($"FileOutputPlugin LaunchFileSink. Cucumber Messages is ENABLED. Base Directory: {baseDirectory}"); trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); @@ -100,7 +100,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) public void Publish(ReqnrollCucumberMessage message) { var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + //trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -113,12 +113,12 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { - trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } @@ -139,11 +139,11 @@ private async Task Write(string featureName, string cucumberMessage) { if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, SanitizeFileName($"{featureName}.ndjson"))); } } } - trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {SanitizeFileName($"{featureName}.ndjson")}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); await fileStreams[featureName].WriteLineAsync(cucumberMessage); } catch (System.Exception ex) @@ -186,5 +186,31 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + public static string SanitizeFileName(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Get the invalid characters for file names + char[] invalidChars = Path.GetInvalidFileNameChars(); + + // Replace invalid characters with underscores + string sanitized = new string(input.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray()); + + // Remove leading and trailing spaces and dots + sanitized = sanitized.Trim().Trim('.'); + + // Ensure the filename is not empty after sanitization + if (string.IsNullOrEmpty(sanitized)) + return "_"; + + // Truncate the filename if it's too long (255 characters is a common limit) + const int maxLength = 255; + if (sanitized.Length > maxLength) + sanitized = sanitized.Substring(0, maxLength); + + return sanitized; + } + } } diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs index cb4ae4ae9..ec9da8b6c 100644 --- a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -46,8 +46,8 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } - public Dictionary StepsById { get; private set; } = new(); - public Dictionary StepsByEvent { get; private set; } = new(); + private Dictionary StepsById { get; set; } = new(); + private Dictionary StepsByEvent { get; set; } = new(); public List Steps { get diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson index f44c8ac17..cfaced93a 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson @@ -1,36 +1,52 @@ -{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+86e9495e4e6815ab377bdbc6ea2eebd6f89a0537"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} -{"source":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} -{"gherkinDocument":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} -{"pickle":{"id":"14","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"10","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"11","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"12","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"13","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} -{"pickle":{"id":"20","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"15","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"16","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"17","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"18","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"19","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} -{"stepDefinition":{"id":"21","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} -{"stepDefinition":{"id":"22","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} -{"stepDefinition":{"id":"23","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} -{"stepDefinition":{"id":"24","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} -{"stepDefinition":{"id":"25","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} -{"testRunStarted":{"timestamp":{"seconds":1727192229,"nanos":420947400}}} -{"testCase":{"id":"26","pickleId":"14","testSteps":[{"id":"28","pickleStepId":"10","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"11","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"26","timestamp":{"seconds":1727192229,"nanos":452538900}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"28","timestamp":{"seconds":1727192229,"nanos":455045500}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"28","testStepResult":{"duration":{"seconds":0,"nanos":19933400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":474978900}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"29","timestamp":{"seconds":1727192229,"nanos":476934500}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"29","testStepResult":{"duration":{"seconds":0,"nanos":698600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":477633100}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"30","timestamp":{"seconds":1727192229,"nanos":478014600}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"30","testStepResult":{"duration":{"seconds":0,"nanos":1699400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":479714000}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"31","timestamp":{"seconds":1727192229,"nanos":479992900}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"31","testStepResult":{"duration":{"seconds":0,"nanos":551600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":480544500}}} -{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"seconds":1727192229,"nanos":482528500},"willBeRetried":false}} -{"testCase":{"id":"32","pickleId":"20","testSteps":[{"id":"34","pickleStepId":"15","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"17","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"18","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"38","pickleStepId":"19","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"33","testCaseId":"32","timestamp":{"seconds":1727192229,"nanos":495596400}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"34","timestamp":{"seconds":1727192229,"nanos":495781000}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"34","testStepResult":{"duration":{"seconds":0,"nanos":278500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496059500}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"35","timestamp":{"seconds":1727192229,"nanos":496095600}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"35","testStepResult":{"duration":{"seconds":0,"nanos":83500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496179100}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"36","timestamp":{"seconds":1727192229,"nanos":496202200}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"36","testStepResult":{"duration":{"seconds":0,"nanos":685000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496887200}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"37","timestamp":{"seconds":1727192229,"nanos":496910500}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"37","testStepResult":{"duration":{"seconds":0,"nanos":512200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497422700}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"38","timestamp":{"seconds":1727192229,"nanos":497441700}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"38","testStepResult":{"duration":{"seconds":0,"nanos":194200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497635900}}} -{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":1727192229,"nanos":497750600},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"seconds":1727192229,"nanos":499781200}}} +{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+098e7ef7a96ecab08f239f15463aec6b9098c778"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n\r\nRule: A rule with a background\r\n\tBackground: First Transfer Money\r\n\t\tWhen I transfer $50 from savings to checking\r\n\t\tThen My savings account has a balance of $150\r\n\tScenario: total balance unchanged\r\n\t\tWhen the accounts are combined\r\n\t\tThen I have $700\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}},{"rule":{"location":{"line":19,"column":1},"tags":[],"keyword":"Rule","name":"A rule with a background","description":"","children":[{"background":{"location":{"line":20,"column":2},"keyword":"Background","name":"First Transfer Money","description":"","steps":[{"location":{"line":21,"column":3},"keyword":"When ","keywordType":"Action","text":"I transfer $50 from savings to checking","id":"10"},{"location":{"line":22,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"My savings account has a balance of $150","id":"11"}],"id":"12"}},{"scenario":{"location":{"line":23,"column":2},"tags":[],"keyword":"Scenario","name":"total balance unchanged","description":"","steps":[{"location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"13"},{"location":{"line":25,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"14"}],"examples":[],"id":"15"}}],"id":"16"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} +{"pickle":{"id":"21","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"17","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"18","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"19","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"20","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} +{"pickle":{"id":"27","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"22","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"23","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"24","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"25","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"26","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} +{"pickle":{"id":"34","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"total balance unchanged","language":"en-US","steps":[{"astNodeIds":["0"],"id":"28","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"29","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["10"],"id":"30","type":"Action","text":"I transfer $50 from savings to checking"},{"astNodeIds":["11"],"id":"31","type":"Outcome","text":"My savings account has a balance of $150"},{"astNodeIds":["13"],"id":"32","type":"Action","text":"the accounts are combined"},{"astNodeIds":["14"],"id":"33","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["15"]}} +{"stepDefinition":{"id":"35","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} +{"stepDefinition":{"id":"36","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"37","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} +{"stepDefinition":{"id":"38","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} +{"stepDefinition":{"id":"39","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} +{"testRunStarted":{"timestamp":{"seconds":1727518980,"nanos":932282100}}} +{"testCase":{"id":"40","pickleId":"21","testSteps":[{"id":"42","pickleStepId":"17","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"43","pickleStepId":"18","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"44","pickleStepId":"19","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"45","pickleStepId":"20","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"41","testCaseId":"40","timestamp":{"seconds":1727518980,"nanos":988751100}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"42","timestamp":{"seconds":1727518980,"nanos":991631700}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"42","testStepResult":{"duration":{"seconds":0,"nanos":31313600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":22945300}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"43","timestamp":{"seconds":1727518981,"nanos":25132700}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"43","testStepResult":{"duration":{"seconds":0,"nanos":856400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":25989100}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"44","timestamp":{"seconds":1727518981,"nanos":26528500}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"44","testStepResult":{"duration":{"seconds":0,"nanos":2514700},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":29043200}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"45","timestamp":{"seconds":1727518981,"nanos":29468400}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"45","testStepResult":{"duration":{"seconds":0,"nanos":728600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":30197000}}} +{"testCaseFinished":{"testCaseStartedId":"41","timestamp":{"seconds":1727518981,"nanos":33150200},"willBeRetried":false}} +{"testCase":{"id":"46","pickleId":"27","testSteps":[{"id":"48","pickleStepId":"22","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"49","pickleStepId":"23","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"50","pickleStepId":"24","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"51","pickleStepId":"25","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"26","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"47","testCaseId":"46","timestamp":{"seconds":1727518981,"nanos":75179500}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"48","timestamp":{"seconds":1727518981,"nanos":75361800}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"48","testStepResult":{"duration":{"seconds":0,"nanos":378000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75739800}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"49","timestamp":{"seconds":1727518981,"nanos":75791400}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"49","testStepResult":{"duration":{"seconds":0,"nanos":133800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75925200}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"50","timestamp":{"seconds":1727518981,"nanos":75964700}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"50","testStepResult":{"duration":{"seconds":0,"nanos":986400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":76951100}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"51","timestamp":{"seconds":1727518981,"nanos":76991800}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"51","testStepResult":{"duration":{"seconds":0,"nanos":739400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":77731200}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"52","timestamp":{"seconds":1727518981,"nanos":77758700}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"52","testStepResult":{"duration":{"seconds":0,"nanos":283000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":78041700}}} +{"testCaseFinished":{"testCaseStartedId":"47","timestamp":{"seconds":1727518981,"nanos":78211800},"willBeRetried":false}} +{"testCase":{"id":"53","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"28","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"56","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"57","pickleStepId":"30","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"50"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"58","pickleStepId":"31","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"150"},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"60","pickleStepId":"33","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"53","timestamp":{"seconds":1727518981,"nanos":85654600}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"55","timestamp":{"seconds":1727518981,"nanos":85763100}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"55","testStepResult":{"duration":{"seconds":0,"nanos":490900},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86254000}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"56","timestamp":{"seconds":1727518981,"nanos":86283700}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"56","testStepResult":{"duration":{"seconds":0,"nanos":101800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86385500}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"57","timestamp":{"seconds":1727518981,"nanos":86404200}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"57","testStepResult":{"duration":{"seconds":0,"nanos":270000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86674200}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"58","timestamp":{"seconds":1727518981,"nanos":86695700}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"58","testStepResult":{"duration":{"seconds":0,"nanos":91100},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86786800}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"59","timestamp":{"seconds":1727518981,"nanos":86800600}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"59","testStepResult":{"duration":{"seconds":0,"nanos":173800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86974400}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"60","timestamp":{"seconds":1727518981,"nanos":86988900}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"60","testStepResult":{"duration":{"seconds":0,"nanos":225400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":87214300}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":1727518981,"nanos":87242200},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"seconds":1727518981,"nanos":96745400}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 6ef87429d..3eb7719de 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -79,6 +79,31 @@ When I eat 5 cukes ShouldAllScenariosPass(); } + [TestMethod] + public void SmokeTestMultipleFeatures() + { + ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); + ResetCucumberMessages("Cucumber Messages Smoke Test Second Smoke Test.ndjson"); + EnableCucumberMessages(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test Second Smoke Test + Scenario: Eating Other Cukes + When I eat 6 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] public void SmokeOutlineTest() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 0813739d3..d9a14c9e4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -70,9 +70,11 @@ Always + + Always + - From 9582c8ce2bdf7e0f3b24379639a2be17e89c777d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:14:16 -0500 Subject: [PATCH 078/218] Fix Publisher to subscribe to TestThreadPublisher on CustomizeTestThreadDependencies. Check for null Feature Context on hooks that execute before a feature is started. --- Reqnroll/CucumberMessages/CucumberMessagePublisher.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index d927d8961..730b4083d 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -26,7 +26,7 @@ public CucumberMessagePublisher() } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { objectContainer = args.ObjectContainer; _brokerFactory = new Lazy(() => objectContainer.Resolve()); @@ -184,7 +184,9 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. + // Bypass them by checking for null + var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingStartedEvent); } @@ -194,7 +196,9 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. + // Bypass them by checking for null + var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingFinishedEvent); } From 4ccd7ae973cf27425d65e8b9f2240880fb0452db Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:03:07 -0500 Subject: [PATCH 079/218] Added the Empty scenario from the CCK; adjusted test validation to account for step-less Scenarios. --- .../CCK/empty/empty.cs | 12 ++++++++++++ .../CCK/empty/empty.feature | 7 +++++++ .../CCK/empty/empty.feature.ndjson | 9 +++++++++ .../CucumberCompatibilityTests.cs | 1 + .../CucumberMessages.CompatibilityTests.csproj | 8 ++++++++ .../CucumberMessagesValidator.cs | 13 ++++++++++--- 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs new file mode 100644 index 000000000..d606b7f0a --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.empty +{ + internal class Empty + { + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature new file mode 100644 index 000000000..eb6eced01 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature @@ -0,0 +1,7 @@ +Feature: Empty Scenarios + Sometimes we want to quickly jot down a new scenario without specifying any actual steps + for what should be executed. + + In this instance we want to stipulate what should / shouldn't run and what the output is + + Scenario: Blank Scenario diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson new file mode 100644 index 000000000..4d171d6dd --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson @@ -0,0 +1,9 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"17.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"22.7.0"}}} +{"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is\n\n Scenario: Blank Scenario\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/empty/empty.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"0","keyword":"Scenario","location":{"column":3,"line":7},"name":"Blank Scenario","steps":[],"tags":[]}}],"description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Empty Scenarios","tags":[]},"uri":"samples/empty/empty.feature"}} +{"pickle":{"astNodeIds":["0"],"id":"1","language":"en","name":"Blank Scenario","steps":[],"tags":[],"uri":"samples/empty/empty.feature"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"2","pickleId":"1","testSteps":[]}} +{"testCaseStarted":{"attempt":0,"id":"3","testCaseId":"2","timestamp":{"nanos":1000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"3","timestamp":{"nanos":2000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":3000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 3eb7719de..43c196c8b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -282,6 +282,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("minimal", "minimal")] [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] + [DataRow("empty", "Empty Scenarios")] [DataRow("examples-tables", "Examples Tables")] [DataRow("data-tables", "Data Tables")] [DataRow("hooks", "Hooks")] diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index d9a14c9e4..d4620485d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -15,6 +15,7 @@ + @@ -38,6 +39,8 @@ + + @@ -73,6 +76,9 @@ Always + + Always + @@ -85,6 +91,8 @@ Always + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 7bcb01a15..e8710f5c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -148,8 +148,11 @@ public void ResultShouldPassSanityChecks() private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() { - // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message + // IF the expected results contains no TestStepStarted messages, then there is nothing to check + if (!expecteds_elementsByType.Keys.Contains(typeof(TestStepStarted))) + return; + // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message var actualTestStepStarted_TestStepIds = actuals_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); var expectedTestStepStarteds_TestStepIds = expecteds_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); @@ -190,12 +193,16 @@ private void TestExecutionStepsShouldProperlyReferenceTestCases() var testCaseStartedIds = testCaseStarteds.Select(tcs => tcs.Id).ToList(); var testCaseFinisheds = actuals_elementsByType[typeof(TestCaseFinished)].OfType().ToList(); + testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); + + // IF the Scenario has no steps, return early. + if (!actuals_elementsByType.Keys.Contains(typeof(TestStepStarted))) + return; var testStepStarteds = actuals_elementsByType[typeof(TestStepStarted)].OfType().ToList(); var testStepFinisheds = actuals_elementsByType[typeof(TestStepFinished)].OfType().ToList(); - + testCaseStartedIds.Should().Contain(id => testStepStarteds.Any(tss => tss.TestCaseStartedId == id), "a test case started should be referenced by at least one test step started message"); testCaseStartedIds.Should().Contain(id => testStepFinisheds.Any(tsf => tsf.TestCaseStartedId == id), "a test case started should be referenced by at least one test step finished message"); - testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); } private void TestExecutionMessagesShouldProperlyNest() From 0f37bc717ed94bfe653a46b1976063c306aa58e7 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:26:53 -0500 Subject: [PATCH 080/218] Removing the Messages FileSink Plugin directory as it is no longer needed. --- .../CucumberMessages-config-schema.json | 34 --- .../FileSinkConfiguration.cs | 35 ---- .../FileSinkPlugin.cs | 194 ------------------ ...mberMessage.FileSink.ReqnrollPlugin.csproj | 21 -- 4 files changed, 284 deletions(-) delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json deleted file mode 100644 index f7fcd8f17..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "filSinkEnabled": { - "type": "boolean", - "description": "Indicates whether the program is enabled or not" - }, - "destinations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Indicates whether this destination setting is enabled" - }, - "basePath": { - "type": "string", - "description": "The base path for output files" - }, - "outputDirectory": { - "type": "string", - "description": "The directory path where output should go" - } - }, - "required": [ "enabled", "basePath", "outputDirectory" ] - }, - "minItems": 1, - "description": "Array of destination settings" - } - }, - "required": [ "programEnabled", "destinations" ] -} \ No newline at end of file diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs deleted file mode 100644 index 7a56f4233..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - internal class FileSinkConfiguration - { - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs deleted file mode 100644 index d95de1804..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Reqnroll.CucumberMessages; -using Reqnroll.Plugins; -using Reqnroll.UnitTestProvider; -using Io.Cucumber.Messages; -using Cucumber.Messages; -using Io.Cucumber.Messages.Types; -using System.Reflection; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; -using System.Diagnostics; -using Reqnroll.Events; -using System.Collections.Concurrent; -using System.Text.Json; -using Reqnroll.Tracing; -using Reqnroll.BoDi; - -[assembly: RuntimePlugin(typeof(FileSinkPlugin))] - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin - { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; - private Task? fileWritingTask; - - //Thread safe collections to hold: - // 1. Inbound Cucumber Messages - BlockingCollection - // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - private object _lock = new(); - private readonly BlockingCollection postedMessages = new(); - private readonly ConcurrentDictionary fileStreams = new(); - private FileSinkConfiguration? configuration; - private string baseDirectory = ""; - private Lazy? traceListener; - private ITraceListener? trace => traceListener?.Value; - private IObjectContainer? objectContainer; - - public FileSinkPlugin() - { - traceListener = new Lazy(() => objectContainer!.Resolve()); - } - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - objectContainer = args.ObjectContainer; - testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); - testThreadExecutionEventPublisher.AddHandler(CloseFileSink); - }; - } - - private string ProcessConfiguration(FileSinkConfiguration configuration) - { - var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeDestination != null) - { - var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); - if (!Directory.Exists(basePath)) - { - lock (_lock) - { - if (!Directory.Exists(basePath)) - Directory.CreateDirectory(basePath); - } - } - trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); - return basePath; - } - else - { - var location = Assembly.GetExecutingAssembly().Location; - trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); - return location; - } - } - - private void CloseFileSink(TestRunFinishedEvent @event) - { - trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); - postedMessages.CompleteAdding(); - fileWritingTask?.Wait(); - fileWritingTask = null; - } - - private void LaunchFileSink(TestRunStartedEvent testRunStarted) - { - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) - { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); - return; - } - - baseDirectory = ProcessConfiguration(configuration); - - fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); - } - - public void Publish(ReqnrollCucumberMessage message) - { - var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); - postedMessages.Add(message); - } - - private async Task ConsumeAndWriteToFiles() - { - foreach (var message in postedMessages.GetConsumingEnumerable()) - { - var featureName = message.CucumberMessageSource; - - if (message.Envelope != null) - { - var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); - await Write(featureName, cm); - } - else - { - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); - CloseFeatureStream(featureName); - } - } - } - - - private string Serialize(Envelope message) - { - return NdjsonSerializer.Serialize(message); - } - private async Task Write(string featureName, string cucumberMessage) - { - try - { - if (!fileStreams.ContainsKey(featureName)) - { - lock (_lock) - { - if (!fileStreams.ContainsKey(featureName)) - { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); - } - } - } - trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); - await fileStreams[featureName].WriteLineAsync(cucumberMessage); - } - catch (System.Exception ex) - { - trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); - } - } - - private void CloseFeatureStream(string featureName) - { - trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); - fileStreams[featureName].Close(); - fileStreams.TryRemove(featureName, out var _); - } - private bool disposedValue; - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - CloseFileSink(new TestRunFinishedEvent()); - postedMessages.Dispose(); - foreach (var stream in fileStreams.Values) - { - stream.Close(); - stream.Dispose(); - }; - fileStreams.Clear(); - } - - disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj deleted file mode 100644 index 3b49d3ad1..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netstandard2.0 - enable - enable - True - Reqnroll Plugin which publishes test results as Cucumber Messages - - - - - - - - - - - - - From a45e83c2e449f92c662dcdf4f98487254518e394 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:43:58 -0500 Subject: [PATCH 081/218] Moved the assignment of a pickle step ID from inline generated code to the TestExecutionEngine ExecuteStepAsync. it also calls NextStep on the pickleStepSequence. --- .../Generation/ScenarioPartHelper.cs | 6 +----- .../Generation/UnitTestMethodGenerator.cs | 3 ++- Reqnroll/ITestRunner.cs | 10 +++++----- .../Infrastructure/ITestExecutionEngine.cs | 3 ++- .../Infrastructure/TestExecutionEngine.cs | 6 +++++- Reqnroll/ScenarioInfo.cs | 6 +++++- Reqnroll/TestRunner.cs | 20 +++++++++---------- .../CucumberExpressionIntegrationTests.cs | 2 +- 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 959d3f4f6..63d7dd7ab 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -95,8 +95,7 @@ public void GenerateStep(TestClassGenerationContext generationContext, List ConvertArg(object value, IBindingType typeToConvertTo #region Given-When-Then - public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId = null) + public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg) { StepDefinitionType stepDefinitionType = stepDefinitionKeyword == StepDefinitionKeyword.And || stepDefinitionKeyword == StepDefinitionKeyword.But ? GetCurrentBindingType() : (StepDefinitionType) stepDefinitionKeyword; + var stepSequenceIdentifiers = ScenarioContext.ScenarioInfo.PickleStepSequence; + var pickleStepId = stepSequenceIdentifiers?.CurrentPickleStepId ?? ""; + _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); @@ -638,6 +641,7 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, finally { _testThreadExecutionEventPublisher.PublishEvent(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + stepSequenceIdentifiers?.NextStep(); _contextManager.CleanupStepContext(); } } diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 0247f7e54..7271f6f5b 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -1,3 +1,4 @@ +using Reqnroll.CucumberMessages; using System; using System.Collections.Specialized; using System.Linq; @@ -40,8 +41,10 @@ public class ScenarioInfo /// The PickleId of the Scenario when exported as a Cucumber Message "pickle". /// public string PickleId { get; } + // The list of step PickleIds in the step sequence for this test case. + public PickleStepSequence PickleStepSequence { get; private set; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null) + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null, PickleStepSequence stepSequence = null) { Title = title; Description = description; @@ -49,6 +52,7 @@ public ScenarioInfo(string title, string description, string[] tags, IOrderedDic Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); PickleId = pickleId; + PickleStepSequence = stepSequence; } } } \ No newline at end of file diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index dbbe282dc..82c5bff7a 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -73,29 +73,29 @@ public async Task OnTestRunEndAsync() await _executionEngine.OnTestRunEndAsync(); } - public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg); } - public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg); } - public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg); } - public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg); } - public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg); } public void Pending() diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 2f4511a66..4c140d512 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -186,7 +186,7 @@ private async Task PerformStepExecution(string methodName, strin await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo("en-US"), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); await engine.OnScenarioStartAsync(); engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null)); - await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null, "stubPickleStepId"); + await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); var contextManager = testThreadContainer.Resolve(); contextManager.ScenarioContext.ScenarioExecutionStatus.Should().Be(ScenarioExecutionStatus.OK, $"should not fail with '{contextManager.ScenarioContext.TestError?.Message}'"); From cea66b868242ac9a2a1399ba832d52b33cff9490 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:44:35 -0500 Subject: [PATCH 082/218] Refactored Configuration into distinct subsystem. --- .../Configuration/ConfigurationDTO.cs | 51 +++++++ .../Configuration/CucumberConfiguration.cs | 130 ++++++++++++++++++ .../CucumberConfigurationConstants.cs | 11 ++ .../CucumberMessages-config-schema.json | 32 +++++ .../DefaultConfigurationSource.cs | 28 ++++ .../Configuration/IConfigurationSource.cs | 11 ++ .../Configuration/ICucumberConfiguration.cs | 7 + .../RCM_ConfigFile_ConfigurationSource.cs | 20 +++ .../Configuration/ResolvedConfiguration.cs | 11 ++ .../CucumberMessages/CucumberConfiguration.cs | 72 ---------- .../CucumberMessages-config-schema.json | 34 ----- .../CucumberOutputConfiguration.cs | 40 ------ Reqnroll/CucumberMessages/FileOutputPlugin.cs | 33 ++--- .../DefaultDependencyProvider.cs | 2 + .../CucumberCompatibilityTestBase.cs | 13 +- .../CucumberMessages.configuration.json | 3 +- 16 files changed, 324 insertions(+), 174 deletions(-) create mode 100644 Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json create mode 100644 Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs delete mode 100644 Reqnroll/CucumberMessages/CucumberConfiguration.cs delete mode 100644 Reqnroll/CucumberMessages/CucumberMessages-config-schema.json delete mode 100644 Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs new file mode 100644 index 000000000..c3c5d0a82 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + /// + /// This class holds configuration information from a configuration source. + /// Each configuration source may provide one or more Profiles (such as Dev or Prod). + /// The default profile is always named 'DEFAULT'. + /// + public class ConfigurationDTO + { + + public bool FileOutputEnabled { get; set; } + public string ActiveProfileName { get; set; } + public List Profiles { get; set; } + + public Profile ActiveProfile => Profiles.Where(p => p.ProfileName == ActiveProfileName).FirstOrDefault(); + + public ConfigurationDTO() : this(true) { } + public ConfigurationDTO(bool enabled) : this(enabled, "DEFAULT", new List()) { } + public ConfigurationDTO(bool enabled, string activeProfile, List profiles) + { + FileOutputEnabled = enabled; + ActiveProfileName = activeProfile; + Profiles = profiles; + } + + } + + public class Profile + { + public string ProfileName { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + public string OutputFileName { get; set; } + + public Profile(string profileName, string basePath, string outputDirectory, string outputFileName) + { + ProfileName = string.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; + BasePath = basePath ?? ""; + OutputDirectory = outputDirectory ?? ""; + OutputFileName = outputFileName ?? ""; + } + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs new file mode 100644 index 000000000..18af77f5d --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -0,0 +1,130 @@ +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Tracing; +using System; +using System.IO; +using System.Linq; + +namespace Reqnroll.CucumberMessages.Configuration +{ + public class CucumberConfiguration : ICucumberConfiguration + { + private ITraceListener _trace; + private IEnvironmentWrapper _environmentWrapper; + + private ResolvedConfiguration outputConfiguration = new(); + private bool _enablementOverrideFlag = true; + + public bool Enabled => _enablementOverrideFlag && outputConfiguration.Enabled; + public string BaseDirectory => outputConfiguration.BaseDirectory; + public string OutputDirectory => outputConfiguration.OutputDirectory; + public string OutputFileName => outputConfiguration.OutputFileName; + + public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) + { + _trace = traceListener; + _environmentWrapper = environmentWrapper; + } + #region Override API + public void SetEnabled(bool value) + { + _enablementOverrideFlag = value; + } + #endregion + public ResolvedConfiguration ResolveConfiguration() + { + var config = ApplyHierarchicalConfiguration(); + var resolved = ApplyEnvironmentOverrides(config); + EnsureOutputDirectory(resolved); + + string logEntry; + logEntry = $"Cucumber Messages: FileOutput Initialized. Output Path: {Path.Combine(resolved.BaseDirectory, resolved.OutputDirectory, resolved.OutputFileName)}"; + + _trace!.WriteTestOutput(logEntry); + outputConfiguration = resolved; + return resolved; + } + private ConfigurationDTO ApplyHierarchicalConfiguration() + { + var defaultConfigurationProvider = new DefaultConfigurationSource(_environmentWrapper); + var fileBasedConfigurationProvider = new RCM_ConfigFile_ConfigurationSource(); + + ConfigurationDTO config = defaultConfigurationProvider.GetConfiguration(); + config = AddConfig(config, fileBasedConfigurationProvider.GetConfiguration()); + return config; + } + + private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) + { + var baseOutDirValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE); + var relativePathValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE); + var fileNameValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE); + var profileValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); + string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + + var activeConfiguredDestination = config.Profiles.Where(d => d.ProfileName == profileName).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + config.ActiveProfileName = profileName; + }; + var result = new ResolvedConfiguration() + { + Enabled = config.FileOutputEnabled, + BaseDirectory = config.ActiveProfile.BasePath, + OutputDirectory = config.ActiveProfile.OutputDirectory, + OutputFileName = config.ActiveProfile.OutputFileName + }; + + if (baseOutDirValue is Success) + result.BaseDirectory = ((Success)baseOutDirValue).Result; + + if (relativePathValue is Success) + result.OutputDirectory = ((Success)relativePathValue).Result; + + if (fileNameValue is Success) + result.OutputFileName = ((Success)fileNameValue).Result; + var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; + + result.Enabled = Convert.ToBoolean(enabled); + + return result; + } + + private ConfigurationDTO AddConfig(ConfigurationDTO config, ConfigurationDTO overridingConfig) + { + if (overridingConfig != null) + { + config.Profiles.AddRange(overridingConfig.Profiles); + if (overridingConfig.ActiveProfileName != null && !config.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) + { + // The incoming configuration DTO points to a profile that doesn't exist. + _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {config.ActiveProfileName} instead."); + } + else if (overridingConfig.ActiveProfileName != null) + config.ActiveProfileName = overridingConfig.ActiveProfileName; + + config.FileOutputEnabled = overridingConfig.FileOutputEnabled; + } + + return config; + } + + private void EnsureOutputDirectory(ResolvedConfiguration config) + { + + if (!Directory.Exists(config.BaseDirectory)) + { + Directory.CreateDirectory(config.BaseDirectory); + Directory.CreateDirectory(config.OutputDirectory); + } + else if (!Directory.Exists(Path.Combine(config.BaseDirectory, config.OutputDirectory))) + { + Directory.CreateDirectory(Path.Combine(config.BaseDirectory, config.OutputDirectory)); + } + } + + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs new file mode 100644 index 000000000..335ce9e6a --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public static class CucumberConfigurationConstants + { + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME"; + public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json new file mode 100644 index 000000000..8f7c6e917 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json @@ -0,0 +1,32 @@ +{ + "description": "This class holds configuration information from a configuration source.\nEach configuration source may provide one or more Profiles (such as Dev or Prod). \nThe default profile is always named \u0027DEFAULT\u0027.", + "type": "object", + "properties": { + "FileOutputEnabled": { + "type": "boolean" + }, + "ActiveProfileName": { + "type": "string" + }, + "Profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ProfileName": { + "type": "string" + }, + "BasePath": { + "type": "string" + }, + "OutputDirectory": { + "type": "string" + }, + "OutputFileName": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs new file mode 100644 index 000000000..1138b160e --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -0,0 +1,28 @@ +using Reqnroll.EnvironmentAccess; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal class DefaultConfigurationSource : IConfigurationSource + { + private IEnvironmentWrapper _environmentWrapper; + + public DefaultConfigurationSource(IEnvironmentWrapper environmentWrapper) + { + _environmentWrapper = environmentWrapper; + } + public ConfigurationDTO GetConfiguration() + { + var res = new ConfigurationDTO(); + string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); + string defaultOutputFileName = "reqnroll_report.ndjson"; + + var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName); + res.FileOutputEnabled = false; + res.Profiles.Add(defaultProfile); + return res; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs new file mode 100644 index 000000000..2993802cb --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal interface IConfigurationSource + { + ConfigurationDTO GetConfiguration(); + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs new file mode 100644 index 000000000..5f77e3d58 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs @@ -0,0 +1,7 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public interface ICucumberConfiguration + { + ResolvedConfiguration ResolveConfiguration(); + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs new file mode 100644 index 000000000..d3521255d --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Text.Json; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal class RCM_ConfigFile_ConfigurationSource : IConfigurationSource + { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + + public ConfigurationDTO GetConfiguration() + { + ConfigurationDTO configurationDTO = null; + if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) + { + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + } + return configurationDTO; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs new file mode 100644 index 000000000..a11c69f05 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public class ResolvedConfiguration + { + public bool Enabled { get; set; } + public string BaseDirectory { get; set; } + public string OutputDirectory { get; set; } + public string OutputFileName { get; set; } + } +} + diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs deleted file mode 100644 index 2c7722ffe..000000000 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Reqnroll.CommonModels; -using Reqnroll.EnvironmentAccess; -using Reqnroll.Tracing; -using System; -using System.IO; -using System.Linq; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class CucumberConfiguration - { - internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; - internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; - public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; - - private ITraceListener _trace; - private IEnvironmentWrapper _environmentWrapper; - private object _lock = new(); - - private CucumberOutputConfiguration outputConfiguration; - public bool Enabled => outputConfiguration != null ? outputConfiguration.FileOutputEnabled : false; - - public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) - { - _trace = traceListener; - _environmentWrapper = environmentWrapper; - } - - public string ConfigureOutputDirectory(CucumberOutputConfiguration config) - { - outputConfiguration = config; - string outputDirectory; - string configuredOutputDirectory = string.Empty; - string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); - var outDirValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); - var profileValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); - string environmentVariableOutputDirectory = outDirValue is Success ? ((Success)outDirValue).Result : null; - string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; - - var activeConfiguredDestination = config.Destinations.Where(d => d.ProfileName == profileName).FirstOrDefault(); - - if (activeConfiguredDestination != null) - { - configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); - } - - outputDirectory = defaultOutputDirectory; - if (!String.IsNullOrEmpty(configuredOutputDirectory)) - outputDirectory = configuredOutputDirectory; - if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) - outputDirectory = environmentVariableOutputDirectory; - - string logEntry; - if (outputDirectory == defaultOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; - else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; - else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - - _trace!.WriteTestOutput(logEntry); - if (!Directory.Exists(outputDirectory)) - { - lock (_lock) - { - if (!Directory.Exists(outputDirectory)) - Directory.CreateDirectory(outputDirectory); - } - } - return outputDirectory; - } - - } -} - diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json deleted file mode 100644 index b6f6b7919..000000000 --- a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "fileOutputEnabled": { - "type": "boolean", - "description": "Indicates whether the program is enabled or not" - }, - "destinations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "profileName": { - "type": "string", - "description": "Provides a description for this destination (ex: 'dev' or 'prod')" - }, - "basePath": { - "type": "string", - "description": "The base path for output files" - }, - "outputDirectory": { - "type": "string", - "description": "The subdirectory of the base path where output should go" - } - }, - "required": [ "profileName", "basePath", "outputDirectory" ] - }, - "minItems": 1, - "description": "Array of destination settings" - } - }, - "required": [ "fileOutputEnabled", "destinations" ] -} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs deleted file mode 100644 index e6c9499e3..000000000 --- a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Reqnroll.Time; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class CucumberOutputConfiguration - { - - public bool FileOutputEnabled { get; set; } - public List Destinations { get; set; } - - public CucumberOutputConfiguration() : this(true) { } - public CucumberOutputConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public CucumberOutputConfiguration(bool fileSinkEnabled, List destinations) - { - FileOutputEnabled = fileSinkEnabled; - Destinations = destinations; - } - - } - - public class Destination - { - public string ProfileName { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(string profileName, string basePath, string outputDirectory) - { - ProfileName = String.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 9c066f390..b2711813f 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -17,13 +17,13 @@ using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; using System.Diagnostics; +using Reqnroll.CucumberMessages.Configuration; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; private object _lock = new(); @@ -33,16 +33,18 @@ public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugi private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private string baseDirectory = ""; + private ICucumberConfiguration _configuration; private Lazy traceListener; private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; private IObjectContainer? globalObjectContainer; - public FileOutputPlugin() + public FileOutputPlugin(ICucumberConfiguration configuration) { + _configuration = configuration; traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); - //Debugger.Launch(); + Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -70,29 +72,18 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - var environment = testThreadObjectContainer!.Resolve(); - var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; - bool isMessagesEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + ResolvedConfiguration config = _configuration.ResolveConfiguration(); - CucumberOutputConfiguration configurationDTO; - if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) + if (!config.Enabled) { - configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - } - else - configurationDTO = new CucumberOutputConfiguration(isMessagesEnabled); - - if (!configurationDTO.FileOutputEnabled) - { - trace!.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace!.WriteTestOutput("Cucumber Messages is DISABLED."); + // By returning here, we don't launch the File writing thread, + // and this class is not registered as a CucumberMessageSink, which indicates to the Broker that Messages are disabled. return; } + baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); - var configuration = new CucumberConfiguration(trace, environment); - baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - trace!.WriteTestOutput($"FileOutputPlugin LaunchFileSink. Cucumber Messages is ENABLED. Base Directory: {baseDirectory}"); - trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); + trace?.WriteToolOutput("Cuccumber Messages: Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 21f481c22..b9c8fbae6 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -18,6 +18,7 @@ using Reqnroll.PlatformCompatibility; using Reqnroll.CucumberMessages; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.CucumberMessages.Configuration; namespace Reqnroll.Infrastructure { @@ -103,6 +104,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); //Support for publishing Cucumber Messages + container.RegisterTypeAs(); container.RegisterTypeAs("FileOutputPlugin"); container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index f97f78f78..19a268395 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,6 +1,6 @@ using FluentAssertions; using Moq; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.CucumberMessages.Configuration; using Reqnroll.EnvironmentAccess; using Reqnroll.SystemTests; using Reqnroll.Tracing; @@ -23,8 +23,8 @@ protected override void TestCleanup() protected void EnableCucumberMessages() { - Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); - Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE", "LOCAL"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE, "true"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE, "LOCAL"); } protected void DisableCucumberMessages() @@ -69,14 +69,15 @@ protected void CucumberMessagesAddConfigurationFile(string configFileName) protected static string ActualsResultLocationDirectory() { - var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + //var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + //var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var tracerMock = new Mock(); var env = new EnvironmentWrapper(); CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); - var resultLocation = configuration.ConfigureOutputDirectory(config); + var resolvedconfiguration = configuration.ResolveConfiguration(); + var resultLocation = Path.Combine(resolvedconfiguration.BaseDirectory, resolvedconfiguration.OutputDirectory); return resultLocation; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 22b0b03c8..a9d0152ee 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -1,6 +1,7 @@ { "fileOutputEnabled": true, - "destinations": [ + "activeProfileName": "LOCAL", + "profiles": [ { "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", From b7929f9de204dd3cb574fde769f07df5227984d5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:45:32 -0500 Subject: [PATCH 083/218] Add Explicit Text.Json assembly dependencies to MSBuild.Generation In order to avoid assembly version conflicts, adding explicit assembly dependencies to the MSBuild.Generation nuspec. --- .../Reqnroll.Tools.MsBuild.Generation.nuspec | 8 +++++++- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec index 263b360ab..00f3987df 100644 --- a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec +++ b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec @@ -29,7 +29,13 @@ exclude="bin\$config$\netstandard2.0\System.*;bin\$config$\netstandard2.0\Microsoft.*" target="tasks\$Reqnroll_Core_Tools_TFM$" /> - + + + + + + + diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index b2711813f..083f55d33 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -44,7 +44,6 @@ public FileOutputPlugin(ICucumberConfiguration configuration) { _configuration = configuration; traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); - Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) From 6081e225f10f89f70c658868e49d67679ec0de3d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:36:47 -0500 Subject: [PATCH 084/218] Improved PickleJar's handling of edge-case in which we have no Pickles. Will allow code gen and execution to continue without support for Messages. --- .../Generation/UnitTestFeatureGenerator.cs | 12 +++---- Reqnroll/CucumberMessages/PickleJar.cs | 31 ++++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 4b13a7f2c..b4c6c7edb 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -18,11 +18,11 @@ namespace Reqnroll.Generator.Generation { public class UnitTestFeatureGenerator : IFeatureGenerator { - private const string PICKLES = "Pickles"; private const string PICKLEJAR = "PICKLEJAR"; private readonly CodeDomHelper _codeDomHelper; private readonly IDecoratorRegistry _decoratorRegistry; + private readonly ITraceListener _traceListener; private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _testGeneratorProvider; @@ -33,12 +33,14 @@ public UnitTestFeatureGenerator( IUnitTestGeneratorProvider testGeneratorProvider, CodeDomHelper codeDomHelper, ReqnrollConfiguration reqnrollConfiguration, - IDecoratorRegistry decoratorRegistry) + IDecoratorRegistry decoratorRegistry, + ITraceListener traceListener) { _testGeneratorProvider = testGeneratorProvider; _codeDomHelper = codeDomHelper; _reqnrollConfiguration = reqnrollConfiguration; _decoratorRegistry = decoratorRegistry; + _traceListener = traceListener; _linePragmaHandler = new LinePragmaHandler(_reqnrollConfiguration, _codeDomHelper); _scenarioPartHelper = new ScenarioPartHelper(_reqnrollConfiguration, _codeDomHelper); _unitTestMethodGenerator = new UnitTestMethodGenerator(testGeneratorProvider, decoratorRegistry, _codeDomHelper, _scenarioPartHelper, _reqnrollConfiguration); @@ -240,14 +242,12 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); // Save the Pickles to the GenerationContext so that the Pickle and Step Ids can be injected as arguments into the Scenario and Step method signatures - //TODO: Confirm whether the Pickles are nessessary as the PickleJar already includes them - generationContext.CustomData.Add(PICKLES, featurePickleMessages); generationContext.CustomData.Add(PICKLEJAR, new PickleJar(featurePickleMessages)); } - catch + catch(Exception e) { + _traceListener.WriteToolOutput($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. - generationContext.CustomData.Add(PICKLES, null); generationContext.CustomData.Add(PICKLEJAR, new PickleJar(new List())); return; } diff --git a/Reqnroll/CucumberMessages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs index e6f230cd9..af5308d5b 100644 --- a/Reqnroll/CucumberMessages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -11,6 +11,8 @@ public class PickleJar public const string PICKLEJAR_VARIABLE_NAME = "m_pickleJar"; public int _PickleCounter = 0; + + public bool HasPickles { get; } public IEnumerable Pickles { get; set; } //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) @@ -22,18 +24,22 @@ public PickleJar(IEnumerable pickles, int { Pickles = pickles; _PickleCounter = pickleCounter; + HasPickles = pickles != null && pickles.Count() > 0; } - public string CurrentPickleId { get { return Pickles.ElementAt(_PickleCounter).Id; } } - public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } - - public IEnumerable PickleStepIdsFor(string pickleId) + public string CurrentPickleId { - return Pickles.Where(p => p.Id == pickleId).SelectMany(p => p.Steps.Select(s => s.Id)).ToArray(); + get + { + if (!HasPickles) return null; + return Pickles.ElementAt(_PickleCounter).Id; + } } + public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } + public PickleStepSequence PickleStepSequenceFor(string pickleId) { - return new PickleStepSequence(Pickles.Where(p => p.Id == pickleId).First()); + return new PickleStepSequence(HasPickles, Pickles.Where(p => p.Id == pickleId).First()); } public void NextPickle() @@ -45,12 +51,14 @@ public void NextPickle() public class PickleStepSequence { + public bool HasPickles { get; } public Pickle CurrentPickle { get; } private int _PickleStepCounter; - public PickleStepSequence(Gherkin.CucumberMessages.Types.Pickle pickle) + public PickleStepSequence(bool hasPickles, Gherkin.CucumberMessages.Types.Pickle pickle) { + HasPickles = hasPickles; CurrentPickle = pickle; _PickleStepCounter = 0; } @@ -58,7 +66,14 @@ public void NextStep() { _PickleStepCounter++; } - public string CurrentPickleStepId { get { return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; } } + public string CurrentPickleStepId + { + get + { + if (!HasPickles) return null; + return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; + } + } } } From 6eed6e9dfe381776ef1f689732a1f2f948a8e6bf Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:35:21 -0500 Subject: [PATCH 085/218] Add TimeStamp to all ExecutionEvents. These will be used when serializing events as Cucumber Messages. --- Reqnroll/Events/ExecutionEvent.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index afa94a42d..cc6ab3742 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -5,6 +5,9 @@ namespace Reqnroll.Events { public class ExecutionEvent : IExecutionEvent { + public DateTime Timestamp { get; } + + public ExecutionEvent() => Timestamp = DateTime.Now; } public class TestRunStartedEvent : ExecutionEvent From 1bb48228227cec44377795e2068234ad009132ea Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:56:02 -0500 Subject: [PATCH 086/218] Experiment: Threading.Channel used to pump Messages from TestThreadExecutionEvents to one or more Message Sinks. Default implementation of one Sink that serializes Messages to an ndjson file per feature. Not finished: hooking up the TestThreadExecution event handlers. --- .../CucumberMessagEnumConverter.cs | 40 +++++++++++++ .../FileSinkPlugin.cs | 57 +++++++++++++++++++ .../NdjsonSerializer.cs | 56 ++++++++++++++++++ ...mberMessage.FileSink.ReqnrollPlugin.csproj | 23 ++++++++ Reqnroll.sln | 10 ++++ .../CucumberMessageBroker.cs | 51 +++++++++++++++++ .../CucumberMessageExtensions.cs | 27 +++++++++ .../CucumberMessagePublisher.cs | 39 +++++++++++++ .../CucumberMessageSinkBase.cs | 36 ++++++++++++ .../CucumberMesssages/ICucumberMessageSink.cs | 10 ++++ .../ReqnrollCucumberMessage.cs | 11 ++++ Reqnroll/Reqnroll.csproj | 7 +++ 12 files changed, 367 insertions(+) create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs create mode 100644 Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageBroker.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessageSink.cs create mode 100644 Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs new file mode 100644 index 000000000..c5fb0457e --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Cucumber.Messages +{ + internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum + { + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + protected internal CucumberMessageEnumConverter() + { + var type = typeof(T); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var value = (T)field.GetValue(null)!; + var attribute = field.GetCustomAttribute(); + var name = attribute?.Description ?? field.Name; + _enumToString[value] = name; + _stringToEnum[name] = value; + } + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString(); + return _stringToEnum.TryGetValue(stringValue!, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : value.ToString()); + } + } + +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs new file mode 100644 index 000000000..826f9041a --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -0,0 +1,57 @@ +using Reqnroll.CucumberMesssages; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using Io.Cucumber.Messages; +using Cucumber.Messages; +using Io.Cucumber.Messages.Types; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class FileSinkPlugin : CucumberMessageSinkBase + { + new public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + base.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + + Task.Run(() => ConsumeAndWriteToFiles()); + } + + private async Task ConsumeAndWriteToFiles() + { + await foreach (var message in Consume()) + { + var featureName = message.CucumberMessageSource; + if (message.Envelope != null) + { + Write(featureName, Serialize(message.Envelope)); + } + else + { + CloseFeature(featureName); + } + } + } + + private Dictionary fileStreams = new(); + private string baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "CucumberMessages"); + + private string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + private void Write(string featureName, string cucumberMessage) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + fileStreams[featureName].WriteLine(cucumberMessage); + } + + private void CloseFeature(string featureName) + { + fileStreams[featureName].Close(); + fileStreams.Remove(featureName); + } + } +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs new file mode 100644 index 000000000..4ea75d028 --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs @@ -0,0 +1,56 @@ +using Io.Cucumber.Messages.Types; +using System; +using System.Text.Json; + +namespace Cucumber.Messages +{ + /// + /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. + /// Consumers of Cucumber.Messages should use these options, or their serialization library's equivalent options. + /// These options should work with System.Text.Json v6 or above. + /// + public class NdjsonSerializer + { + private static readonly Lazy _jsonOptions = new(() => + { + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + + return options; + }); + + private static JsonSerializerOptions JsonOptions { get + { + return _jsonOptions.Value; + } + } + + public static string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + + internal static string Serialize(T message) + { + return JsonSerializer.Serialize(message, JsonOptions); + } + + public static Envelope Deserialize(string json) + { + return NdjsonSerializer.Deserialize(json); + } + + internal static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions)!; + } + } +} diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj new file mode 100644 index 000000000..0b4956b03 --- /dev/null +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + enable + enable + + + + + + + + + + + + + ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + + + + diff --git a/Reqnroll.sln b/Reqnroll.sln index 521df415a..60a1c844f 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,6 +116,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessageConsumerTest", "CucumberMessageConsumerTest", "{01874043-F824-4C72-A7F5-CA1B16031714}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -242,6 +246,10 @@ Global {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -280,6 +288,8 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} + {01874043-F824-4C72-A7F5-CA1B16031714} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs new file mode 100644 index 000000000..c771724cb --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -0,0 +1,51 @@ +using Reqnroll.BoDi; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Security.Authentication.ExtendedProtection; +using System.Text; +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + + public interface ICucumberMessageBroker + { + Task CompleteAsync(string cucumberMessageSource); + Task PublishAsync(ReqnrollCucumberMessage message); + } + + public class CucumberMessageBroker : ICucumberMessageBroker + { + private List registeredSinks; + + + public CucumberMessageBroker(IObjectContainer objectContainer) + { + var sinks = objectContainer.ResolveAll(); + registeredSinks = new List(sinks); + } + public async Task PublishAsync(ReqnrollCucumberMessage message) + { + foreach (var sink in registeredSinks) + { + await sink.Publish(message); + } + } + + // using an empty CucumberMessage to indicate completion + public async Task CompleteAsync(string cucumberMessageSource) + { + var completionMessage = new ReqnrollCucumberMessage + { + CucumberMessageSource = cucumberMessageSource + }; + + foreach (var sink in registeredSinks) + { + await sink.Publish(completionMessage); + } + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs new file mode 100644 index 000000000..de2ae191c --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; + + +namespace Reqnroll.CucumberMesssages +{ +#if NETSTANDARD2_0 + public static class CucumberMessageExtensions + { + public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (reader.TryRead(out T? item)) + { + yield return item; + } + } + } + } +#endif +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs new file mode 100644 index 000000000..1e16854eb --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -0,0 +1,39 @@ +using Reqnroll.BoDi; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Io.Cucumber.Messages; +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMesssages +{ + public class CucumberMessagePublisher + { + private ICucumberMessageBroker broker; + + public CucumberMessagePublisher(ITestThreadExecutionEventPublisher eventSource, ICucumberMessageBroker CucumberMessageBroker) + { + broker = CucumberMessageBroker; + eventSource.AddHandler(FeatureStartedEventHandler); + eventSource.AddHandler(FeatureFinishedEventHandler); + } + + private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) + { + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.FolderPath; + Task.Run(() => broker.CompleteAsync(featureName)); + } + + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + { + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.FolderPath; + Task.Run(() => broker.PublishAsync(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) + })); + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs new file mode 100644 index 000000000..99be48a47 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -0,0 +1,36 @@ +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using System.Collections.Generic; +using System.Threading.Channels; +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + public class CucumberMessageSinkBase : ICucumberMessageSink, IRuntimePlugin + { + private Channel _channel = Channel.CreateUnbounded(); + public async Task Publish(ReqnrollCucumberMessage message) + { + await _channel.Writer.WriteAsync(message); + if (message.Envelope == null) + { + _channel.Writer.Complete(); + } + } + + public async IAsyncEnumerable Consume() + { + await foreach (var message in _channel.Reader.ReadAllAsync()) + { + yield return message; + } + } + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + } + + } +} diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs new file mode 100644 index 000000000..a796bdb24 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMesssages +{ + public interface ICucumberMessageSink + { + Task Publish(ReqnrollCucumberMessage message); + } +} diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs new file mode 100644 index 000000000..d91af932d --- /dev/null +++ b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs @@ -0,0 +1,11 @@ +using Io.Cucumber.Messages.Types; + + +namespace Reqnroll.CucumberMesssages +{ + public class ReqnrollCucumberMessage + { + public string CucumberMessageSource { get; set; } + public Envelope Envelope { get; set; } + } +} diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 85bca01b9..ed47c9ca2 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -29,6 +29,7 @@ + @@ -41,6 +42,12 @@ + + + ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + + + From 558e12b93c90c856a9e22c355699dc982ce01173 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:33:18 -0500 Subject: [PATCH 087/218] Experimental: plumbed the instantiation and intialization of the Cucumber Messages Publisher and Broker to the initialization sequence provided by the Plugin architecture. --- .../FileSinkPlugin.cs | 4 ++-- Reqnroll.sln | 8 ++++---- .../CucumberMesssages/CucumberMessagePublisher.cs | 12 ++++++++---- .../CucumberMesssages/CucumberMessageSinkBase.cs | 9 ++++++++- .../CucumberMesssages/ICucumberMessagePublisher.cs | 9 +++++++++ 5 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 826f9041a..501106f87 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -27,7 +27,7 @@ private async Task ConsumeAndWriteToFiles() } else { - CloseFeature(featureName); + CloseFeatureStream(featureName); } } } @@ -48,7 +48,7 @@ private void Write(string featureName, string cucumberMessage) fileStreams[featureName].WriteLine(cucumberMessage); } - private void CloseFeature(string featureName) + private void CloseFeatureStream(string featureName) { fileStreams[featureName].Close(); fileStreams.Remove(featureName); diff --git a/Reqnroll.sln b/Reqnroll.sln index 60a1c844f..b17411911 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,10 +116,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessageConsumerTest", "CucumberMessageConsumerTest", "{01874043-F824-4C72-A7F5-CA1B16031714}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -288,8 +288,8 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {01874043-F824-4C72-A7F5-CA1B16031714} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} + {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} + {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1e16854eb..063eb906b 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -9,15 +9,19 @@ namespace Reqnroll.CucumberMesssages { - public class CucumberMessagePublisher + public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; - public CucumberMessagePublisher(ITestThreadExecutionEventPublisher eventSource, ICucumberMessageBroker CucumberMessageBroker) + public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker) { broker = CucumberMessageBroker; - eventSource.AddHandler(FeatureStartedEventHandler); - eventSource.AddHandler(FeatureFinishedEventHandler); + } + + public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) + { + testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); + testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index 99be48a47..199664475 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -1,4 +1,5 @@ -using Reqnroll.Plugins; +using Reqnroll.Events; +using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; using System.Collections.Generic; using System.Threading.Channels; @@ -30,6 +31,12 @@ public async IAsyncEnumerable Consume() public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var publisher = args.ObjectContainer.Resolve(); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; } } diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs new file mode 100644 index 000000000..458747f4f --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs @@ -0,0 +1,9 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + public interface ICucumberMessagePublisher + { + public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher); + } +} \ No newline at end of file From b2265afd78cec37729da4dc4e51fd058c17daff2 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:30:16 -0500 Subject: [PATCH 088/218] Exploratory: Extended System Tests to create a CucumberCompatibilityTest project. The File Sink is hooked in as a nuget package. Next: exploratory debugging to confirm the event listeners are wired up correctly at Feature start time. --- .../CucumberMessagEnumConverter.cs | 0 .../FileSinkPlugin.cs | 6 ++++- .../NdjsonSerializer.cs | 0 ...mberMessage.FileSink.ReqnrollPlugin.csproj | 7 +++-- Reqnroll.sln | 21 ++++++++++----- .../CucumberCompatibilityTestBase.cs | 18 +++++++++++++ .../CucumberCompatibilityTests.cs | 24 +++++++++++++++++ ...CucumberMessages.CompatibilityTests.csproj | 27 +++++++++++++++++++ nuget.config | 3 ++- 9 files changed, 95 insertions(+), 11 deletions(-) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/CucumberMessagEnumConverter.cs (100%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/FileSinkPlugin.cs (92%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/NdjsonSerializer.cs (100%) rename {Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin}/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj (55%) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs similarity index 100% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs similarity index 92% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 501106f87..ac9359a90 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -4,6 +4,10 @@ using Io.Cucumber.Messages; using Cucumber.Messages; using Io.Cucumber.Messages.Types; +using System.Reflection; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; + +[assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { @@ -25,7 +29,7 @@ private async Task ConsumeAndWriteToFiles() { Write(featureName, Serialize(message.Envelope)); } - else + else { CloseFeatureStream(featureName); } diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs similarity index 100% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs diff --git a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj similarity index 55% rename from Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj rename to Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 0b4956b03..afea6d0f2 100644 --- a/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -4,6 +4,9 @@ netstandard2.0 enable enable + True + Reqnroll Plugin which publishes test results as Cucumber Messages + @@ -11,12 +14,12 @@ - + - ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll + ..\..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll diff --git a/Reqnroll.sln b/Reqnroll.sln index b17411911..e0e020419 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,10 +116,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.CompatibilityTests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.CompatibilityTests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Plugins\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{58B078D5-37A2-416B-91DA-E5E6BD510062}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -246,10 +248,14 @@ Global {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6}.Release|Any CPU.Build.0 = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.Build.0 = Release|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -288,8 +294,9 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {DA93AE98-D1E7-4C51-8CF1-6874F87ADED6} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} + {5072F73C-8CDD-4B44-B3F8-4212F65C3708} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} + {58B078D5-37A2-416B-91DA-E5E6BD510062} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs new file mode 100644 index 000000000..38463ae2d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -0,0 +1,18 @@ +using Reqnroll.SystemTests; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + public class CucumberCompatibilityTestBase : SystemTestBase + { + + protected void AddCucumberMessagePlugIn() + { + _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs new file mode 100644 index 000000000..d45c31fa5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -0,0 +1,24 @@ +namespace CucumberMessages.CompatibilityTests +{ + [TestClass] + public class CucumberCompatibilityTests : CucumberCompatibilityTestBase + { + [TestMethod] + public void SmokeTest() + { + AddCucumberMessagePlugIn(); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Smoke Test + Given I have a passing step + """); + + AddPassingStepBinding(); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + } +} \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj new file mode 100644 index 000000000..ab72b2ffb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/nuget.config b/nuget.config index 27a5cefad..c8c559d96 100644 --- a/nuget.config +++ b/nuget.config @@ -1,8 +1,9 @@ - + + \ No newline at end of file From 8ab0c2481aae9d4a71ffbed3ccf122f045023339 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:30:06 -0500 Subject: [PATCH 089/218] Plugin stucture complete; test case runs, but no output. Need to figure out why trace logging also not working. --- .../FileSinkPlugin.cs | 75 +++++++++++++++++-- ...mberMessage.FileSink.ReqnrollPlugin.csproj | 7 +- .../CucumberMessageBroker.cs | 20 ++++- .../CucumberMessageExtensions.cs | 2 - .../CucumberMessagePublisher.cs | 31 ++++++-- .../CucumberMessageSinkBase.cs | 40 ++++++---- .../DefaultDependencyProvider.cs | 5 ++ Reqnroll/Reqnroll.csproj | 7 +- 8 files changed, 141 insertions(+), 46 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index ac9359a90..24c61aff4 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -6,28 +6,58 @@ using Io.Cucumber.Messages.Types; using System.Reflection; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using System.Diagnostics; +using Reqnroll.Events; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - public class FileSinkPlugin : CucumberMessageSinkBase + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - new public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + private Task? fileWritingTask; + + private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - base.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - Task.Run(() => ConsumeAndWriteToFiles()); + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); + testThreadExecutionEventPublisher.AddHandler(CloseFileSink); + }; + } + + private void CloseFileSink(TestRunFinishedEvent @event) + { + fileWritingTask?.Wait(); + fileWritingTask = null; + } + + private void LaunchFileSink(TestRunStartedEvent testRunStarted) + { + Console.WriteLine( "LaunchFileSink called" ); + fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } private async Task ConsumeAndWriteToFiles() { - await foreach (var message in Consume()) + Console.WriteLine( "ConsumeAndWriteToFiles called" ); + + await foreach (var message in sinkBase.Consume()) { var featureName = message.CucumberMessageSource; + + Console.WriteLine( "ConsumeAndWriteToFiles: " + featureName ); if (message.Envelope != null) { - Write(featureName, Serialize(message.Envelope)); + var cm = Serialize(message.Envelope); + Console.WriteLine("ConsumeAndWriteToFiles: " + cm); + Write(featureName, cm); } else { @@ -37,7 +67,8 @@ private async Task ConsumeAndWriteToFiles() } private Dictionary fileStreams = new(); - private string baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "CucumberMessages"); + private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); + private bool disposedValue; private string Serialize(Envelope message) { @@ -45,9 +76,12 @@ private string Serialize(Envelope message) } private void Write(string featureName, string cucumberMessage) { + string appName = Process.GetCurrentProcess().ProcessName; + string appDirectory = Path.Combine(baseDirectory, appName); + if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(appDirectory, $"{featureName}.ndjson")); } fileStreams[featureName].WriteLine(cucumberMessage); } @@ -57,5 +91,30 @@ private void CloseFeatureStream(string featureName) fileStreams[featureName].Close(); fileStreams.Remove(featureName); } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CloseFileSink(new TestRunFinishedEvent()); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public Task Publish(ReqnrollCucumberMessage message) + { + return sinkBase.Publish(message); + } } } diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index afea6d0f2..7dbb46029 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,6 +10,7 @@ + @@ -17,10 +18,4 @@ - - - ..\..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll - - - diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index c771724cb..f98343066 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -1,4 +1,5 @@ using Reqnroll.BoDi; +using Reqnroll.Tracing; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -19,17 +20,25 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { private List registeredSinks; - + private IObjectContainer _objectContainer; + //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) - { + { + _objectContainer = objectContainer; var sinks = objectContainer.ResolveAll(); registeredSinks = new List(sinks); } public async Task PublishAsync(ReqnrollCucumberMessage message) { + var _traceListener = _objectContainer.Resolve(); + _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); + + foreach (var sink in registeredSinks) - { + { + _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); + await sink.Publish(message); } } @@ -37,6 +46,9 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public async Task CompleteAsync(string cucumberMessageSource) { + var _traceListener = _objectContainer.Resolve(); + _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); + var completionMessage = new ReqnrollCucumberMessage { CucumberMessageSource = cucumberMessageSource @@ -44,6 +56,8 @@ public async Task CompleteAsync(string cucumberMessageSource) foreach (var sink in registeredSinks) { + _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); + await sink.Publish(completionMessage); } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs index de2ae191c..638102fa9 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -8,7 +8,6 @@ namespace Reqnroll.CucumberMesssages { -#if NETSTANDARD2_0 public static class CucumberMessageExtensions { public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, @@ -23,5 +22,4 @@ public static async IAsyncEnumerable ReadAllAsync(this ChannelReader re } } } -#endif } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 063eb906b..833093695 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -6,38 +6,55 @@ using System.Threading.Tasks; using Io.Cucumber.Messages; using Io.Cucumber.Messages.Types; +using Reqnroll.Tracing; namespace Reqnroll.CucumberMesssages { public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; + private IObjectContainer objectContainer; + private bool initialized = false; - public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker) - { + public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) + { + this.objectContainer = objectContainer; broker = CucumberMessageBroker; } public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { + if (initialized) + { + return; + } + initialized = true; + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); + testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { - var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.FolderPath; - Task.Run(() => broker.CompleteAsync(featureName)); + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + broker.CompleteAsync(featureName); } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { - var featureName = featureStartedEvent.FeatureContext.FeatureInfo.FolderPath; - Task.Run(() => broker.PublishAsync(new ReqnrollCucumberMessage + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + + broker.PublishAsync(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) - })); + }); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index 199664475..ea085a694 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -1,5 +1,7 @@ -using Reqnroll.Events; +using Reqnroll.BoDi; +using Reqnroll.Events; using Reqnroll.Plugins; +using Reqnroll.Tracing; using Reqnroll.UnitTestProvider; using System.Collections.Generic; using System.Threading.Channels; @@ -8,11 +10,29 @@ namespace Reqnroll.CucumberMesssages { - public class CucumberMessageSinkBase : ICucumberMessageSink, IRuntimePlugin + public class CucumberMessageSinkBase { + protected IObjectContainer _testThreadContainer; + + private ICucumberMessagePublisher _publisher; private Channel _channel = Channel.CreateUnbounded(); + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + _testThreadContainer = args.ObjectContainer; + _publisher = args.ObjectContainer.Resolve(); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + _publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; + } + public async Task Publish(ReqnrollCucumberMessage message) { + var traceListener = _testThreadContainer.Resolve(); + traceListener.WriteTestOutput($"Cucumber Message Sink publishing message."); + await _channel.Writer.WriteAsync(message); if (message.Envelope == null) { @@ -22,22 +42,14 @@ public async Task Publish(ReqnrollCucumberMessage message) public async IAsyncEnumerable Consume() { + var traceListener = _testThreadContainer.Resolve(); + traceListener.WriteTestOutput($"Cucumber Message Sink Consume() called."); + await foreach (var message in _channel.Reader.ReadAllAsync()) { + // _traceListener.WriteTestOutput($"Cucumber Message Sink consuming message."); yield return message; } } - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - var publisher = args.ObjectContainer.Resolve(); - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); - }; - } - } } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index eb489540e..a4bb84221 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,6 +16,7 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; +using Reqnroll.CucumberMesssages; namespace Reqnroll.Infrastructure { @@ -98,6 +99,10 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + + //Support for publishing Cucumber Messages + container.RegisterTypeAs(); + container.RegisterTypeAs(); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index ed47c9ca2..c9afb5bbe 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,6 +16,7 @@ + @@ -42,12 +43,6 @@ - - - ..\..\cucumber\messages\dotnet\Cucumber.Messages\bin\Debug\netstandard2.0\Cucumber.Messages.dll - - - From 67f70965a0c1b950609db2943d1a1b5c451155a4 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:15:32 -0500 Subject: [PATCH 090/218] Made pulling of registered sinks to be dynamic; eliminated Publisher check of whether it had been initialized (temp). (Temp) - added Debugger.Launch to FileSinkPlugin to force launch of debugger. --- .../FileSinkPlugin.cs | 6 ++++++ Reqnroll/CucumberMesssages/CucumberMessageBroker.cs | 8 ++++---- .../CucumberMesssages/CucumberMessagePublisher.cs | 12 ++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 24c61aff4..6946b10a0 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -19,8 +19,14 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + public FileSinkPlugin() + { + Debugger.Launch(); + } + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index f98343066..ab33ec4a1 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Security.Authentication.ExtendedProtection; using System.Text; using System.Threading.Tasks; @@ -19,22 +20,19 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { - private List registeredSinks; private IObjectContainer _objectContainer; //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; - var sinks = objectContainer.ResolveAll(); - registeredSinks = new List(sinks); } public async Task PublishAsync(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); + var registeredSinks = _objectContainer.ResolveAll().ToList(); _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); - foreach (var sink in registeredSinks) { _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); @@ -46,6 +44,8 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public async Task CompleteAsync(string cucumberMessageSource) { + var registeredSinks = _objectContainer.ResolveAll().ToList(); + var _traceListener = _objectContainer.Resolve(); _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 833093695..fa29eab8d 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -14,7 +14,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private bool initialized = false; + //private bool initialized = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -24,11 +24,11 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - if (initialized) - { - return; - } - initialized = true; + //if (initialized) + //{ + // return; + //} + //initialized = true; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); From 7751a2486a0d4bf6ede241540032fce47c4f6925 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:38:54 -0500 Subject: [PATCH 091/218] Refactored Publisher into an internally loaded PlugIn. Separated the Publisher from the FileSinkPlugIn completely. --- .../CucumberMessagePublisher.cs | 19 +++++++++++++++---- .../CucumberMessageSinkBase.cs | 4 ---- .../DefaultDependencyProvider.cs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index fa29eab8d..7c3c47f2c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -7,20 +7,30 @@ using Io.Cucumber.Messages; using Io.Cucumber.Messages.Types; using Reqnroll.Tracing; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; namespace Reqnroll.CucumberMesssages { - public class CucumberMessagePublisher : ICucumberMessagePublisher + public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; //private bool initialized = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) - { - this.objectContainer = objectContainer; + { broker = CucumberMessageBroker; } + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => + { + objectContainer = args.ObjectContainer; + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; + } public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { @@ -37,6 +47,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); } + private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; @@ -50,7 +61,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - broker.PublishAsync(new ReqnrollCucumberMessage + broker.PublishAsync(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs index ea085a694..3e3057a80 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs @@ -14,7 +14,6 @@ public class CucumberMessageSinkBase { protected IObjectContainer _testThreadContainer; - private ICucumberMessagePublisher _publisher; private Channel _channel = Channel.CreateUnbounded(); public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -22,9 +21,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { _testThreadContainer = args.ObjectContainer; - _publisher = args.ObjectContainer.Resolve(); - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - _publisher.HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); }; } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index a4bb84221..d4f0e2d98 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -102,7 +102,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); - container.RegisterTypeAs(); + container.RegisterTypeAs("CucumberMessagePublisher"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) From b43f05fc303d0fde3446995bbca3dd6ed717b362 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:33:47 -0500 Subject: [PATCH 092/218] Simplified by eliminating use of Threading.Channels; substituting in the use of a BlockingCollection. Eliminated base class for Sinks. Eliminated use of Async for MessageSinks. Sinks have to ensure their own thread-safety and non-blocking behavior. May want to revisit that. --- .../FileSinkPlugin.cs | 58 ++++++++++++------- .../CucumberMessageBroker.cs | 14 +++-- .../CucumberMessagePublisher.cs | 4 +- .../CucumberMessageSinkBase.cs | 51 ---------------- .../CucumberMesssages/ICucumberMessageSink.cs | 2 +- 5 files changed, 49 insertions(+), 80 deletions(-) delete mode 100644 Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 6946b10a0..19364b8a8 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -8,16 +8,24 @@ using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; using System.Diagnostics; using Reqnroll.Events; +using System.Collections.Concurrent; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { + //TODO: Add support for Reqnroll Configuration to initialize the FileSinkPlugin by specifying the path to the base directory. + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private Task? fileWritingTask; - private CucumberMessageSinkBase sinkBase = new CucumberMessageSinkBase(); + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) + + private readonly BlockingCollection postedMessages = new(); + private readonly ConcurrentDictionary fileStreams = new(); public FileSinkPlugin() { @@ -27,7 +35,6 @@ public FileSinkPlugin() public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - sinkBase.Initialize(runtimePluginEvents, runtimePluginParameters, unitTestProviderConfiguration); runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => @@ -40,30 +47,42 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar private void CloseFileSink(TestRunFinishedEvent @event) { + postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; } private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - Console.WriteLine( "LaunchFileSink called" ); - fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + Console.WriteLine("LaunchFileSink called"); + + if (!Directory.Exists(baseDirectory)) + { + Directory.CreateDirectory(baseDirectory); + } + + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + } + + public void Publish(ReqnrollCucumberMessage message) + { + postedMessages.Add(message); } private async Task ConsumeAndWriteToFiles() { - Console.WriteLine( "ConsumeAndWriteToFiles called" ); + Console.WriteLine("ConsumeAndWriteToFiles called"); - await foreach (var message in sinkBase.Consume()) + foreach (var message in postedMessages.GetConsumingEnumerable()) { var featureName = message.CucumberMessageSource; - Console.WriteLine( "ConsumeAndWriteToFiles: " + featureName ); + Console.WriteLine("ConsumeAndWriteToFiles: " + featureName); if (message.Envelope != null) { var cm = Serialize(message.Envelope); Console.WriteLine("ConsumeAndWriteToFiles: " + cm); - Write(featureName, cm); + await Write(featureName, cm); } else { @@ -72,7 +91,6 @@ private async Task ConsumeAndWriteToFiles() } } - private Dictionary fileStreams = new(); private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); private bool disposedValue; @@ -80,22 +98,20 @@ private string Serialize(Envelope message) { return NdjsonSerializer.Serialize(message); } - private void Write(string featureName, string cucumberMessage) + private async Task Write(string featureName, string cucumberMessage) { - string appName = Process.GetCurrentProcess().ProcessName; - string appDirectory = Path.Combine(baseDirectory, appName); if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(appDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); } - fileStreams[featureName].WriteLine(cucumberMessage); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); } private void CloseFeatureStream(string featureName) { fileStreams[featureName].Close(); - fileStreams.Remove(featureName); + fileStreams.TryRemove(featureName, out var _); } protected virtual void Dispose(bool disposing) @@ -105,6 +121,13 @@ protected virtual void Dispose(bool disposing) if (disposing) { CloseFileSink(new TestRunFinishedEvent()); + postedMessages.Dispose(); + foreach (var stream in fileStreams.Values) + { + stream.Close(); + stream.Dispose(); + }; + fileStreams.Clear(); } disposedValue = true; @@ -117,10 +140,5 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - public Task Publish(ReqnrollCucumberMessage message) - { - return sinkBase.Publish(message); - } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index ab33ec4a1..54411bd83 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -14,8 +14,8 @@ namespace Reqnroll.CucumberMesssages public interface ICucumberMessageBroker { - Task CompleteAsync(string cucumberMessageSource); - Task PublishAsync(ReqnrollCucumberMessage message); + void Complete(string cucumberMessageSource); + void Publish(ReqnrollCucumberMessage message); } public class CucumberMessageBroker : ICucumberMessageBroker @@ -27,9 +27,11 @@ public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; } - public async Task PublishAsync(ReqnrollCucumberMessage message) + public void Publish(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); + + //TODO: find a way to populate this list a single time var registeredSinks = _objectContainer.ResolveAll().ToList(); _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); @@ -37,12 +39,12 @@ public async Task PublishAsync(ReqnrollCucumberMessage message) { _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); - await sink.Publish(message); + sink.Publish(message); } } // using an empty CucumberMessage to indicate completion - public async Task CompleteAsync(string cucumberMessageSource) + public void Complete(string cucumberMessageSource) { var registeredSinks = _objectContainer.ResolveAll().ToList(); @@ -58,7 +60,7 @@ public async Task CompleteAsync(string cucumberMessageSource) { _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); - await sink.Publish(completionMessage); + sink.Publish(completionMessage); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 7c3c47f2c..a70a48afb 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -51,7 +51,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - broker.CompleteAsync(featureName); + broker.Complete(featureName); } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) @@ -61,7 +61,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - broker.PublishAsync(new ReqnrollCucumberMessage + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs b/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs deleted file mode 100644 index 3e3057a80..000000000 --- a/Reqnroll/CucumberMesssages/CucumberMessageSinkBase.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Reqnroll.BoDi; -using Reqnroll.Events; -using Reqnroll.Plugins; -using Reqnroll.Tracing; -using Reqnroll.UnitTestProvider; -using System.Collections.Generic; -using System.Threading.Channels; -using System.Threading.Tasks; - - -namespace Reqnroll.CucumberMesssages -{ - public class CucumberMessageSinkBase - { - protected IObjectContainer _testThreadContainer; - - private Channel _channel = Channel.CreateUnbounded(); - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - _testThreadContainer = args.ObjectContainer; - }; - } - - public async Task Publish(ReqnrollCucumberMessage message) - { - var traceListener = _testThreadContainer.Resolve(); - traceListener.WriteTestOutput($"Cucumber Message Sink publishing message."); - - await _channel.Writer.WriteAsync(message); - if (message.Envelope == null) - { - _channel.Writer.Complete(); - } - } - - public async IAsyncEnumerable Consume() - { - var traceListener = _testThreadContainer.Resolve(); - traceListener.WriteTestOutput($"Cucumber Message Sink Consume() called."); - - await foreach (var message in _channel.Reader.ReadAllAsync()) - { - // _traceListener.WriteTestOutput($"Cucumber Message Sink consuming message."); - yield return message; - } - } - } -} diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs index a796bdb24..926e2cf1c 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -5,6 +5,6 @@ namespace Reqnroll.CucumberMesssages { public interface ICucumberMessageSink { - Task Publish(ReqnrollCucumberMessage message); + void Publish(ReqnrollCucumberMessage message); } } From bf9beb973ab10d22a77b89213cba3acbf792c8fd Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 10 Aug 2024 15:28:32 -0500 Subject: [PATCH 093/218] TestRunStarted and TestRunFinished Cucumber Messages supported. --- .../FileSinkPlugin.cs | 2 +- .../CucumberMessagePublisher.cs | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 19364b8a8..7a6165a15 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -29,7 +29,7 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin public FileSinkPlugin() { - Debugger.Launch(); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index a70a48afb..5ff54ceae 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -9,6 +9,8 @@ using Reqnroll.Tracing; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; +using Reqnroll.Time; +using Cucumber.Messages; namespace Reqnroll.CucumberMesssages { @@ -34,11 +36,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - //if (initialized) - //{ - // return; - //} - //initialized = true; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); @@ -51,6 +48,15 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + + var ts = objectContainer.Resolve().GetNowDateAndTime(); + + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) + }); + broker.Complete(featureName); } @@ -61,10 +67,12 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + var ts = objectContainer.Resolve().GetNowDateAndTime(); + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestCaseStarted(1, "1", "2", "0", new Timestamp(1, 1))) + Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); } } From d053088c197062da28f5cb39ebd865c220f0b9eb Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:03:44 -0500 Subject: [PATCH 094/218] Proof of Concept working for the generation and emit of the static Cuke Messages (meta, Source, GherkinDocument, and Pickles). --- .../Generation/UnitTestFeatureGenerator.cs | 46 ++- Reqnroll.Parser/CucumberMessagesConverter.cs | 41 +++ Reqnroll.Parser/ICucumberMessageConverter.cs | 14 + .../CucumberMessageExtensions.cs | 25 -- .../CucumberMessagePublisher.cs | 41 +++ .../CucumberMessageTransformer.cs | 342 ++++++++++++++++++ .../FeatureLevelCucumberMessages.cs | 20 + Reqnroll/FeatureInfo.cs | 6 + Reqnroll/Reqnroll.csproj | 1 - 9 files changed, 509 insertions(+), 27 deletions(-) create mode 100644 Reqnroll.Parser/CucumberMessagesConverter.cs create mode 100644 Reqnroll.Parser/ICucumberMessageConverter.cs delete mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs create mode 100644 Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 404d22d75..9741738ea 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -3,7 +3,9 @@ using System.Globalization; using System.Linq; using System.Reflection; +using Gherkin.CucumberMessages; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; @@ -171,7 +173,7 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Name = GeneratorConstants.TESTCLASS_INITIALIZE_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testClassInitializeMethod); - + _testGeneratorProvider.SetTestClassInitializeMethod(generationContext); //testRunner = TestRunnerManager.GetTestRunnerForAssembly(null, [test_worker_id]); @@ -200,6 +202,8 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio _codeDomHelper.TargetLanguage.ToString()), new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)))); + PersistStaticCucumberMessagesToFeatureInfo(generationContext, testClassInitializeMethod); + //await testRunner.OnFeatureStartAsync(featureInfo); var onFeatureStartExpression = new CodeMethodInvokeExpression( testRunnerField, @@ -211,6 +215,46 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Statements.Add(onFeatureStartExpression); } + private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) + { + var CucumberMessagesInitializeMethod = new CodeMemberMethod(); + CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + CucumberMessagesInitializeMethod.Name = "CucumberMessagesInitializeMethod"; + CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); + generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); + + //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); + var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); + var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(generationContext.Document); + + // Serialize the Cucumber Messages to strings + var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); + var featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); + var featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); + + // Create a FeatureLevelCucumberMessages object and add it to featureInfo + var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), + new CodePrimitiveExpression(featureSourceMessageString), + new CodePrimitiveExpression(featureGherkinDocumentMessageString), + new CodePrimitiveExpression(featurePickleMessagesString)); + CucumberMessagesInitializeMethod.Statements.Add( + new CodeAssignStatement( + new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), + featureLevelCucumberMessagesExpression)); + + // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod + var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( + null, + "CucumberMessagesInitializeMethod", + new CodeVariableReferenceExpression("featureInfo")); + + // Add the CodeMethodInvokeExpression to the testClassInitializeMethod statements + testClassInitializeMethod.Statements.Add(invokeCucumberMessagesInitializeMethod); + + } + private void SetupTestClassCleanupMethod(TestClassGenerationContext generationContext) { var testClassCleanupMethod = generationContext.TestClassCleanupMethod; diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs new file mode 100644 index 000000000..9ccf304c1 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser +{ + public class CucumberMessagesConverter : ICucumberMessagesConverters + { + private IIdGenerator _idGenerator; + + public CucumberMessagesConverter(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) + { + var converter = new AstMessagesConverter(_idGenerator); + var location = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)); + return converter.ConvertGherkinDocumentToEventArgs(gherkinDocument, location); + } + + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) + { + var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + return new Source + { + Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), + Data = sourceText, + MediaType = "text/x.cucumber.gherkin+plain" + }; + } + + public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument) + { + var pickleCompiler = new Gherkin.CucumberMessages.Pickles.PickleCompiler(_idGenerator); + var gd = ConvertToCucumberMessagesGherkinDocument(gherkinDocument); + return pickleCompiler.Compile(gd); + } + } +} diff --git a/Reqnroll.Parser/ICucumberMessageConverter.cs b/Reqnroll.Parser/ICucumberMessageConverter.cs new file mode 100644 index 000000000..3198c0843 --- /dev/null +++ b/Reqnroll.Parser/ICucumberMessageConverter.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser +{ + public interface ICucumberMessagesConverters + { + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument); + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument); + public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument); + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs deleted file mode 100644 index 638102fa9..000000000 --- a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable - -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Channels; - - -namespace Reqnroll.CucumberMesssages -{ - public static class CucumberMessageExtensions - { - public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) - { - while (reader.TryRead(out T? item)) - { - yield return item; - } - } - } - } -} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5ff54ceae..94fc5ab96 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -69,6 +69,47 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var ts = objectContainer.Resolve().GetNowDateAndTime(); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(new Meta( + Cucumber.Messages.ProtocolVersion.Version, + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + null)) + }); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(messageSource) + }); + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(gherkinDocument) + }); + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + foreach (var pickle in pickles) + { + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(pickle) + }); + } + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs new file mode 100644 index 000000000..4b95b9be1 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -0,0 +1,342 @@ +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reqnroll.CucumberMesssages +{ + internal class CucumberMessageTransformer + { + internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessages.Types.Source gherkinSource) + { + var result = new Io.Cucumber.Messages.Types.Source + ( + gherkinSource.Uri, + gherkinSource.Data, + gherkinSource.MediaType == "text/x.cucumber.gherkin+plain" ? Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN : Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN + ); + return result; + } + + internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) + { + var result = new Io.Cucumber.Messages.Types.GherkinDocument + ( + gherkinDoc.Uri, + CucumberMessageTransformer.ToFeature(gherkinDoc.Feature), + CucumberMessageTransformer.ToComments(gherkinDoc.Comments) + ); + return result; + } + + private static Io.Cucumber.Messages.Types.Feature ToFeature(Gherkin.CucumberMessages.Types.Feature feature) + { + if (feature == null) + { + return null; + } + + var children = feature.Children.Select(ToFeatureChild).ToList(); + var tags = feature.Tags.Select(ToTag).ToList(); + + return new Io.Cucumber.Messages.Types.Feature( + ToLocation(feature.Location), + tags, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + children) + ; + } + + private static Io.Cucumber.Messages.Types.Location ToLocation(Gherkin.CucumberMessages.Types.Location location) + { + if (location == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Location(location.Line, location.Column); + } + + + private static Io.Cucumber.Messages.Types.Tag ToTag(Gherkin.CucumberMessages.Types.Tag tag) + { + if (tag == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Tag(ToLocation(tag.Location), tag.Name, tag.Id); + } + + private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(Gherkin.CucumberMessages.Types.FeatureChild child) + { + if (child == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.FeatureChild + ( + ToRule(child.Rule), + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static Io.Cucumber.Messages.Types.Scenario ToScenario(Gherkin.CucumberMessages.Types.Scenario scenario) + { + if (scenario == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Scenario + ( + ToLocation(scenario.Location), + scenario.Tags.Select(ToTag).ToList(), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.Select(ToStep).ToList(), + scenario.Examples.Select(ToExamples).ToList(), + scenario.Id + ); + } + + private static Io.Cucumber.Messages.Types.Examples ToExamples(Gherkin.CucumberMessages.Types.Examples examples) + { + if (examples == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Examples( + ToLocation(examples.Location), + examples.Tags.Select(ToTag).ToList(), + examples.Keyword, + examples.Name, + examples.Description, + ToTableRow(examples.TableHeader), + examples.TableBody.Select(ToTableRow).ToList(), + examples.Id + ); + } + private static Io.Cucumber.Messages.Types.TableCell ToTableCell(Gherkin.CucumberMessages.Types.TableCell cell) + { + return new Io.Cucumber.Messages.Types.TableCell( + ToLocation(cell.Location), + cell.Value + ); + } + + private static Io.Cucumber.Messages.Types.TableRow ToTableRow(Gherkin.CucumberMessages.Types.TableRow row) + { + return new Io.Cucumber.Messages.Types.TableRow( + ToLocation(row.Location), + row.Cells.Select(ToTableCell).ToList(), + row.Id + ); + } + private static Io.Cucumber.Messages.Types.Step ToStep(Gherkin.CucumberMessages.Types.Step step) + { + if (step == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Step( + ToLocation(step.Location), + step.Keyword, + ToKeyWordType(step.KeywordType), + step.Text, + step.DocString == null ? null : ToDocString(step.DocString), + step.DataTable == null ? null : ToDataTable(step.DataTable), + step.Id + ); + } + + private static Io.Cucumber.Messages.Types.Background ToBackground(Gherkin.CucumberMessages.Types.Background background) + { + if (background == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Background( + ToLocation(background.Location), + background.Keyword, + background.Name, + background.Description, + background.Steps.Select(ToStep).ToList(), + background.Id + ); + } + + private static Io.Cucumber.Messages.Types.Rule ToRule(Gherkin.CucumberMessages.Types.Rule rule) + { + if (rule == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Rule( + ToLocation(rule.Location), + rule.Tags.Select(ToTag).ToList(), + rule.Keyword, + rule.Name, + rule.Description, + rule.Children.Select(ToRuleChild).ToList(), + rule.Id + ); + } + + private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(Gherkin.CucumberMessages.Types.RuleChild child) + { + return new Io.Cucumber.Messages.Types.RuleChild( + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static List ToComments(IReadOnlyCollection comments) + { + return comments.Select(ToComment).ToList(); + } + + private static Io.Cucumber.Messages.Types.Comment ToComment(Gherkin.CucumberMessages.Types.Comment comment) + { + return new Io.Cucumber.Messages.Types.Comment( + ToLocation(comment.Location), + comment.Text + ); + } + private static Io.Cucumber.Messages.Types.StepKeywordType ToKeyWordType(Gherkin.StepKeywordType keywordType) + { + return keywordType switch + { + //case Gherkin.StepKeywordType.Unspecified: + // return Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN; + Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.StepKeywordType.CONTEXT, + Gherkin.StepKeywordType.Conjunction => Io.Cucumber.Messages.Types.StepKeywordType.CONJUNCTION, + Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.StepKeywordType.ACTION, + Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.StepKeywordType.OUTCOME, + Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN, + _ => throw new ArgumentException($"Invalid keyword type: {keywordType}"), + }; + } + + private static Io.Cucumber.Messages.Types.DocString ToDocString(Gherkin.CucumberMessages.Types.DocString docString) + { + return new Io.Cucumber.Messages.Types.DocString( + ToLocation(docString.Location), + docString.MediaType, + docString.Content, + docString.Delimiter + ); + } + + private static Io.Cucumber.Messages.Types.DataTable ToDataTable(Gherkin.CucumberMessages.Types.DataTable dataTable) + { + return new Io.Cucumber.Messages.Types.DataTable( + ToLocation(dataTable.Location), + dataTable.Rows.Select(ToTableRow).ToList() + ); + } + + internal static List ToPickles(IReadOnlyCollection pickles) + { + return pickles.Select(ToPickle).ToList(); + } + + private static Io.Cucumber.Messages.Types.Pickle ToPickle(Gherkin.CucumberMessages.Types.Pickle pickle) + { + return new Io.Cucumber.Messages.Types.Pickle( + pickle.Id, + pickle.Uri, + pickle.Name, + pickle.Language, + pickle.Steps.Select(ToPickleStep).ToList(), + pickle.Tags.Select(ToPickleTag).ToList(), + pickle.AstNodeIds.ToList() + ); + } + private static Io.Cucumber.Messages.Types.PickleTag ToPickleTag(Gherkin.CucumberMessages.Types.PickleTag pickleTag) + { + return new Io.Cucumber.Messages.Types.PickleTag( + pickleTag.Name, + pickleTag.AstNodeId + ); + } + private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(Gherkin.CucumberMessages.Types.PickleStep pickleStep) + { + return new Io.Cucumber.Messages.Types.PickleStep( + ToPickleStepArgument(pickleStep.Argument), + pickleStep.AstNodeIds.ToList(), + pickleStep.Id, + ToPickleStepType(pickleStep.Type), + pickleStep.Text + ); + } + private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgument(Gherkin.CucumberMessages.Types.PickleStepArgument pickleStepArgument) + { + if (pickleStepArgument == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleStepArgument( + ToPickleDocString(pickleStepArgument.DocString), + ToPickleTable(pickleStepArgument.DataTable) + ); + } + + private static Io.Cucumber.Messages.Types.PickleStepType ToPickleStepType(Gherkin.StepKeywordType pickleStepType) + { + return pickleStepType switch + { + Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.PickleStepType.UNKNOWN, + Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.PickleStepType.ACTION, + Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.PickleStepType.OUTCOME, + Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.PickleStepType.CONTEXT, + _ => throw new ArgumentException($"Invalid pickle step type: {pickleStepType}") + }; + } + private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gherkin.CucumberMessages.Types.PickleDocString pickleDocString) + { + if (pickleDocString == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleDocString( + pickleDocString.Content, + pickleDocString.MediaType + ); + } + + private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(Gherkin.CucumberMessages.Types.PickleTable pickleTable) + { + if (pickleTable == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleTable( + pickleTable.Rows.Select(ToPickleTableRow).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableRow ToPickleTableRow(Gherkin.CucumberMessages.Types.PickleTableRow pickleTableRow) + { + return new Io.Cucumber.Messages.Types.PickleTableRow( + pickleTableRow.Cells.Select(ToPickleTableCell).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableCell ToPickleTableCell(Gherkin.CucumberMessages.Types.PickleTableCell pickleTableCell) + { + return new Io.Cucumber.Messages.Types.PickleTableCell( + pickleTableCell.Value + ); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs new file mode 100644 index 000000000..bdf39518d --- /dev/null +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public class FeatureLevelCucumberMessages + { + public FeatureLevelCucumberMessages(string source, string gkerkinDocument, string pickles) + { + Source = source; + GherkinDocument = gkerkinDocument; + Pickles = pickles; + } + + public string Source { get; set; } + public string GherkinDocument { get; set; } + public string Pickles { get; set; } + } +} diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index cdb62f7b7..dcdbc9cea 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Reqnroll.CucumberMesssages; using Reqnroll.Tracing; namespace Reqnroll @@ -15,6 +16,11 @@ public class FeatureInfo public string Description { get; private set; } public CultureInfo Language { get; private set; } + + // TODO: Add this to the constructor and update all the related tests + // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired + public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } + public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) { diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index c9afb5bbe..583dd298c 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -30,7 +30,6 @@ - From 5d4263299d2baa2b3c4256d18d10ecb6641dbc04 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:57:20 -0500 Subject: [PATCH 095/218] Added CucumberMessageFactory as a draft method of converting Reqnroll information into Messages. First example is transforming StepBinding classes into messages of type StepDefinition. --- .../CucumberMessageFactory.cs | 31 +++++++++++++++++++ .../CucumberMessagePublisher.cs | 17 ++++++++++ .../CucumberCompatibilityTests.cs | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageFactory.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs new file mode 100644 index 000000000..1405a7b32 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -0,0 +1,31 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + internal class CucumberMessageFactory + { + internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) + { + var bindingSourceText = binding.SourceExpression; + var expressionType = binding.ExpressionType; + var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; + var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + + var methodName = binding.Method.Name; + var className = binding.Method.Type.Name; + var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); + var methodDescription = new JavaMethod(className, className, paramTypes); + var sourceRef = SourceReference.Create(methodDescription); + + var result = new StepDefinition + ( + "XX", // TODO: Generate an ID for this + stepDefinitionPattern, + sourceRef + ); + return result; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 94fc5ab96..ba823d572 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -11,6 +11,7 @@ using Reqnroll.UnitTestProvider; using Reqnroll.Time; using Cucumber.Messages; +using Reqnroll.Bindings; namespace Reqnroll.CucumberMesssages { @@ -110,11 +111,27 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) }); } + var bindingRegistry = objectContainer.Resolve(); + if (bindingRegistry.IsValid) + { + foreach (var binding in bindingRegistry.GetStepDefinitions()) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); + broker.Publish(new ReqnrollCucumberMessage + { + CucumberMessageSource = featureName, + Envelope = Envelope.Create(stepDefinition) + }); + } + } + broker.Publish(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); + + // throw new ApplicationException(); } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d45c31fa5..1a362738c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -14,7 +14,7 @@ public void SmokeTest() Given I have a passing step """); - AddPassingStepBinding(); + AddPassingStepBinding("Given"); ExecuteTests(); From 21a44ae2ee6fd858e4358f7417b295286ef171b3 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:10:35 -0500 Subject: [PATCH 096/218] Fixed a bug in how the cucumber messages were created by the UnitTestGenerator. --- Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs | 6 +++--- Reqnroll.Parser/CucumberMessagesConverter.cs | 5 ++--- Reqnroll.Parser/ICucumberMessageConverter.cs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 9741738ea..52d41c8e9 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -219,7 +219,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte { var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; - CucumberMessagesInitializeMethod.Name = "CucumberMessagesInitializeMethod"; + CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); @@ -227,7 +227,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); - var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); // Serialize the Cucumber Messages to strings var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); @@ -247,7 +247,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( null, - "CucumberMessagesInitializeMethod", + CucumberMessagesInitializeMethod.Name, new CodeVariableReferenceExpression("featureInfo")); // Add the CodeMethodInvokeExpression to the testClassInitializeMethod statements diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 9ccf304c1..ec9e26af7 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -31,11 +31,10 @@ public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) }; } - public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument) + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument) { var pickleCompiler = new Gherkin.CucumberMessages.Pickles.PickleCompiler(_idGenerator); - var gd = ConvertToCucumberMessagesGherkinDocument(gherkinDocument); - return pickleCompiler.Compile(gd); + return pickleCompiler.Compile(gherkinDocument); } } } diff --git a/Reqnroll.Parser/ICucumberMessageConverter.cs b/Reqnroll.Parser/ICucumberMessageConverter.cs index 3198c0843..b08a3773c 100644 --- a/Reqnroll.Parser/ICucumberMessageConverter.cs +++ b/Reqnroll.Parser/ICucumberMessageConverter.cs @@ -9,6 +9,6 @@ public interface ICucumberMessagesConverters { public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument); public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument); - public IEnumerable ConvertToCucumberMessagesPickles(ReqnrollDocument gherkinDocument); + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument); } } From 1a63998dedf14825899ef99bdbe76b88db1c8e0e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:07:05 -0500 Subject: [PATCH 097/218] Fixed bug that was causing a namespace conflict with the Specs project. --- Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 52d41c8e9..458926d61 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -220,7 +220,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; - CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression("FeatureInfo", "featureInfo")); + CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo From 751c315f3c121af8e76f1494b5f2c55e37fb1f53 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:51:53 -0500 Subject: [PATCH 098/218] When Pickles can't be created (such as with ExternalData), suspend support for CucumberMessages for that feature. --- .../Generation/UnitTestFeatureGenerator.cs | 32 +++++++++++++------ .../CucumberMessagePublisher.cs | 17 +++++++++- Reqnroll/CucumberMesssages/FeatureState.cs | 8 +++++ 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/FeatureState.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 458926d61..1e17b09b3 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -217,22 +217,34 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) { + string featureSourceMessageString = null; + string featureGherkinDocumentMessageString = null; + string featurePickleMessagesString = null; + try + { + //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); + var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); + var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); + + // Serialize the Cucumber Messages to strings + featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); + featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); + featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); + } + catch + { + // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. + return; + } + // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object var CucumberMessagesInitializeMethod = new CodeMemberMethod(); CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); - //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo - var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); - var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); - var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); - var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); - - // Serialize the Cucumber Messages to strings - var featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); - var featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); - var featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); // Create a FeatureLevelCucumberMessages object and add it to featureInfo var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index ba823d572..1c971e473 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -19,7 +19,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - //private bool initialized = false; + private Dictionary featureState = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -49,6 +49,9 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + var featureState = this.featureState[featureName]; + if (!featureState.Enabled) + return; var ts = objectContainer.Resolve().GetNowDateAndTime(); @@ -64,10 +67,22 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + var featureState = new FeatureState + { + Name = featureName, + Enabled = enabled + }; + + this.featureState[featureName] = featureState; var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + if (!enabled) + return; + var ts = objectContainer.Resolve().GetNowDateAndTime(); broker.Publish(new ReqnrollCucumberMessage diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs new file mode 100644 index 000000000..ebb6d46f5 --- /dev/null +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.CucumberMesssages +{ + internal class FeatureState + { + public string Name { get; set; } + public bool Enabled { get; set; } //This will be false if the feature could not be pickled + } +} \ No newline at end of file From 0478bb0380e87b8fb22feae2ffa904f5af76dc81 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:42:54 -0500 Subject: [PATCH 099/218] Messages are cached until the last thread is finished with the Feature, then all are published. --- .../CucumberMessagePublisher.cs | 52 +++++++++++++++---- .../CucumberMessageTransformer.cs | 5 ++ Reqnroll/CucumberMesssages/FeatureState.cs | 17 +++++- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1c971e473..663c63c69 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -12,6 +12,11 @@ using Reqnroll.Time; using Cucumber.Messages; using Reqnroll.Bindings; +using System.Reflection; +using ScenarioNameIDMap = System.Collections.Generic.Dictionary; +using StepPatternIDMap = System.Collections.Generic.Dictionary; +using TestCaseToPickleMap = System.Collections.Generic.Dictionary; +using System.Collections.Concurrent; namespace Reqnroll.CucumberMesssages { @@ -19,7 +24,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private Dictionary featureState = new(); + private ConcurrentDictionary featureStatesByFeatureName = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -49,18 +54,36 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = this.featureState[featureName]; + var featureState = featureStatesByFeatureName[featureName]; + featureState.workerThreadMarkers.TryPop(out int result); + + lock (featureState) + { + if (featureState.workerThreadMarkers.TryPeek(out result)) + { + // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet + return; + } + } + + if (!featureState.Enabled) return; var ts = objectContainer.Resolve().GetNowDateAndTime(); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, + //TODO: add feature status Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) }); + foreach (var message in featureState.Messages) + { + broker.Publish(message); + } + broker.Complete(featureName); } @@ -75,7 +98,15 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Enabled = enabled }; - this.featureState[featureName] = featureState; + if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + { + // This feature has already been started by another thread (executing a different scenario) + var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well + + // None of the rest of this method should be executed + return; + } var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); @@ -85,7 +116,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var ts = objectContainer.Resolve().GetNowDateAndTime(); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new Meta( @@ -99,7 +130,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(messageSource) @@ -108,7 +139,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(gherkinDocument) @@ -119,7 +150,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) foreach (var pickle in pickles) { - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(pickle) @@ -132,7 +163,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(stepDefinition) @@ -140,13 +171,12 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) } } - broker.Publish(new ReqnrollCucumberMessage + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) }); - // throw new ApplicationException(); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index 4b95b9be1..17ccc9d7e 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -9,6 +9,11 @@ namespace Reqnroll.CucumberMesssages { + /// + /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace + /// + /// once the Gherkin project is updated to directly consume and produce Cucumber messages, this class can be removed + /// internal class CucumberMessageTransformer { internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessages.Types.Source gherkinSource) diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index ebb6d46f5..8ae47e19e 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,8 +1,23 @@ -namespace Reqnroll.CucumberMesssages +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMesssages { internal class FeatureState { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled + + // These two flags are used to avoid duplicate messages being sent when Scenarios within Features are run concurrently + // and multiple FeatureStartedEvent and FeatureFinishedEvent events are fired + public bool Started { get; set; } + public bool Finished { get; set; } + + public Dictionary ScenarioNameIDMap = new Dictionary(); // + public Dictionary StepPatternIDMap = new Dictionary(); // + + public ConcurrentQueue Messages = new ConcurrentQueue(); + public ConcurrentStack workerThreadMarkers = new ConcurrentStack(); } } \ No newline at end of file From 3cf686be004339afcae9d2d123b5818e0452d74e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 18 Aug 2024 10:31:29 -0500 Subject: [PATCH 100/218] Slight change to handling of FeatureFiinished event for thread safety. --- Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 663c63c69..419530f1f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -55,10 +55,13 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - featureState.workerThreadMarkers.TryPop(out int result); lock (featureState) { + // Remove the worker thread marker for this thread + featureState.workerThreadMarkers.TryPop(out int result); + + // Check if there are other threads still working on this feature if (featureState.workerThreadMarkers.TryPeek(out result)) { // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet From f79ea6e1d83b5347ff9ea974ed48ee0ae0c91287 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:08:53 -0500 Subject: [PATCH 101/218] Initial setup of ScenarioStart and Finish. --- .../CucumberMessageFactory.cs | 7 ++- .../CucumberMessagePublisher.cs | 63 ++++++++++++++++--- Reqnroll/CucumberMesssages/FeatureState.cs | 20 ++++-- .../CucumberMesssages/IdGeneratorFactory.cs | 43 +++++++++++++ Reqnroll/CucumberMesssages/ScenarioState.cs | 43 +++++++++++++ 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/IdGeneratorFactory.cs create mode 100644 Reqnroll/CucumberMesssages/ScenarioState.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 1405a7b32..4886df10c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,4 +1,5 @@ -using Io.Cucumber.Messages.Types; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; using System.Linq; @@ -6,7 +7,7 @@ namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { - internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) + internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { var bindingSourceText = binding.SourceExpression; var expressionType = binding.ExpressionType; @@ -21,7 +22,7 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding) var result = new StepDefinition ( - "XX", // TODO: Generate an ID for this + idGenerator.GetNewId(), stepDefinitionPattern, sourceRef ); diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 419530f1f..4fdce34e2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -13,10 +13,8 @@ using Cucumber.Messages; using Reqnroll.Bindings; using System.Reflection; -using ScenarioNameIDMap = System.Collections.Generic.Dictionary; -using StepPatternIDMap = System.Collections.Generic.Dictionary; -using TestCaseToPickleMap = System.Collections.Generic.Dictionary; using System.Collections.Concurrent; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -48,9 +46,10 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); + testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); + testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); } - private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; @@ -117,8 +116,6 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - var ts = objectContainer.Resolve().GetNowDateAndTime(); - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -151,8 +148,13 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + string lastID = ExtractID(pickles); + featureState.IDGenerator = IdGeneratorFactory.Create(lastID); + foreach (var pickle in pickles) { + featureState.PicklesByScenarioName.Add(pickle.Name, pickle); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -165,7 +167,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { foreach (var binding in bindingRegistry.GetStepDefinitions()) { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding); + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, featureState.IDGenerator); + var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + featureState.StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, @@ -177,9 +182,51 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(ts))) + Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp))) }); } + + private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + { + var sr = stepDefinition.SourceReference; + var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + return $"{stepDefinition.Pattern}({signature})"; + } + + private string ExtractID(List pickles) + { + return pickles.Last().Id; + } + + private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + { + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; + var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; + + var featureState = featureStatesByFeatureName[featureName]; + + var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, featureState); + featureState.ScenarioName2StateMap.Add(scenarioName, scenarioState); + + foreach (Envelope e in scenarioState.ProcessEvent(scenarioStartedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } + } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + { + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; + var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; + var scenarioState = featureState.ScenarioName2StateMap[scenarioName]; + foreach (Envelope e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } + } + + } } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 8ae47e19e..b0ab44439 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,4 +1,6 @@ -using System.Collections; +using Gherkin.CucumberMessages; +using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -14,10 +16,18 @@ internal class FeatureState public bool Started { get; set; } public bool Finished { get; set; } - public Dictionary ScenarioNameIDMap = new Dictionary(); // - public Dictionary StepPatternIDMap = new Dictionary(); // + //ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator + public IIdGenerator IDGenerator { get; set; } - public ConcurrentQueue Messages = new ConcurrentQueue(); - public ConcurrentStack workerThreadMarkers = new ConcurrentStack(); + //Lookup tables + public Dictionary StepDefinitionsByPattern = new(); + public Dictionary PicklesByScenarioName = new(); + + public Dictionary ScenarioName2StateMap = new(); + + public ConcurrentQueue Messages = new(); + public ConcurrentStack workerThreadMarkers = new(); } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs new file mode 100644 index 000000000..8ff9bc652 --- /dev/null +++ b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs @@ -0,0 +1,43 @@ +using Gherkin.CucumberMessages; +using System; +using System.Threading; +namespace Reqnroll.CucumberMesssages +{ + public class IdGeneratorFactory + { + public static IIdGenerator Create(string previousId) + { + if (Guid.TryParse(previousId, out var _)) + { + return new GuidIdGenerator(); + } + else + { + return new SeedableIncrementingIdGenerator(int.Parse(previousId)); + } + } + } + + public class SeedableIncrementingIdGenerator : IIdGenerator + { + public SeedableIncrementingIdGenerator(int seed) + { + _counter = seed; + } + + private int _counter = 0; + + public string GetNewId() + { + // Using thread-safe incrementing in case scenarios are running in parallel + var nextId = Interlocked.Increment(ref _counter); + return nextId.ToString(); + } + + + public void Reset() + { + _counter = 0; + } + } +} diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs new file mode 100644 index 000000000..f097d3905 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -0,0 +1,43 @@ +using Cucumber.Messages; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMesssages +{ + internal class ScenarioState + { + private readonly IIdGenerator _idGenerator; + private string TestCaseStartedID; + + public string Name { get; set; } + public string TestCaseID { get; set; } + public string PickleID { get; set; } + + public ScenarioState(IScenarioContext context, FeatureState featureState) + { + _idGenerator = featureState.IDGenerator; + + Name = context.ScenarioInfo.Title; + TestCaseID = _idGenerator.GetNewId(); + PickleID = featureState.PicklesByScenarioName[Name].Id; + } + + internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + TestCaseStartedID = _idGenerator.GetNewId(); + + //TODO: move Message creation to the CucumberMessageFactory + yield return Envelope.Create(new TestCase(TestCaseID, PickleID, new List())); + yield return Envelope.Create(new TestCaseStarted(0, TestCaseStartedID, TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp))); + } + + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + //TODO: move Message creation to the CucumberMessageFactory + + yield return Envelope.Create(new TestCaseFinished(TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false)); + } + } +} \ No newline at end of file From 0d89be1f0a7e3feeae5dad901cc7189691f4b5fe Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:17:57 -0500 Subject: [PATCH 102/218] Refactored Message creation to Factory class. FeatureState and ScenarioState emit sequences of Envelopes for consumption by the Publisher. --- .../CucumberMessageFactory.cs | 28 ++++++- .../CucumberMessagePublisher.cs | 82 +------------------ Reqnroll/CucumberMesssages/FeatureState.cs | 72 +++++++++++++++- Reqnroll/CucumberMesssages/ScenarioState.cs | 11 +-- 4 files changed, 104 insertions(+), 89 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 4886df10c..e19e1646c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,12 +1,38 @@ -using Gherkin.CucumberMessages; +using Cucumber.Messages; +using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Generic; using System.Linq; namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { + public static TestRunStarted ToTestRunStarted(FeatureState featureState, FeatureStartedEvent featureStartedEvent) + { + return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); + } + internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + { + var testCase = new TestCase + ( + scenarioState.TestCaseID, + scenarioState.PickleID, + new List() + ); + return testCase; + } + internal static TestCaseStarted ToTestCaseStarted(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + { + return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); + } + internal static TestCaseFinished ToTestCaseFinished(ScenarioState scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + { + return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); + } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { var bindingSourceText = binding.SourceExpression; diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 4fdce34e2..5b0c0bd8b 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -116,88 +116,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(new Meta( - Cucumber.Messages.ProtocolVersion.Version, - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - null)) - }); - - Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(messageSource) - }); - - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(gherkinDocument) - }); - - var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); - var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); - - string lastID = ExtractID(pickles); - featureState.IDGenerator = IdGeneratorFactory.Create(lastID); - - foreach (var pickle in pickles) + foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) { - featureState.PicklesByScenarioName.Add(pickle.Name, pickle); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(pickle) - }); - } - - var bindingRegistry = objectContainer.Resolve(); - if (bindingRegistry.IsValid) - { - foreach (var binding in bindingRegistry.GetStepDefinitions()) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, featureState.IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); - featureState.StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(stepDefinition) - }); - } + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage - { - CucumberMessageSource = featureName, - Envelope = Envelope.Create(new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp))) - }); - - } - - private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) - { - var sr = stepDefinition.SourceReference; - var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; - - return $"{stepDefinition.Pattern}({signature})"; - } - - private string ExtractID(List pickles) - { - return pickles.Last().Id; } private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index b0ab44439..643d1db30 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -1,8 +1,14 @@ -using Gherkin.CucumberMessages; +using Cucumber.Messages; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.BoDi; +using Reqnroll.Events; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -29,5 +35,69 @@ internal class FeatureState public ConcurrentQueue Messages = new(); public ConcurrentStack workerThreadMarkers = new(); + + internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) + { + yield return Envelope.Create(new Meta( + Cucumber.Messages.ProtocolVersion.Version, + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + new Product("placeholder", "placeholder"), + null)); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + yield return Envelope.Create(messageSource); + + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + yield return Envelope.Create(gherkinDocument); + + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + string lastID = ExtractLastID(pickles); + IDGenerator = IdGeneratorFactory.Create(lastID); + + foreach (var pickle in pickles) + { + PicklesByScenarioName.Add(pickle.Name, pickle); + yield return Envelope.Create(pickle); + } + + var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + if (bindingRegistry.IsValid) + { + foreach (var binding in bindingRegistry.GetStepDefinitions()) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + + yield return Envelope.Create(stepDefinition); + } + } + + yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); + + } + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + + private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + { + var sr = stepDefinition.SourceReference; + var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + return $"{stepDefinition.Pattern}({signature})"; + } + + + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index f097d3905..cd8ffd9da 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -9,8 +9,8 @@ namespace Reqnroll.CucumberMesssages internal class ScenarioState { private readonly IIdGenerator _idGenerator; - private string TestCaseStartedID; + public string TestCaseStartedID; public string Name { get; set; } public string TestCaseID { get; set; } public string PickleID { get; set; } @@ -28,16 +28,13 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted { TestCaseStartedID = _idGenerator.GetNewId(); - //TODO: move Message creation to the CucumberMessageFactory - yield return Envelope.Create(new TestCase(TestCaseID, PickleID, new List())); - yield return Envelope.Create(new TestCaseStarted(0, TestCaseStartedID, TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp))); + yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { - //TODO: move Message creation to the CucumberMessageFactory - - yield return Envelope.Create(new TestCaseFinished(TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); } } } \ No newline at end of file From c877eb297de7f602f41ec5e8a24e1ea7257261b7 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:15:15 -0500 Subject: [PATCH 103/218] Refactor: deferred processing of all Scenario and step events until after scenario completion so that we have full binding Match info available to create the TestCase message. --- .../CucumberMessageFactory.cs | 2 +- Reqnroll/CucumberMesssages/FeatureState.cs | 18 ++++--- Reqnroll/CucumberMesssages/ScenarioState.cs | 50 +++++++++++++++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index e19e1646c..28d61ebac 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -21,7 +21,7 @@ internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStarted ( scenarioState.TestCaseID, scenarioState.PickleID, - new List() + new List () ); return testCase; } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 643d1db30..9c82f3d91 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -74,7 +74,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(stepDefinition); + var pattern = CanonicalizeStepDefinitionPattern(binding); StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); @@ -89,15 +89,21 @@ private string ExtractLastID(List pickles) return pickles.Last().Id; } - private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + //private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) + //{ + // var sr = stepDefinition.SourceReference; + // var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + // return $"{stepDefinition.Pattern}({signature})"; + //} + public string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { - var sr = stepDefinition.SourceReference; - var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + + var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; - return $"{stepDefinition.Pattern}({signature})"; + return $"{stepDefinition.SourceExpression}({signature})"; } - } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index cd8ffd9da..72bb781c3 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -1,8 +1,11 @@ using Cucumber.Messages; using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; using Reqnroll.Events; +using System; using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMesssages { @@ -14,6 +17,13 @@ internal class ScenarioState public string Name { get; set; } public string TestCaseID { get; set; } public string PickleID { get; set; } + public Pickle Pickle { get; set; } + + // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages + private Queue _events = new(); + + // this holds the step definitions bindings that were executed by each step in this scenario. this will be used to find the Cucumber stepDefinitions that were used + private Queue _stepBindingsAsUsed = new(); public ScenarioState(IScenarioContext context, FeatureState featureState) { @@ -21,20 +31,54 @@ public ScenarioState(IScenarioContext context, FeatureState featureState) Name = context.ScenarioInfo.Title; TestCaseID = _idGenerator.GetNewId(); + Pickle = featureState.PicklesByScenarioName[Name]; PickleID = featureState.PicklesByScenarioName[Name].Id; } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { TestCaseStartedID = _idGenerator.GetNewId(); + _events.Enqueue(scenarioStartedEvent); + return Enumerable.Empty(); + } - yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + { + _events.Enqueue(stepStartedEvent); + return Enumerable.Empty(); + } + + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + _events.Enqueue(stepFinishedEvent); + _stepBindingsAsUsed.Enqueue(stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding); + + return Enumerable.Empty(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); + _events.Enqueue(scenarioFinishedEvent); + + while (_events.Count > 0) + { + var executionEvent = _events.Dequeue(); + + switch (executionEvent) + { + case ScenarioStartedEvent scenarioStartedEvent: + yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + break; + case ScenarioFinishedEvent scenarioFinished: + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); + break; + // add more cases for other event types + default: + throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); + } + } + } } } \ No newline at end of file From 0d9d70a72e1a09bb7451da8eda287636e1876020 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:32:23 -0500 Subject: [PATCH 104/218] Completed structure of event processors and state tracker objects for simple scenarios. --- .../CucumberMessageFactory.cs | 95 +++++++++++++++- .../CucumberMessagePublisher.cs | 103 ++++++++++-------- Reqnroll/CucumberMesssages/FeatureState.cs | 69 +++++++++--- Reqnroll/CucumberMesssages/ScenarioState.cs | 41 +++++-- Reqnroll/CucumberMesssages/StepState.cs | 84 ++++++++++++++ .../RuntimeBindingRegistryBuilderTests.cs | 2 +- 6 files changed, 325 insertions(+), 69 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/StepState.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 28d61ebac..c66e44155 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -15,13 +15,25 @@ public static TestRunStarted ToTestRunStarted(FeatureState featureState, Feature { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } + + public static TestRunFinished ToTestRunFinished(FeatureState featureState, FeatureFinishedEvent featureFinishedEvent) + { + return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); + } internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) { + var testSteps = new List(); + + foreach (var stepState in scenarioState.Steps) + { + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState); + testSteps.Add(testStep); + } var testCase = new TestCase ( scenarioState.TestCaseID, scenarioState.PickleID, - new List () + testSteps ); return testCase; } @@ -54,5 +66,86 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, ); return result; } + + internal static TestStep ToTestStep(ScenarioState scenarioState, StepState stepState) + { + //TODO: This only works if the step is properly bound. Need to determine what to do otherwise + + var args = stepState.StepArguments + .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) + .ToList(); + + var result = new TestStep( + null, + stepState.TestStepID, + stepState.PickleStepID, + new List { stepState.StepDefinitionId }, + new List { new StepMatchArgumentsList(args) } + ); + + return result; + } + + internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) + { + return new StepMatchArgument( + new Group( + null, + null, + argument.Value + ), + argument.Type); + } + internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStartedEvent stepStartedEvent) + { + return new TestStepStarted( + stepState.TestCaseStartedID, + stepState.TestStepID, + Converters.ToTimestamp(stepStartedEvent.Timestamp)); + } + + internal static TestStepFinished ToTestStepFinished(StepState stepState, StepFinishedEvent stepFinishedEvent) + { + return new TestStepFinished( + stepState.TestCaseStartedID, + stepState.TestStepID, + ToTestStepResult(stepState, stepFinishedEvent), + Converters.ToTimestamp(stepFinishedEvent.Timestamp)); + } + + private static TestStepResult ToTestStepResult(StepState stepState, StepFinishedEvent stepFinishedEvent) + { + return new TestStepResult( + Converters.ToDuration(stepState.Duration), + "", + ToTestStepResultStatus(stepState.Status), + null); + + } + + private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStatus status) + { + return status switch + { + ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, + ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, + ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, + ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, + ScenarioExecutionStatus.StepDefinitionPending => TestStepResultStatus.PENDING, + _ => TestStepResultStatus.UNKNOWN + }; + } + + + // utility methods + public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + { + + var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + + return $"{stepDefinition.SourceExpression}({signature})"; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5b0c0bd8b..5f415cabf 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -48,13 +48,47 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); + testThreadEventPublisher.AddHandler(StepStartedEventHandler); + testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + { + var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + var featureState = new FeatureState + { + Name = featureName, + Enabled = enabled + }; + + if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + { + // This feature has already been started by another thread (executing a different scenario) + var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well + + // None of the rest of this method should be executed + return; + } + + var traceListener = objectContainer.Resolve(); + traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + + if (!enabled) + return; + + foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) + { + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - + lock (featureState) { // Remove the worker thread marker for this thread @@ -66,20 +100,17 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet return; } + featureState.Finished = true; } if (!featureState.Enabled) return; - var ts = objectContainer.Resolve().GetNowDateAndTime(); - - featureState.Messages.Enqueue(new ReqnrollCucumberMessage + foreach (Envelope e in featureState.ProcessEvent(featureFinishedEvent)) { - CucumberMessageSource = featureName, - //TODO: add feature status - Envelope = Envelope.Create(new TestRunFinished(null, true, Converters.ToTimestamp(ts), null)) - }); + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); + } foreach (var message in featureState.Messages) { @@ -89,61 +120,41 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve broker.Complete(featureName); } - private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { - var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - - var featureState = new FeatureState - { - Name = featureName, - Enabled = enabled - }; + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; - if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + foreach (Envelope e in featureState.ProcessEvent(scenarioStartedEvent)) { - // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; - featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well - - // None of the rest of this method should be executed - return; + featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } - - var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); - - if (!enabled) - return; - - foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) + } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + { + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; + var featureState = featureStatesByFeatureName[featureName]; + foreach (Envelope e in featureState.ProcessEvent(scenarioFinishedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } } - private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { - var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - + var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - - var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, featureState); - featureState.ScenarioName2StateMap.Add(scenarioName, scenarioState); - - foreach (Envelope e in scenarioState.ProcessEvent(scenarioStartedEvent)) + foreach (Envelope e in featureState.ProcessEvent(stepStartedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } } - private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + + private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { - var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; var featureState = featureStatesByFeatureName[featureName]; - var scenarioState = featureState.ScenarioName2StateMap[scenarioName]; - foreach (Envelope e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + foreach (Envelope e in featureState.ProcessEvent(stepFinishedEvent)) { featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureState.cs index 9c82f3d91..7357d780a 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureState.cs @@ -12,7 +12,7 @@ namespace Reqnroll.CucumberMesssages { - internal class FeatureState + public class FeatureState { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled @@ -22,6 +22,14 @@ internal class FeatureState public bool Started { get; set; } public bool Finished { get; set; } + public bool Success + { + get + { + return Enabled && Finished && ScenarioName2StateMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK) ; + } + } + //ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator @@ -31,7 +39,7 @@ internal class FeatureState public Dictionary StepDefinitionsByPattern = new(); public Dictionary PicklesByScenarioName = new(); - public Dictionary ScenarioName2StateMap = new(); + public Dictionary ScenarioName2StateMap = new(); public ConcurrentQueue Messages = new(); public ConcurrentStack workerThreadMarkers = new(); @@ -74,7 +82,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CanonicalizeStepDefinitionPattern(binding); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); @@ -84,26 +92,61 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); } + + + internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); + } + + internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, this); + ScenarioName2StateMap.Add(scenarioName, scenarioState); + + foreach (var e in scenarioState.ProcessEvent(scenarioStartedEvent)) + { + yield return e; + } + } + private string ExtractLastID(List pickles) { return pickles.Last().Id; } - //private string CanonicalizeStepDefinitionPattern(StepDefinition stepDefinition) - //{ - // var sr = stepDefinition.SourceReference; - // var signature = sr.JavaMethod != null ? String.Join(",", sr.JavaMethod.MethodParameterTypes) : ""; + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; + + foreach (var e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + { + yield return e; + } + } - // return $"{stepDefinition.Pattern}({signature})"; - //} - public string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { - - var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; - return $"{stepDefinition.SourceExpression}({signature})"; + foreach (var e in scenarioState.ProcessEvent(stepStartedEvent)) + { + yield return e; + } } + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; + var scenarioState = ScenarioName2StateMap[scenarioName]; + foreach (var e in scenarioState.ProcessEvent(stepFinishedEvent)) + { + yield return e; + } + } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioState.cs index 72bb781c3..bd0189f26 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioState.cs @@ -9,9 +9,10 @@ namespace Reqnroll.CucumberMesssages { - internal class ScenarioState + public class ScenarioState { - private readonly IIdGenerator _idGenerator; + internal readonly IIdGenerator IdGenerator; + internal readonly FeatureState FeatureState; public string TestCaseStartedID; public string Name { get; set; } @@ -22,22 +23,30 @@ internal class ScenarioState // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); - // this holds the step definitions bindings that were executed by each step in this scenario. this will be used to find the Cucumber stepDefinitions that were used - private Queue _stepBindingsAsUsed = new(); + public Dictionary StepsByEvent { get; set; } = new(); + public List Steps + { + get + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Select(kvp => kvp.Value).ToList(); + } + } + public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } public ScenarioState(IScenarioContext context, FeatureState featureState) { - _idGenerator = featureState.IDGenerator; + IdGenerator = featureState.IDGenerator; + FeatureState = featureState; Name = context.ScenarioInfo.Title; - TestCaseID = _idGenerator.GetNewId(); + TestCaseID = IdGenerator.GetNewId(); Pickle = featureState.PicklesByScenarioName[Name]; PickleID = featureState.PicklesByScenarioName[Name].Id; } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - TestCaseStartedID = _idGenerator.GetNewId(); + TestCaseStartedID = IdGenerator.GetNewId(); _events.Enqueue(scenarioStartedEvent); return Enumerable.Empty(); } @@ -45,13 +54,20 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); + + var stepState = new StepState(this, stepStartedEvent); + StepsByEvent.Add(stepStartedEvent, stepState); + stepState.ProcessEvent(stepStartedEvent); + return Enumerable.Empty(); } internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { _events.Enqueue(stepFinishedEvent); - _stepBindingsAsUsed.Enqueue(stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding); + var stepState = StepsByEvent.Values.Last(); + stepState.ProcessEvent(stepFinishedEvent); + StepsByEvent.Add(stepFinishedEvent, stepState); return Enumerable.Empty(); } @@ -71,8 +87,17 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); break; case ScenarioFinishedEvent scenarioFinished: + ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); break; + case StepStartedEvent stepStartedEvent: + var stepState = StepsByEvent[stepStartedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState, stepStartedEvent)); + break; + case StepFinishedEvent stepFinishedEvent: + var stepFinishedState = StepsByEvent[stepFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState, stepFinishedEvent)); + break; // add more cases for other event types default: throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); diff --git a/Reqnroll/CucumberMesssages/StepState.cs b/Reqnroll/CucumberMesssages/StepState.cs new file mode 100644 index 000000000..96a5360da --- /dev/null +++ b/Reqnroll/CucumberMesssages/StepState.cs @@ -0,0 +1,84 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Assist; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + public class StepArgument + { + public string Value; + public string Type; + } + + public class StepState + { + private ScenarioState scenarioState; + private StepStartedEvent stepStartedEvent; + + public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStartedEvent) + { + scenarioState = parentScenarioState; + this.stepStartedEvent = stepStartedEvent; + + } + + public string TestStepID { get; set; } + public string TestCaseStartedID => scenarioState.TestCaseStartedID; + public string PickleStepID { get; set; } + public bool Bound { get; set; } + public string CanonicalizedStepPattern { get; set; } + public string StepDefinitionId { get; private set; } + public IStepDefinitionBinding StepDefinition { get; set; } + + public StepArgument[] StepArguments { get; set; } + public TimeSpan Duration { get; set; } + public ScenarioExecutionStatus Status { get; set; } + + + + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + { + TestStepID = scenarioState.IdGenerator.GetNewId(); + return Enumerable.Empty(); + } + + private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) + { + return scenarioState.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + } + + private string FindPickleStepIDByStepText(string stepText) + { + return scenarioState.FeatureState.PicklesByScenarioName[scenarioState.Name].Steps.Where(st => st.Text == stepText).First().Id; + } + + internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + StepDefinition = stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding; + Bound = !(StepDefinition == null || StepDefinition == BindingMatch.NonMatching); + + if (Bound) + { + CanonicalizedStepPattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition); + StepDefinitionId = FindStepDefIDByStepPattern(CanonicalizedStepPattern); + + PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); + + Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; + Status = stepFinishedEvent.StepContext.Status; + + StepArguments = stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument + { + Value = arg.ToString(), + Type = arg.GetType().Name + }).ToArray(); + } + return Enumerable.Empty(); + } + + } +} \ No newline at end of file diff --git a/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs b/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs index 0135b7038..33492bece 100644 --- a/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/RuntimeBindingRegistryBuilderTests.cs @@ -42,7 +42,7 @@ public int TransformWithOrderAndWithoutRegex(string val) private BindingSourceProcessorStub bindingSourceProcessorStub; - /* Steps that are feature scoped */ + /* StepsByEvent that are feature scoped */ [Binding] public class ScopedStepTransformationExample From 5c8765468523bce7630d1bcda0ca75a8bc91e557 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:33:56 -0500 Subject: [PATCH 105/218] Basic structure in place. minimal and pending scenarios added. --- .../CucumberMessageFactory.cs | 2 +- .../CCK/minimal/minimal.cs | 19 +++++++++++ .../CCK/minimal/minimal.feature | 10 ++++++ .../CCK/minimal/minimal.feature.ndjson | 12 +++++++ .../CCK/pending/pending.cs | 31 +++++++++++++++++ .../CCK/pending/pending.feature | 18 ++++++++++ .../CCK/pending/pending.feature.ndjson | 30 ++++++++++++++++ .../CucumberCompatibilityTestBase.cs | 16 ++++++++- .../CucumberCompatibilityTests.cs | 34 ++++++++++++++++++- ...CucumberMessages.CompatibilityTests.csproj | 26 ++++++++++++++ .../Drivers/TestFileManager.cs | 10 +++--- Tests/Reqnroll.SystemTests/SystemTestBase.cs | 5 +-- 12 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index c66e44155..8974ba766 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -90,7 +90,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) { return new StepMatchArgument( new Group( - null, + new List(), null, argument.Value ), diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs new file mode 100644 index 000000000..2207c6ede --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.Minimal +{ + [Binding] + public class Minimal + { + [Given("I have {int} cukes in my belly")] + public void GivenIHaveCukesInMyBelly(int p0) + { + // pass + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature new file mode 100644 index 000000000..5f1e5f688 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature @@ -0,0 +1,10 @@ +Feature: minimal + + Cucumber doesn't execute this markdown, but @cucumber/react renders it + + * This is + * a bullet + * list + + Scenario: cukes + Given I have 42 cukes in my belly diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson new file mode 100644 index 000000000..d10bc4bb3 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/minimal/minimal.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":9},"name":"cukes","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"I have 42 cukes in my belly"}],"tags":[]}}],"description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"minimal","tags":[]},"uri":"samples/minimal/minimal.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cukes","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 cukes in my belly","type":"Context"}],"tags":[],"uri":"samples/minimal/minimal.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"I have {int} cukes in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/minimal/minimal.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs new file mode 100644 index 000000000..6d2fb8755 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs @@ -0,0 +1,31 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.pending +{ + [Binding] + internal class Pending + { + [Given("an unimplemented pending step")] + public void GivenAnUnimplementedPendingStep() + { + throw new PendingStepException(); + } + + [Given("an implemented non-pending step")] + public void GivenAnImplementedNonPendingStep() + { + //nop + } + + [Given("an implemented step that is skipped")] + public void GivenAnImplementedStepThatIsSkipped() + { + throw new ApplicationException("This step should not have been executed"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature new file mode 100644 index 000000000..767ece531 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature @@ -0,0 +1,18 @@ +Feature: Pending steps + During development, step definitions can signal at runtime that they are + not yet implemented (or "pending") by returning or throwing a particular + value. + + This causes subsequent steps in the scenario to be skipped, and the overall + result to be treated as a failure. + + Scenario: Unimplemented step signals pending status + Given an unimplemented pending step + + Scenario: Steps before unimplemented steps are executed + Given an implemented non-pending step + And an unimplemented pending step + + Scenario: Steps after unimplemented steps are skipped + Given an unimplemented pending step + And an implemented step that is skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson new file mode 100644 index 000000000..b8a2c03b4 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson @@ -0,0 +1,30 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Pending steps\n During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.\n\n Scenario: Unimplemented step signals pending status\n Given an unimplemented pending step\n\n Scenario: Steps before unimplemented steps are executed\n Given an implemented non-pending step\n And an unimplemented pending step\n\n Scenario: Steps after unimplemented steps are skipped\n Given an unimplemented pending step\n And an implemented step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/pending/pending.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"4","keyword":"Scenario","location":{"column":3,"line":9},"name":"Unimplemented step signals pending status","steps":[{"id":"3","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"7","keyword":"Scenario","location":{"column":3,"line":12},"name":"Steps before unimplemented steps are executed","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":13},"text":"an implemented non-pending step"},{"id":"6","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":14},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":16},"name":"Steps after unimplemented steps are skipped","steps":[{"id":"8","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":17},"text":"an unimplemented pending step"},{"id":"9","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":18},"text":"an implemented step that is skipped"}],"tags":[]}}],"description":" During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Pending steps","tags":[]},"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["4"],"id":"12","language":"en","name":"Unimplemented step signals pending status","steps":[{"astNodeIds":["3"],"id":"11","text":"an unimplemented pending step","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["7"],"id":"15","language":"en","name":"Steps before unimplemented steps are executed","steps":[{"astNodeIds":["5"],"id":"13","text":"an implemented non-pending step","type":"Context"},{"astNodeIds":["6"],"id":"14","text":"an unimplemented pending step","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"18","language":"en","name":"Steps after unimplemented steps are skipped","steps":[{"astNodeIds":["8"],"id":"16","text":"an unimplemented pending step","type":"Context"},{"astNodeIds":["9"],"id":"17","text":"an implemented step that is skipped","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"an implemented non-pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/pending/pending.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"an implemented step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/pending/pending.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"an unimplemented pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/pending/pending.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"20","pickleId":"12","testSteps":[{"id":"19","pickleStepId":"11","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"23","pickleId":"15","testSteps":[{"id":"21","pickleStepId":"13","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"22","pickleStepId":"14","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"26","pickleId":"18","testSteps":[{"id":"24","pickleStepId":"16","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"17","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"20","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"19","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"19","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"23","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"22","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"22","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"29","testCaseId":"26","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"25","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"25","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 38463ae2d..419805f39 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,7 +1,9 @@ -using Reqnroll.SystemTests; +using FluentAssertions; +using Reqnroll.SystemTests; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -14,5 +16,17 @@ protected void AddCucumberMessagePlugIn() { _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); } + + protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) + { + var bindingCLassFileContent = _testFileManager.GetTestFileContent(fileName, prefix, assemblyToLoadFrom); + AddBindingClass(bindingCLassFileContent); + } + + protected void ShouldAllScenariosPend(int? expectedNrOfTestsSpec = null) + { + int expectedNrOfTests = ConfirmAllTestsRan(expectedNrOfTestsSpec); + _vsTestExecutionDriver.LastTestExecutionResult.Pending.Should().Be(expectedNrOfTests, "all tests should pend"); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 1a362738c..7bc6963ae 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,7 +1,10 @@ + +using System.Reflection; + namespace CucumberMessages.CompatibilityTests { [TestClass] - public class CucumberCompatibilityTests : CucumberCompatibilityTestBase + public class CucumberCompatibilitySmokeTest : CucumberCompatibilityTestBase { [TestMethod] public void SmokeTest() @@ -20,5 +23,34 @@ Given I have a passing step ShouldAllScenariosPass(); } + + [TestMethod] + [DataRow("minimal")] + public void CCKPassingScenarios(string scenarioName) + { + AddCucumberMessagePlugIn(); + + AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] + [DataRow("pending")] + public void CCKPendingScenarios(string scenarioName) + { + AddCucumberMessagePlugIn(); + + AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + + ExecuteTests(); + + ShouldAllScenariosPend(); + } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index ab72b2ffb..4464aac06 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -9,6 +9,27 @@ true + + + + + + + + + + + + + + + + + + + + + @@ -17,6 +38,7 @@ + @@ -24,4 +46,8 @@ + + + + diff --git a/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs b/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs index 95cb6149b..9a0bd6f1c 100644 --- a/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs +++ b/Tests/Reqnroll.SystemTests/Drivers/TestFileManager.cs @@ -14,12 +14,14 @@ public class TestFileManager private const string TestFileFolder = "Resources"; private readonly string _prefix = $"{RootNamespace}.{TestFileFolder}"; - public string GetTestFileContent(string testFileName) + public string GetTestFileContent(string testFileName, string? prefixOverride = null, Assembly? assemblyToLoadFrom = null) { + + var prefix = prefixOverride ?? _prefix; + var assembly = assemblyToLoadFrom ?? Assembly.GetExecutingAssembly(); var testFileResourceName = testFileName.Replace('/', '.'); - var resourceName = $"{_prefix}.{testFileResourceName}"; - var projectTemplateStream = Assembly - .GetExecutingAssembly() + var resourceName = $"{prefix}.{testFileResourceName}"; + var projectTemplateStream = assembly .GetManifestResourceStream(resourceName); projectTemplateStream.Should().NotBeNull($"Resource with name '{resourceName}' should be an embedded resource"); Debug.Assert(projectTemplateStream != null); diff --git a/Tests/Reqnroll.SystemTests/SystemTestBase.cs b/Tests/Reqnroll.SystemTests/SystemTestBase.cs index 53092dc25..201a269d9 100644 --- a/Tests/Reqnroll.SystemTests/SystemTestBase.cs +++ b/Tests/Reqnroll.SystemTests/SystemTestBase.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -101,9 +102,9 @@ protected virtual void TestCleanup() _folderCleaner.CleanSolutionFolder(); } - protected void AddFeatureFileFromResource(string fileName, int? preparedTests = null) + protected void AddFeatureFileFromResource(string fileName, string? prefixOverride = null, Assembly? assembly = null, int? preparedTests = null) { - var featureFileContent = _testFileManager.GetTestFileContent(fileName); + var featureFileContent = _testFileManager.GetTestFileContent(fileName, prefixOverride, assembly); AddFeatureFile(featureFileContent, preparedTests); } From 24fdc102fefd960c6771ab7d69912ca11bd0bff4 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:38:13 -0500 Subject: [PATCH 106/218] All CCK examples loaded, except for markdown, retry, and stack-traces which I don't think apply to the .NET implementation. Need to add support for images and documents as embedded resources to be added to generated test projects before attachments and hooks can be implemented and tested. Not yet added infrastructure to compare output ndjon with expected. --- .../CCK/attachments/attachments.feature | 51 ++++++++ .../attachments/attachments.feature.ndjson | 91 +++++++++++++++ .../CCK/attachments/cucumber.jpeg | Bin 0 -> 1444 bytes .../CCK/attachments/cucumber.png | Bin 0 -> 1739 bytes .../CCK/attachments/document.pdf | Bin 0 -> 10061 bytes .../CCK/cdata/cdata.cs | 18 +++ .../CCK/cdata/cdata.feature | 5 + .../CCK/cdata/cdata.feature.ndjson | 12 ++ .../CCK/data-tables/data_tables.cs | 110 ++++++++++++++++++ .../CCK/data-tables/data_tables.feature | 13 +++ .../data-tables/data_tables.feature.ndjson | 15 +++ .../CCK/examples-tables/examples_tables.cs | 54 +++++++++ .../examples-tables/examples_tables.feature | 43 +++++++ .../examples_tables.feature.ndjson | 100 ++++++++++++++++ .../CCK/hooks/cucumber.svg | 7 ++ .../CCK/hooks/hooks.feature | 21 ++++ .../CCK/hooks/hooks.feature.ndjson | 77 ++++++++++++ .../CCK/parameter-types/parameter_types.cs | 36 ++++++ .../parameter-types/parameter_types.feature | 11 ++ .../parameter_types.feature.ndjson | 13 +++ .../CCK/rules/rules.cs | 61 ++++++++++ .../CCK/rules/rules.feature | 29 +++++ .../CCK/rules/rules.feature.ndjson | 47 ++++++++ .../CCK/skipped/skipped.cs | 37 ++++++ .../CCK/skipped/skipped.feature | 19 +++ .../CCK/skipped/skipped.feature.ndjson | 33 ++++++ .../CCK/undefined/undefined.cs | 23 ++++ .../CCK/undefined/undefined.feature | 16 +++ .../CCK/undefined/undefined.feature.ndjson | 29 +++++ .../unknown_parameter_type.cs | 23 ++++ .../unknown_parameter_type.feature | 6 + .../unknown_parameter_type.feature.ndjson | 12 ++ .../CucumberCompatibilityTests.cs | 26 ++--- ...CucumberMessages.CompatibilityTests.csproj | 64 ++++++++++ 34 files changed, 1088 insertions(+), 14 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature new file mode 100644 index 000000000..063960e9c --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature @@ -0,0 +1,51 @@ +Feature: Attachments + It is sometimes useful to take a screenshot while a scenario runs. + Or capture some logs. + + Cucumber lets you `attach` arbitrary files during execution, and you can + specify a content type for the contents. + + Formatters can then render these attachments in reports. + + Attachments must have a body and a content type + + Scenario: Strings can be attached with a media type + Beware that some formatters such as @cucumber/react use the media type + to determine how to display an attachment. + + When the string "hello" is attached as "application/octet-stream" + + Scenario: Log text + When the string "hello" is logged + + Scenario: Log ANSI coloured text + When text with ANSI escapes is logged + + Scenario: Log JSON + When the following string is attached as "application/json": + ``` + {"message": "The big question", "foo": "bar"} + ``` + + Scenario: Byte arrays are base64-encoded regardless of media type + When an array with 10 bytes is attached as "text/plain" + + Scenario: Attaching JPEG images + When a JPEG image is attached + + Scenario: Attaching PNG images + When a PNG image is attached + + Scenario Outline: Attaching images in an examples table + When a image is attached + + Examples: + | type | + | JPEG | + | PNG | + + Scenario: Attaching PDFs with a different filename + When a PDF document is attached and renamed + + Scenario: Attaching URIs + When a link to "https://cucumber.io" is attached diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson new file mode 100644 index 000000000..1e42d17af --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson @@ -0,0 +1,91 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching JPEG images\n When a JPEG image is attached\n\n Scenario: Attaching PNG images\n When a PNG image is attached\n\n Scenario Outline: Attaching images in an examples table\n When a image is attached\n\n Examples:\n | type |\n | JPEG |\n | PNG |\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/attachments/attachments.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":12},"name":"Strings can be attached with a media type","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":16},"text":"the string \"hello\" is attached as \"application/octet-stream\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":18},"name":"Log text","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":19},"text":"the string \"hello\" is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"14","keyword":"Scenario","location":{"column":3,"line":21},"name":"Log ANSI coloured text","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":22},"text":"text with ANSI escapes is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"16","keyword":"Scenario","location":{"column":3,"line":24},"name":"Log JSON","steps":[{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```","location":{"column":8,"line":26}},"id":"15","keyword":"When ","keywordType":"Action","location":{"column":6,"line":25},"text":"the following string is attached as \"application/json\":"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":30},"name":"Byte arrays are base64-encoded regardless of media type","steps":[{"id":"17","keyword":"When ","keywordType":"Action","location":{"column":5,"line":31},"text":"an array with 10 bytes is attached as \"text/plain\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"20","keyword":"Scenario","location":{"column":3,"line":33},"name":"Attaching JPEG images","steps":[{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":5,"line":34},"text":"a JPEG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"22","keyword":"Scenario","location":{"column":3,"line":36},"name":"Attaching PNG images","steps":[{"id":"21","keyword":"When ","keywordType":"Action","location":{"column":5,"line":37},"text":"a PNG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"27","keyword":"Examples","location":{"column":5,"line":42},"name":"","tableBody":[{"cells":[{"location":{"column":9,"line":44},"value":"JPEG"}],"id":"25","location":{"column":7,"line":44}},{"cells":[{"location":{"column":9,"line":45},"value":"PNG"}],"id":"26","location":{"column":7,"line":45}}],"tableHeader":{"cells":[{"location":{"column":9,"line":43},"value":"type"}],"id":"24","location":{"column":7,"line":43}},"tags":[]}],"id":"28","keyword":"Scenario Outline","location":{"column":3,"line":39},"name":"Attaching images in an examples table","steps":[{"id":"23","keyword":"When ","keywordType":"Action","location":{"column":5,"line":40},"text":"a image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"30","keyword":"Scenario","location":{"column":3,"line":47},"name":"Attaching PDFs with a different filename","steps":[{"id":"29","keyword":"When ","keywordType":"Action","location":{"column":5,"line":48},"text":"a PDF document is attached and renamed"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"32","keyword":"Scenario","location":{"column":3,"line":50},"name":"Attaching URIs","steps":[{"id":"31","keyword":"When ","keywordType":"Action","location":{"column":5,"line":51},"text":"a link to \"https://cucumber.io\" is attached"}],"tags":[]}}],"description":" It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Attachments","tags":[]},"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"34","language":"en","name":"Strings can be attached with a media type","steps":[{"astNodeIds":["9"],"id":"33","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"36","language":"en","name":"Log text","steps":[{"astNodeIds":["11"],"id":"35","text":"the string \"hello\" is logged","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["14"],"id":"38","language":"en","name":"Log ANSI coloured text","steps":[{"astNodeIds":["13"],"id":"37","text":"text with ANSI escapes is logged","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["16"],"id":"40","language":"en","name":"Log JSON","steps":[{"argument":{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}"}},"astNodeIds":["15"],"id":"39","text":"the following string is attached as \"application/json\":","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["18"],"id":"42","language":"en","name":"Byte arrays are base64-encoded regardless of media type","steps":[{"astNodeIds":["17"],"id":"41","text":"an array with 10 bytes is attached as \"text/plain\"","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["20"],"id":"44","language":"en","name":"Attaching JPEG images","steps":[{"astNodeIds":["19"],"id":"43","text":"a JPEG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["22"],"id":"46","language":"en","name":"Attaching PNG images","steps":[{"astNodeIds":["21"],"id":"45","text":"a PNG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["28","25"],"id":"48","language":"en","name":"Attaching images in an examples table","steps":[{"astNodeIds":["23","25"],"id":"47","text":"a JPEG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["28","26"],"id":"50","language":"en","name":"Attaching images in an examples table","steps":[{"astNodeIds":["23","26"],"id":"49","text":"a PNG image is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["30"],"id":"52","language":"en","name":"Attaching PDFs with a different filename","steps":[{"astNodeIds":["29"],"id":"51","text":"a PDF document is attached and renamed","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"astNodeIds":["32"],"id":"54","language":"en","name":"Attaching URIs","steps":[{"astNodeIds":["31"],"id":"53","text":"a link to \"https://cucumber.io\" is attached","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the string {string} is attached as {string}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"the string {string} is logged","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"text with ANSI escapes is logged","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"the following string is attached as {string}:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":18},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"an array with {int} bytes is attached as {string}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"5","pattern":{"source":"a JPEG image is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":31},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"6","pattern":{"source":"a PNG image is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":35},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"7","pattern":{"source":"a PDF document is attached and renamed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":39},"uri":"samples/attachments/attachments.feature.ts"}}} +{"stepDefinition":{"id":"8","pattern":{"source":"a link to {string} is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":43},"uri":"samples/attachments/attachments.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"56","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"},{"group":{"children":[{"children":[{"children":[]}],"start":35,"value":"application/octet-stream"},{"children":[{"children":[]}]}],"start":34,"value":"\"application/octet-stream\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"58","pickleId":"36","testSteps":[{"id":"57","pickleStepId":"35","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"60","pickleId":"38","testSteps":[{"id":"59","pickleStepId":"37","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"62","pickleId":"40","testSteps":[{"id":"61","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":37,"value":"application/json"},{"children":[{"children":[]}]}],"start":36,"value":"\"application/json\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"64","pickleId":"42","testSteps":[{"id":"63","pickleStepId":"41","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"10"},"parameterTypeName":"int"},{"group":{"children":[{"children":[{"children":[]}],"start":39,"value":"text/plain"},{"children":[{"children":[]}]}],"start":38,"value":"\"text/plain\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"66","pickleId":"44","testSteps":[{"id":"65","pickleStepId":"43","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"68","pickleId":"46","testSteps":[{"id":"67","pickleStepId":"45","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"70","pickleId":"48","testSteps":[{"id":"69","pickleStepId":"47","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"72","pickleId":"50","testSteps":[{"id":"71","pickleStepId":"49","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"74","pickleId":"52","testSteps":[{"id":"73","pickleStepId":"51","stepDefinitionIds":["7"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"76","pickleId":"54","testSteps":[{"id":"75","pickleStepId":"53","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":11,"value":"https://cucumber.io"},{"children":[{"children":[]}]}],"start":10,"value":"\"https://cucumber.io\""},"parameterTypeName":"string"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"77","testCaseId":"56","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"77","testStepId":"55","timestamp":{"nanos":2000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","testCaseStartedId":"77","testStepId":"55"}} +{"testStepFinished":{"testCaseStartedId":"77","testStepId":"55","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"77","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"78","testCaseId":"58","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"78","testStepId":"57","timestamp":{"nanos":6000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"78","testStepId":"57"}} +{"testStepFinished":{"testCaseStartedId":"78","testStepId":"57","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"78","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"79","testCaseId":"60","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"79","testStepId":"59","timestamp":{"nanos":10000000,"seconds":0}}} +{"attachment":{"body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"79","testStepId":"59"}} +{"testStepFinished":{"testCaseStartedId":"79","testStepId":"59","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"79","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"80","testCaseId":"62","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"80","testStepId":"61","timestamp":{"nanos":14000000,"seconds":0}}} +{"attachment":{"body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","testCaseStartedId":"80","testStepId":"61"}} +{"testStepFinished":{"testCaseStartedId":"80","testStepId":"61","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"80","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"81","testCaseId":"64","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"81","testStepId":"63","timestamp":{"nanos":18000000,"seconds":0}}} +{"attachment":{"body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","testCaseStartedId":"81","testStepId":"63"}} +{"testStepFinished":{"testCaseStartedId":"81","testStepId":"63","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"81","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"82","testCaseId":"66","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"82","testStepId":"65","timestamp":{"nanos":22000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"82","testStepId":"65"}} +{"testStepFinished":{"testCaseStartedId":"82","testStepId":"65","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"82","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"83","testCaseId":"68","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"83","testStepId":"67","timestamp":{"nanos":26000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"83","testStepId":"67"}} +{"testStepFinished":{"testCaseStartedId":"83","testStepId":"67","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"83","timestamp":{"nanos":28000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"84","testCaseId":"70","timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"84","testStepId":"69","timestamp":{"nanos":30000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"84","testStepId":"69"}} +{"testStepFinished":{"testCaseStartedId":"84","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"84","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"85","testCaseId":"72","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"71","timestamp":{"nanos":34000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"85","testStepId":"71"}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"nanos":36000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"86","testCaseId":"74","timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"73","timestamp":{"nanos":38000000,"seconds":0}}} +{"attachment":{"body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","fileName":"renamed.pdf","mediaType":"application/pdf","testCaseStartedId":"86","testStepId":"73"}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"87","testCaseId":"76","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"75","timestamp":{"nanos":42000000,"seconds":0}}} +{"attachment":{"body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","testCaseStartedId":"87","testStepId":"75"}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"nanos":44000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":45000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e833d6c77d33e620ae49d01c6341ad7e5543bbd1 GIT binary patch literal 1444 zcmex=ma3|jiIItm zOAI4iKMQ#V{6EAX$ibk;pvlar#K0uT$SlbC{|JK&&=V}oKwrQC2Ma43I|Cy#)Bjrx z93W3JFj%5R(9qV{Ntcq}Ky~tP0uLAPSj53Js=3YaT3(saOkCY9;`5A|n$EGYdN> z$V3JPCPrqUBN+tQg%k~il>!AEoq|My6AOzQCy9zF8yTB~CKo{+&uGtZX0w)!P@n75 z+pl=GZ!o)2TKe4kXH-t@v#4EXEL!JE?zOu6r)a^it+7!xlS1t8{b#8CcjV%O8K0Lw zw!5=@{{4E%lYXmb|9XFR)y|pcq!QHyPd&4_T>Dx#CekoQenQBu?LV{I?)a=Z|5`qv zFCb50m1$zkt9Lo_J0{jC=okGqK3#m~$Ha_v^0sbzj}ElmKM-8K@73}%+E1_Sj1$h= za&Sv<=wdZVW0znRzq!_L_bo^lOYMz6H&?{%)XoFh(vNfajh)BRw*tXt#N5h?h}`dZpTv+M2|iZeX+YndxfsS7m0HMKkjMk z3R-;5(6ii>*OK{q>6sTyS60rQly}eE#LW&5BG%-(C1!D;fUT^{(mfa_yh< z{#u>M^5=OJ`#1Z*!uSb$%D$efQTh0vq2T`C;?gU-q$)Q@ZJHi;`qC^B2V>vgrM{Op zz2Y^CHRFrdI~bc)vyq`s=08J-S2f$|3$=&W)J@FMUY+(-GI%Z`A@L?@)(B$FN=Q(t=#l1^;xf6 ztKqtSx0-4T?$s+Kr!hS|dgkT)$(;KdOXF_sl{kL&qTE`s1rg3$Iy;OF^4Vh9&`X%c- z?@Ir1`muNQ?HGfD#&eeSnRs0PY8_Wf0x zYr%isu|McfyL=m%q*VpMX&9LEAPE|n37MD~SOi&xgcSot42=?%9Sb43h7p*8_w1>- z6Mg&7uRHgQ{q+{+?($T>Bl_*Z+yh@{H3b$5c{NME=w}I8|8lFOYSF`(6w%|A@+BXl zmusIkS2)Mu_D0Bf{>CWJM_W~opFJsmFzB$zE1{4Rf8~=jG8xtK!v7@pU6p$h?!dst zAi;cKJ=ASXzzoOCisUvHRt7dfAw$K$LSa!QBS)~?m_cs48@=pNOiq~YFVP2{3ZGA2 z+`D6=>s#ATRX)mE@f(tl=gZwXkQQv**LRUIg?&x;o5PWIH+oN(d8?&{>zqn{JdOYJ zpDR~h9MUx8cipWf_R8znWtJ&uK2N+Xmx{{Kw?qP#Uw literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png new file mode 100644 index 0000000000000000000000000000000000000000..2760899aa0ce16aeaaea93d3118a32e7ac5bec8d GIT binary patch literal 1739 zcmV;+1~mDJP)G+r@kQ0VOJz1t=x5=pe3owlM@Ei<#*ZtpGI zgKd}Iv=WU6L@O5L5Qs#)gFvk*7OQ`VCsjOx5<>9^RV-lQA0`qL#Hi6fjMC@#y`7oe zS+={g-EDP}m+f}vz4x2%_`Ec`caU@Gwv+u9&;ebh;EPxToe6TQSS7$RIEHqf|0i z0O+ z#n5`Lu?_+l5a?76FBT_ov)oVPu)W&2iyqQk6q$E2`kQlqo37e8-)c+O?QiP<=H+=W zgC{tLKe`UI8wSZ$`}IIZS7^VP!&SQ9uqqj{=4V~1HP0i=kNxUV+pBnmxV8-q#Bxu7a+-a2h-P!vK`Neu>T>`ydm{x2F*T=U zFja;RD&fUT?=ygOHKmEIjm9$XXg4i$m^A1Td}8TX_oouO^1 zftu}jxiw*Sqct@^-zMG^t)7q>c1_O=)^$NOakew|C1vwbb(rq-=>$Sb3W6-RZHa~m zCJ-n2XtkjN1C$hf07a9cuvH0K135T-J;#nfDUID~Sv*Yz!r1hE3|a|Q4_9AFEWLHK zWz~oq!$8|OVjZ(mf6Y*Z+@G&h%4HXA<`4i@t!XO}{GYwU7nxru-K+|Xtu%RHv$V5t zB%F}kxeBctB<7atq+W_2XaHsIuS|aaB@`qli21Zmg1ge~X)cg!Z$@+@T2Kf6wH3yz z&3Yvsd2vMF!t&A=G!b@&(o#XX>H~4DCJ(w*r-TNAhSk2Rta1dYR_3O9WK; zw#R~odmNn4Rrysbu1(x67Wr<38~#FiG1`n}wO&7`@#)Xk3f`-e^xfhbRWBNCp0aZ6 zG7j-w;#H4aR;4%|7`O;-+)Hl;uEM%I>mVnijiapH>2)nQ~8L|KJMiw<@G*t1yFW9;lFYc#uS`DwjogG)sqA zR1ZXz{)UVWQ#sGl0$Aj@M%qBOs8U>G?P+&{nu~!`wccEicA`NtKbHn#g?=au3r^~) z3?!GiqlV-fJj2A0p}mr}21v220F29iQ1fLV>-W%pUD-_S!;=aJIQ+*`W9=9^HPvL{ z0;-_op8_N*ciN&xk6wP@c=5J5d#{rWB-U?ag_zePQ!sYV&o8}(nae94B=$6zwdRg$ z{h^gdI-K$AkW4|MJIzg%@h!p>_RSE#rObbMsUUTO7bj4~!21~&^MANd_d&CC2wth| hfX@)Vpvv|h{{nxzjlgvW5(5AL002ovPDHLkV1gBrLk9o= literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4647f3c9d70173713ad061f405a1aa9146615677 GIT binary patch literal 10061 zcmaKy2Q*x3*T;z{AzBC$Z6bObgBb=PdM9dh#uzQSQKAdcd#}-3^e%cQdJTeTiRdMW z&NuG8?|a|!ecxGUtvP2u``ORg`}yy)X3hKH_{zc7#0idY05Ge)v48KJn093Mdu!dRv?F9bYNy@?siCP3G zWd%bbBoJ_069g_$0byf?GzWkne8R#2N2CJ+W{vBbrXM+u+sXA%a_Q*txv!JrB!nY~ z@ZFyFCT$@ljlEBSrri`7BDeeSAZ@T$_R!+ z0Q|h%s4(IvZ`58p0`9g$6&I+6bZ~+rU%?#yFmjlr3^0~YHhkX8-kKY9K_&VB zD2AVl3;M5;*>!v1sxJ9^zo@Z3v1IYVoWN$TZ9yzSE{5-Y9C-x+%FO5&MxFFAXwg0Z z`In+9DNkMnGe_VNYy@3qMoeh86L4~I3q0rK+G@bV30oZkY%rI7Z#C3#@d>(EAG{oO z&)83Gg|-PeZA}$@R4W*1;altuDkOZF=bJOgCN(a7%t?GC{?+B!|1j3m&&!>fSmfQs z)_80|T!08%C-8UuD_VXp2b*tIp!vuwPw>`>+F+liasy(I-2AIFspI_6IX>PKv%#@F zN%!-i!9?@ZuS-|h(e~tpTQTpef40>$o06!gQ*9*<+n5{3$dY1Rz8|X}`A~lq)ASLq zDQE{Ou`aGyy$*EG|Ni}v0>xT#JuetCBtUU0v}4b690(C5N~K+w@E(fhPFV+PleK-= zW#XO^*A%hs7z&V37FsXh&1$js>Z*Dl@SBq#C z)YXP|=Q^Rs*pm7bIAWmWc=(HLS&f`*q;hSXM1Un#6WPvdpU`m%y@$^Y?vuK$@S-JV z!R)&pPMC5U3Psk;g;D|;N{j4!jVIQ@?4QDFc;K-ebr#Xj^F15$4)7d9v_tj<)8aV> z$eF?xZic_8RhOEOdLcfSnM-eHQ5IV1BM%qXx=~$aB>EEr1SRQn(Um}uYJaAl7Cq4q-b)UPxod0ajNgQ zdi{!Q<(nkMG4ech`H*8Vr7=8*YkN`JZ0zmd1>fhih4LLN){HkkglB!ic@YuMEgMam z1}~|3<&yw%V^zc??zuH=fn4o#I{%C`98lY$+cY9kBx(A%sxYDV>%A+MtpUv{R8mbX z=ye6Tr+BklLWFNj*5mKt%RzGHpy4d66lr*+yBaZhXwj1xOnN#8oNQ+z<3jXOu_~X0 zF$WzSj_{F!^Qh5)J;clK3%?PqN%KcFrjx1%_)))0(#PE?Qr=R)h&b_i%wy_CsY@T$ z_AFyUUectkph4H0$6Ot`#7MrTi0AMPPU{h*Nn(Wkq*}T47T$ur9l0H~;#EVGUl2b& zT8V?rib7sht+<$|EjGAE-4JgSGB>SnNJ};`pD&HG5Yc6G98;uXn*e^XfyOVsfLNm;!~-M?{47qg$2nB`8aFD}UJ%dkm1DW{Xu z9ANN1PnWaRP{c__~sKsj#MZ@*}QZZmmTb>l*6UJ|FU3v~pMc zZ+yJoS;hIO)jhg#eZQ;Ps8qVe8qoLfKvm>vL;~jV!|UMRAo5ELL!%(x?@yWo*iL4m ziLu8?hkm$bPexpalqJQgzD`h zvlaHzyruRFuXWh`Tl%Hqg>vDKUT+k31q!9%(hEqYy;5%@6|JJ)lsVuWbLzgI`fEUq*4C3wEy!1Quf#h|nJEbppeB9~qD zgQmQ_qGsx};gd4(f|1Zyjm49`vNoh=(a+7935o75)}b5cMoPBr+?HcV83r=wiPrtl z-aHUfYG+xe6Mj?lVqaZFpfv)HZx~c$UkeQo<-ICWAv4mPVc}c*n(vXxSnRGr?KQ$<#bpJt0ts2VezEJMu()8b zsRVku+!pPqT{abNUzFkNTkBI_VE5lZy5rKRTpuL|HBb92v^<2~igusBVSm&rZ#q{J zq>nSIVCFbSSNHIP-Y}#R`^N&UnR17`ZPi+mBWe90lyF(xm4!BP+xS~Q`C;lgN3&mB zN1KX*oWHTkYP=PqMzw&KRX*;=09H?Vb8d4vm8uf!(#piN-E>xdvCVW&gl0jp{B+ZF z1(WOucIn(-96N4a;O*rn0f^pc4|+;zCUriKYoVnf)UCKIH$6^GodUAl#6}Rqp#5 zktF5-?BcQhz*07xzi(!e+A2XH)h64c?w;~8;fne(kDloeq_>*qiDU7;%Iy;-X}^9Owm6~bU$1>8}6h?OdbcC=Ln~APTFSO^kdI#vKiUqU59^}B&*BcW?hh; zv~&1fjfbCeX3)jlB4WE@d-T~ueDwN#J&*RIYwDS`LedpznWSedqgrLF(k)wd6OemH zDn*ohjJV_%Nf?*ZoaDLwnrpGJoOaL&O=j@LJ`x+RfYFmCyr5u1G}ia20?kfkMoY+v zfn>NxOsj;&P6~!dNg#Ri!57u<8MWW4STq>Et!5wLN=Hx#ml*XgUp0$jIAR}<9-5qP zr~Vijvda?$OY3}m!7!H)mqbK6!vyKI42}p1!y)8J4a3q{33Fi8%h>|AHGL~@)Q9&T zlRLe}xQ}o4VC(v+M2_3mS*=@9{>jv_*ip>Bg_~c>x5<>DkB?Phw^XH_dFO_lE*9tz zYh&;k<)A(|VipWKb`or8La@Re)F zLa@+1tNZ-k*>7NM^6bVjE5VPS>m1&vdM|q(tN-J%S8B1|PbAl@F`P zFQ}xdtCGu}csayhaL^QqAsi^$Du--iy38HBEjO@A!Dg=q8-3zUr`+kxKAD22G%w== zo`R&pu-6hUNy>#{komZ4m5Aw?^u?6y8g=^Naf#Zz9|FVPqupE8y6N}cSQ=q?zFjZ8 zNKm10qfXO;YIm7?C@R)wly*d=Xa#F&*m;JR;XW(MC_eAf%PY%I_;fOx@|{dXQD@Ie zUg@{<7B}oTbeixucUzstrOp>j|d-Ier)1IPshzbkrVM*b3ZU&QHKkFLU(k*%J zuXlV($z94>`U6kwenZ4{bwTbkdXTA38=xbEA4+X$zQ2A=6DcC7>mO9~&^-i7_W2^} z2AYW7FjzbGG0W%D{GP}BD;_%XJr{u=Rz_|UX-%4PDguzNxY?=OjX}wE4vrkXuZ0O0 z2?0KPsVfsDWeG|*?A~<?xUP<$>>9_ z7;eY*#z3Uy4q%a#i{V*%j;I29__peL2mYPL1fK{J_i0|`mSTb{AB$K7QIyQiFd!Vvgmy=ZWc(LOWmlpjq;Kdot5h9s+-wm+7BH!g76Ai zMVXECU15%PrJ+*-_oD$*_LPe+_rPcdJda*!3#>rT3v z37!{VhlY(@7D&T7;~ieH?fwK z+_xe`4YvK1!TGEm-*Vx?`mL%BxUo4eL>!~%CF>tbYG3T!%Ma&9UuBEh=S9r87+`qH zr!>lYU~ostEW&2dTdoaKqot)1lt8k)DRgx0@+Rol+XNj%6t-c@VFCTjyz&x9_0;b~ zkLtfsbUbl=YEUlQY8r^?>e!Ue4Q`($kqST5d(X0556XvOp$j@3gRNCZ z^visv+6oGZ8FGoDz?{SkHZmG-_JqguxlKG5aeDt{RJ0&s%F~>qhD_Doex;~W6eIcP zF*X6kYM4v!fv|p&rTpuVXIqtht>@$)?y;TZSf=RsTcr%_{bm&Crx`6^b_!^$U?C14 z$P2)Pg%huz`FKeXP-FB8%Gwn<`)XI(Qnm&QCYKO|P{SxqfWg$Mdd3+Px^GfVY=hD> z7K{u{^~k(qG8C#*?^R1MFe=FuDr+TW($8}6>F!rL6l*9}2Bw;0d;2G~7K`)WyIJ@V zUZ)eTRuWb@e>poHB+<-=46>Fb-6N(%TQA;Kv2)7iW6f-1dV#r40+r|TPUH-Il;2rZ zg?*W$rCxWFKC>H&_qz;Vr)ZQfpd`^Hg06z1KT)aKyoO;{)LCpSs zl_bsRXGt;0ZwjA~$n=lm;)j*9E1#U+7mz)!N}65if(OD$wsAOU>dZm%yifHvN#S`{ zLLtFCP?AT#jsmy~1P6EJ_*lvJ`_FTa*2>n{_gdPj%zrPAAJl<)X42gPmBXkYPP-w2 zb5q@yf=PZX>@g?1Uzbj5HRc<0f)~B4wI{__*-5vh<25BZU*=8EGpV>elg(B%Uh=v5 zwBT1_)4Moq;>`18F|0UMcHcB@F$}A>N!8BMP46P80>Vb zt!6N;yn>P(l~(eRRQ9v?$}0;geVwtE!?m#5m0IH3=vo!WVaKlZfYIwg3h z+0TYpuUSu8>sq^8ON9=@bd3a!)TrxDAU5t#3n;aPC@#P8XW1YOdP}Wn*Sbr>LaWDU z61lWKJa~f~GU~;e${&&cQS{q2mNoyGfbV&QI2A>Q>!lX^@(ucf$ ze=>}V$LBUh>Y$~jMgbF+SrLtTAT(cXIn65YBDGPn#O5%&k>_64RKd&jchCH)7ql_X zqB_L!0tkjj$SN#NR(*K2<J;%_z4`d!Xsczs+$ z;car1_Sli+k%OA%elvV4?PvM7ONij8*XC;PuOmI#QtCa=`$r`(8uB9Ya6%SKO};zG8RIbG@pu%*4zb&h*Qcl-1a=PU+H#$(tRU z2{=NiJlf=!!Dji6w*IcyFW>KrAH*+ARau72NAsQYJEeZI@|am?dExaBv(A3(Uw@7a z#!d$q)z&F#&I3O_NPsh84gNavvwZ%}(hm!}2Sd&ep{Z06Hav6`%q_%f`}zTmSVjMR z1&?P@haxfWLCxP6a=K$_yC1`4RyB<0c^2NLy0Ef>-m`_7MvE`yRn!$GHOJVM9j%ed z6WlxyA+uWP`#{K=~bG@nIwOckE{aKAKUzr#=Sv zT0}8K4aV+ACGWrK;zDn&AJ*SumR&AC5Lqdm>o8pRPWT~z^;WyJKO^=B9`od@y_M1W zJZK^Py?2Ftt73EXpW*inTMI1nJ0kfOP?p_O`*oH@3LRv zz3yDLIgyBSqD3>teZ9aL{Q$J7`wh}**aQ{Y^E>MXrvc%GxOPiOJbv`O>FxFhc7~sD z)6hceTgFT}6et21Mutzc)k&>i>C)vbKro9oMq1 zJDce`Lxk!+Z*L~<9_$*m79`m|I$G@W2p@96KPokD6TV{1!kFxUGKpYdT+MN26^CNn=I&fi$Au5^TRgemgT;*o%K3^^0v~m&F&qDJ8;Uu6 z3^J5B1kA}dP|ALil^zjXo@kD~{)aHwP_mGMrmq2%@Wj*>SeX(iQ;uC#)|%{tX~p?J zO{QAb>c02_8gLV=&aFJOaC-^_oO{>LQaCF3NLt@=ZxsV`m`muxmbz!uSRt$HuRgWz zZmUcCT;~s8=jPCVSQNEj&5pol;v1BJhNU1paps1zsT7=VmqVr4WpypOOcI}pr66~Ial^YD)Aylan13uR}XTpw4a-qDv?LEMXG*g zV@j-&yDdvrhl(F%r0w8V90X5ZVZCNK>eEz3XcT!T6>u9(FrDKG#d}Tq8>-iHJFDU6 zdU<*JsAxF(Y>L$|N_t4o%ZBxod;&?vdCn|osfo)zNF--^+(*{)kXGXc;}vZuPh|Lq zTpEv$-r_<i!k|0^bOAY!rdxZ*+SS8`5`poU^l=V?9|zQnr-xf;=@rvinTJ6gSRJ}bsFk(E7pN3~}pCU0Ph zhxOC5_}T)EF9Xq}&Qg6eQ^5kWr6f*70w26jC$&O;uKbP$^StRHUp^@dk$Tl~8!(s} zbJhAJ<%>Wyvk3e67_-k4>v?hNpaP+@nJ_{L`2^fR?D5Gf zIBku_SquJ;Vfi4|>$zCA>mHHY)f0l%me-W)cx<2QiVcH9L`thdoUS@*Nny9-Mp;hUTSh6*kv9^ zaenPVi*dA;BOizM(^auntw*eWFUIpe%`+5;bZUQ?q|v?Sx0(~x+1;1P>^f`~XL4rN z&3Q3U5^9*ctxqZiyI{dBusfe0#4(~bC{j`(l|;{B&|!XR&Va{-bw*7i4)^h7CZmw{ z^f_#|W$|702__{}xaTDfk-MLv06AcZdsri{kVk6tbnc}5aD(ktj(R2uVV$9xfW=Gl z1GWJG6Ibknug!8|oFu1_VKcUu>~%l>P{KwQ`!)TKSB6I3!5GYxN%AzyFSiJ(?q9w8 zWsb&`;o$3Mk$D5fkLHiVLdRpKv_eMKCuFQQ$!C~X8o^A)ij>EmrCbKbYU@WT79!twWjQChNycDy1g{sgJbK8v*O zL*A$SzP)Hn6XzF9yt1n%jc8hu@fV;JF}SZ9gMq}F∨WEz|Va{bk-|&;_RAeM9+J zUnh0UX026ZO7^YamIZ5tFXz1xw3~EOReU=#MGVLU&?oU#=?!Q&VOGMa9g3{|}-(rTcEVg5D)DI{a z2GWDRGn4V{waEAz^x84pWqc%zrU@5vy=C+T^*opZy{sEeGZf(2bRQ7&>;3n3xehtByZm`RwV*79njVs~e!)Ed` z;YH~-tZ&NoXtbHSzJwFj7w*SIi)C%{Z8`Yu97yZ~Sa9T+txx2H)bjqO(lY!JZ9$Wd z?q>Zu#8Mn;b2b@_2~w}=Ym!RS<3n-cI9 zwjH50Myt%cFL0csJ&^^?GTKnrwi)*kO{uyhF(kR-koS?7m80R(c zIKR7oLwk9?hVqQyn)s}&`*Z`ke0eNnCvq*jB#li^lS_Wwa*V!tmKv21n> zIWRmXIB*O$9CN=2MaFzX^}(gDA^Pdq3=-`OpZdO+JS|#VtZN9tHt-cWHTu>u-?F#k zg|~2l_XW@B=IvR%<3*oH%%q4gQvvODblS~06nfp|{k(t{nm$Q>+P@j-Oauu{)-!d1p-7KZuoz;VzBBwu( zGi;S^D}Nr}uHT)sBb(kg=cqeOjYzwL|Arc%fB!xIC)99<9!MY@;SLsdNLzul+F$c0JmcRloYIBFCpvZ;0 zrxE)DdEkIRcsapfEub zk(E%lm6nxIgxR484--`O7Ra}MnAP4oqEeT&F}1xzI#dy67AUUdEr3}R#n~WGfR3^Q zioLP0F#|CFV;ah+=45AQg}6g_09<$M$Ll-jLzfTCr4Imu0lE+n7biats*w5sK5l?6 z7!2VAfw_55xBPr4WP^_nHHs>zKB^!RuK{4LyBPnd!@pod{~vUCCq~vXRzqlXO#k@lV>sE{OE+1y!pwuC+l-%Lw!84+cXP;}JYhW}G195Vmt850;vz9?0pz?(k=tBTq|bz2P^i#wnS0J__|f6=t71Hu#+ z)d1YMs6P1X06@TC9x%WZ@J|~E#Rc6R0Gof>c=%CR&wtoJT&RxucN-MKg~ECM-Npst z{qJ~SD5|^u!w>Sm_(7omwJtv&O0xek7s|zr%JkpkLAm+=WnH8L4AtZgckG**g&V3e ns1{VUwMD&4{_NjB+U+0Jazw%$kbl$)%Et@FWnhq0mcsoXXWVEI literal 0 HcmV?d00001 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs new file mode 100644 index 000000000..036edadcf --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs @@ -0,0 +1,18 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.cdata +{ + [Binding] + internal class Cdata + { + [Given("I have {int} in my belly")] + public void GivenIHaveCukesInMyBelly(int p0) + { + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature new file mode 100644 index 000000000..9b6ebb155 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature @@ -0,0 +1,5 @@ +Feature: cdata + Cucumber xml formatters should be able to handle xml cdata elements + + Scenario: cdata + Given I have 42 in my belly diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson new file mode 100644 index 000000000..51aa3abcb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: cdata\n Cucumber xml formatters should be able to handle xml cdata elements\n\n Scenario: cdata\n Given I have 42 in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/cdata/cdata.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":4},"name":"cdata","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":5},"text":"I have 42 in my belly"}],"tags":[]}}],"description":" Cucumber xml formatters should be able to handle xml cdata elements","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"cdata","tags":[]},"uri":"samples/cdata/cdata.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cdata","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 in my belly","type":"Context"}],"tags":[],"uri":"samples/cdata/cdata.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"I have {int} in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/cdata/cdata.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs new file mode 100644 index 000000000..0e696ae16 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs @@ -0,0 +1,110 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.data_tables +{ + [Binding] + internal class data_tables + { + private Table? _transposedTable; + + [When("the following table is transposed:")] + public void WhenTheFollowingTableIsTransposed(Table table) + { + _transposedTable = Transpose(table); + } + + [Then("it should be:")] + public void ThenItShouldBe(Table expected) + { + TablesEqual(expected, _transposedTable!); + } + + private void TablesEqual(Table expected, Table transposedTable) + { + var ExpectednumRows = expected.Rows.Count; + var ExpectednumCols = expected.Rows[0].Count; + + if (ExpectednumRows != transposedTable.Rows.Count || ExpectednumCols != transposedTable.Rows[0].Count) + { + throw new Exception("Tables are not equal"); + } + + for (int i = 0; i < ExpectednumRows; i++) + { + for (int j = 0; j < ExpectednumCols; j++) + { + if (expected.Rows[i][j].ToString() != transposedTable.Rows[i][j].ToString()) + { + throw new Exception("Tables are not equal"); + } + } + } + } + + private Table Transpose(Table table) + { + + string[][] matrix = GetStringArray(table.Rows); + var t = TransposeMatrix(matrix); + return CreateTable(t); + + static string[][] GetStringArray(DataTableRows rows) + { + int numRows = rows.Count; + int numCols = rows.FirstOrDefault()?.Count ?? 0; + + string[][] result = new string[numRows][]; + for (int i = 0; i < numRows; i++) + { + result[i] = new string[numCols]; + for (int j = 0; j < numCols; j++) + { + result[i][j] = rows[i][j].ToString(); + } + } + + return result; + } + static string[][] TransposeMatrix(string[][] matrix) + { + int numRows = matrix.Length; + int numCols = matrix[0].Length; + + string[][] transposedMatrix = new string[numCols][]; + for (int i = 0; i < numCols; i++) + { + transposedMatrix[i] = new string[numRows]; + } + + for (int i = 0; i < numRows; i++) + { + for (int j = 0; j < numCols; j++) + { + transposedMatrix[j][i] = matrix[i][j]; + } + } + + return transposedMatrix; + } + static Table CreateTable(string[][] matrix) + { + var columnCount = matrix[0].Length; + var headers = Enumerable.Range(0, columnCount).Select(i => $"").ToArray(); + var table = new Table(); + + foreach (var row in matrix) + { + table.AddRow(row); + } + + return table; + } + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature new file mode 100644 index 000000000..2822419e5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature @@ -0,0 +1,13 @@ +Feature: Data Tables + Data Tables can be placed underneath a step and will be passed as the last + argument to the step definition. + + They can be used to represent richer data structures, and can be transformed to other data-types. + + Scenario: transposed table + When the following table is transposed: + | a | b | + | 1 | 2 | + Then it should be: + | a | 1 | + | b | 2 | diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson new file mode 100644 index 000000000..a8a9c2bd7 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson @@ -0,0 +1,15 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Data Tables\n Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.\n\n Scenario: transposed table\n When the following table is transposed:\n | a | b |\n | 1 | 2 |\n Then it should be:\n | a | 1 |\n | b | 2 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/data-tables/data-tables.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":7},"name":"transposed table","steps":[{"dataTable":{"location":{"column":7,"line":9},"rows":[{"cells":[{"location":{"column":9,"line":9},"value":"a"},{"location":{"column":13,"line":9},"value":"b"}],"id":"2","location":{"column":7,"line":9}},{"cells":[{"location":{"column":9,"line":10},"value":"1"},{"location":{"column":13,"line":10},"value":"2"}],"id":"3","location":{"column":7,"line":10}}]},"id":"4","keyword":"When ","keywordType":"Action","location":{"column":5,"line":8},"text":"the following table is transposed:"},{"dataTable":{"location":{"column":7,"line":12},"rows":[{"cells":[{"location":{"column":9,"line":12},"value":"a"},{"location":{"column":13,"line":12},"value":"1"}],"id":"5","location":{"column":7,"line":12}},{"cells":[{"location":{"column":9,"line":13},"value":"b"},{"location":{"column":13,"line":13},"value":"2"}],"id":"6","location":{"column":7,"line":13}}]},"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":11},"text":"it should be:"}],"tags":[]}}],"description":" Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Data Tables","tags":[]},"uri":"samples/data-tables/data-tables.feature"}} +{"pickle":{"astNodeIds":["8"],"id":"11","language":"en","name":"transposed table","steps":[{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"b"}]},{"cells":[{"value":"1"},{"value":"2"}]}]}},"astNodeIds":["4"],"id":"9","text":"the following table is transposed:","type":"Action"},{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"1"}]},{"cells":[{"value":"b"},{"value":"2"}]}]}},"astNodeIds":["7"],"id":"10","text":"it should be:","type":"Outcome"}],"tags":[],"uri":"samples/data-tables/data-tables.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the following table is transposed:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":5},"uri":"samples/data-tables/data-tables.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"it should be:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":9},"uri":"samples/data-tables/data-tables.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"14","pickleId":"11","testSteps":[{"id":"12","pickleStepId":"9","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"13","pickleStepId":"10","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"15","testCaseId":"14","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"12","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"12","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"13","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"13","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"15","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":7000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs new file mode 100644 index 000000000..a87217d3e --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs @@ -0,0 +1,54 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.examples_tables +{ + [Binding] + internal class example_tables + { + private int _count; + private int _friends; + + [Given("there are {int} cucumbers")] + public void GivenThereAreCucumbers(int p0) + { + _count = p0; + } + + [Given("there are {int} friends")] + public void GivenThereAreFriends(int p0) + { + _friends = p0; + } + + [When("I eat {int} cucumbers")] + public void WhenIEatCucumbers(int p0) + { + _count -= p0; + } + + [Then("I should have {int} cuc umbers")] + public void ThenIShouldHaveCucumbers(int p0) + { + if (_count != p0) + { + throw new Exception($"Cucumber count mismatch: Expected {p0}, got {_count}"); + } + } + + [Then("each person can eat {int} cucumbers")] + public void ThenEachPersonCanEatCucumbers(int p0) + { + var share = Math.Floor((double)_count / (1 + _friends)); + + if (share != p0) + { + throw new Exception($"Cucumber share mismatch: Expected {p0}, got {share}"); + } + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature new file mode 100644 index 000000000..6ce92b8c2 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature @@ -0,0 +1,43 @@ +Feature: Examples Tables + Sometimes it can be desirable to run the same scenario multiple times with + different data each time - this can be done by placing an Examples table + underneath a Scenario, and use in the Scenario which match the + table headers. + + The Scenario Outline name can also be parameterized. The name of the resulting + pickle will have the replaced with the value from the examples + table. + + Scenario Outline: Eating cucumbers + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + @passing + Examples: These are passing + | start | eat | left | + | 12 | 5 | 7 | + | 20 | 5 | 15 | + + @failing + Examples: These are failing + | start | eat | left | + | 12 | 20 | 0 | + | 0 | 1 | 0 | + + @undefined + Examples: These are undefined because the value is not an {int} + | start | eat | left | + | 12 | banana | 12 | + | 0 | 1 | apple | + + Scenario Outline: Eating cucumbers with friends + Given there are friends + And there are cucumbers + Then each person can eat cucumbers + + Examples: + | friends | start | share | + | 11 | 12 | 1 | + | 1 | 4 | 2 | + | 0 | 4 | 4 | diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson new file mode 100644 index 000000000..b169fd03d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson @@ -0,0 +1,100 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n @undefined\n Examples: These are undefined because the value is not an {int}\n | start | eat | left |\n | 12 | banana | 12 |\n | 0 | 1 | apple |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/examples-tables/examples-tables.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[{"description":"","id":"12","keyword":"Examples","location":{"column":5,"line":17},"name":"These are passing","tableBody":[{"cells":[{"location":{"column":12,"line":19},"value":"12"},{"location":{"column":19,"line":19},"value":"5"},{"location":{"column":26,"line":19},"value":"7"}],"id":"9","location":{"column":7,"line":19}},{"cells":[{"location":{"column":12,"line":20},"value":"20"},{"location":{"column":19,"line":20},"value":"5"},{"location":{"column":25,"line":20},"value":"15"}],"id":"10","location":{"column":7,"line":20}}],"tableHeader":{"cells":[{"location":{"column":9,"line":18},"value":"start"},{"location":{"column":17,"line":18},"value":"eat"},{"location":{"column":23,"line":18},"value":"left"}],"id":"8","location":{"column":7,"line":18}},"tags":[{"id":"11","location":{"column":5,"line":16},"name":"@passing"}]},{"description":"","id":"17","keyword":"Examples","location":{"column":5,"line":23},"name":"These are failing","tableBody":[{"cells":[{"location":{"column":12,"line":25},"value":"12"},{"location":{"column":18,"line":25},"value":"20"},{"location":{"column":26,"line":25},"value":"0"}],"id":"14","location":{"column":7,"line":25}},{"cells":[{"location":{"column":13,"line":26},"value":"0"},{"location":{"column":19,"line":26},"value":"1"},{"location":{"column":26,"line":26},"value":"0"}],"id":"15","location":{"column":7,"line":26}}],"tableHeader":{"cells":[{"location":{"column":9,"line":24},"value":"start"},{"location":{"column":17,"line":24},"value":"eat"},{"location":{"column":23,"line":24},"value":"left"}],"id":"13","location":{"column":7,"line":24}},"tags":[{"id":"16","location":{"column":5,"line":22},"name":"@failing"}]},{"description":"","id":"22","keyword":"Examples","location":{"column":5,"line":29},"name":"These are undefined because the value is not an {int}","tableBody":[{"cells":[{"location":{"column":12,"line":31},"value":"12"},{"location":{"column":17,"line":31},"value":"banana"},{"location":{"column":29,"line":31},"value":"12"}],"id":"19","location":{"column":7,"line":31}},{"cells":[{"location":{"column":13,"line":32},"value":"0"},{"location":{"column":22,"line":32},"value":"1"},{"location":{"column":26,"line":32},"value":"apple"}],"id":"20","location":{"column":7,"line":32}}],"tableHeader":{"cells":[{"location":{"column":9,"line":30},"value":"start"},{"location":{"column":17,"line":30},"value":"eat"},{"location":{"column":26,"line":30},"value":"left"}],"id":"18","location":{"column":7,"line":30}},"tags":[{"id":"21","location":{"column":5,"line":28},"name":"@undefined"}]}],"id":"23","keyword":"Scenario Outline","location":{"column":3,"line":11},"name":"Eating cucumbers","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":12},"text":"there are cucumbers"},{"id":"6","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"I eat cucumbers"},{"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":14},"text":"I should have cucumbers"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"31","keyword":"Examples","location":{"column":5,"line":39},"name":"","tableBody":[{"cells":[{"location":{"column":14,"line":41},"value":"11"},{"location":{"column":22,"line":41},"value":"12"},{"location":{"column":31,"line":41},"value":"1"}],"id":"28","location":{"column":7,"line":41}},{"cells":[{"location":{"column":15,"line":42},"value":"1"},{"location":{"column":23,"line":42},"value":"4"},{"location":{"column":31,"line":42},"value":"2"}],"id":"29","location":{"column":7,"line":42}},{"cells":[{"location":{"column":15,"line":43},"value":"0"},{"location":{"column":23,"line":43},"value":"4"},{"location":{"column":31,"line":43},"value":"4"}],"id":"30","location":{"column":7,"line":43}}],"tableHeader":{"cells":[{"location":{"column":9,"line":40},"value":"friends"},{"location":{"column":19,"line":40},"value":"start"},{"location":{"column":27,"line":40},"value":"share"}],"id":"27","location":{"column":7,"line":40}},"tags":[]}],"id":"32","keyword":"Scenario Outline","location":{"column":3,"line":34},"name":"Eating cucumbers with friends","steps":[{"id":"24","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":35},"text":"there are friends"},{"id":"25","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":36},"text":"there are cucumbers"},{"id":"26","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":37},"text":"each person can eat cucumbers"}],"tags":[]}}],"description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Examples Tables","tags":[]},"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","9"],"id":"36","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","9"],"id":"33","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","9"],"id":"34","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["7","9"],"id":"35","text":"I should have 7 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@passing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","10"],"id":"40","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","10"],"id":"37","text":"there are 20 cucumbers","type":"Context"},{"astNodeIds":["6","10"],"id":"38","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["7","10"],"id":"39","text":"I should have 15 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@passing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","14"],"id":"44","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","14"],"id":"41","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","14"],"id":"42","text":"I eat 20 cucumbers","type":"Action"},{"astNodeIds":["7","14"],"id":"43","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"16","name":"@failing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","15"],"id":"48","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","15"],"id":"45","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["6","15"],"id":"46","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["7","15"],"id":"47","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"16","name":"@failing"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","19"],"id":"52","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","19"],"id":"49","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","19"],"id":"50","text":"I eat banana cucumbers","type":"Action"},{"astNodeIds":["7","19"],"id":"51","text":"I should have 12 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"21","name":"@undefined"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["23","20"],"id":"56","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","20"],"id":"53","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["6","20"],"id":"54","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["7","20"],"id":"55","text":"I should have apple cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"21","name":"@undefined"}],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","28"],"id":"60","language":"en","name":"Eating cucumbers with 11 friends","steps":[{"astNodeIds":["24","28"],"id":"57","text":"there are 11 friends","type":"Context"},{"astNodeIds":["25","28"],"id":"58","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["26","28"],"id":"59","text":"each person can eat 1 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","29"],"id":"64","language":"en","name":"Eating cucumbers with 1 friends","steps":[{"astNodeIds":["24","29"],"id":"61","text":"there are 1 friends","type":"Context"},{"astNodeIds":["25","29"],"id":"62","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["26","29"],"id":"63","text":"each person can eat 2 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"astNodeIds":["32","30"],"id":"68","language":"en","name":"Eating cucumbers with 0 friends","steps":[{"astNodeIds":["24","30"],"id":"65","text":"there are 0 friends","type":"Context"},{"astNodeIds":["25","30"],"id":"66","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["26","30"],"id":"67","text":"each person can eat 4 cucumbers","type":"Outcome"}],"tags":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"there are {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"there are {int} friends","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"I eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"I should have {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"each person can eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":20},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"72","pickleId":"36","testSteps":[{"id":"69","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"70","pickleStepId":"34","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"35","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"76","pickleId":"40","testSteps":[{"id":"73","pickleStepId":"37","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"74","pickleStepId":"38","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"80","pickleId":"44","testSteps":[{"id":"77","pickleStepId":"41","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"78","pickleStepId":"42","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"43","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"84","pickleId":"48","testSteps":[{"id":"81","pickleStepId":"45","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"82","pickleStepId":"46","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"47","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"88","pickleId":"52","testSteps":[{"id":"85","pickleStepId":"49","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"86","pickleStepId":"50","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"87","pickleStepId":"51","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"12"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"92","pickleId":"56","testSteps":[{"id":"89","pickleStepId":"53","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"90","pickleStepId":"54","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"91","pickleStepId":"55","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"96","pickleId":"60","testSteps":[{"id":"93","pickleStepId":"57","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"11"},"parameterTypeName":"int"}]}]},{"id":"94","pickleStepId":"58","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"95","pickleStepId":"59","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"1"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"100","pickleId":"64","testSteps":[{"id":"97","pickleStepId":"61","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"98","pickleStepId":"62","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"99","pickleStepId":"63","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"2"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"104","pickleId":"68","testSteps":[{"id":"101","pickleStepId":"65","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"102","pickleStepId":"66","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"103","pickleStepId":"67","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"4"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"105","testCaseId":"72","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"69","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"70","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"70","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"105","testStepId":"71","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"105","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"105","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"106","testCaseId":"76","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"73","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"74","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"74","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"75","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"106","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"107","testCaseId":"80","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"77","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"77","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"78","timestamp":{"nanos":20000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"78","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"79","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"79","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:25","status":"FAILED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"107","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"108","testCaseId":"84","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"81","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"81","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"82","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"82","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"83","timestamp":{"nanos":30000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"83","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:26","status":"FAILED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"108","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"109","testCaseId":"88","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"85","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"85","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"86","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"86","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"87","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"87","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"109","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"110","testCaseId":"92","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"89","timestamp":{"nanos":42000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"89","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"90","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"90","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"91","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"91","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"110","timestamp":{"nanos":48000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"111","testCaseId":"96","timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"93","timestamp":{"nanos":50000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"93","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"94","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"94","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"95","timestamp":{"nanos":54000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"95","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":55000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"111","timestamp":{"nanos":56000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"112","testCaseId":"100","timestamp":{"nanos":57000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"97","timestamp":{"nanos":58000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"97","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":59000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"98","timestamp":{"nanos":60000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"98","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":61000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"99","timestamp":{"nanos":62000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"99","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":63000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"112","timestamp":{"nanos":64000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"113","testCaseId":"104","timestamp":{"nanos":65000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"101","timestamp":{"nanos":66000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"101","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":67000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"102","timestamp":{"nanos":68000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"102","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":69000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"103","timestamp":{"nanos":70000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"103","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":71000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"113","timestamp":{"nanos":72000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":73000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg new file mode 100644 index 000000000..e76ff7faf --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature new file mode 100644 index 000000000..4136a8a51 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature @@ -0,0 +1,21 @@ +Feature: Hooks + Hooks are special steps that run before or after each scenario's steps. + + They can also conditionally target specific scenarios, using tag expressions + + Scenario: No tags and a passed step + When a step passes + + Scenario: No tags and a failed step + When a step fails + + Scenario: No tags and a undefined step + When a step does not exist + + @some-tag + Scenario: With a tag, a failure in the hook and a passed step + When a step passes + + @with-attachment + Scenario: With an tag, an valid attachment in the hook and a passed step + When a step passes diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson new file mode 100644 index 000000000..f3946e576 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson @@ -0,0 +1,77 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n\n Scenario: No tags and a undefined step\n When a step does not exist\n\n @some-tag\n Scenario: With a tag, a failure in the hook and a passed step\n When a step passes\n\n @with-attachment\n Scenario: With an tag, an valid attachment in the hook and a passed step\n When a step passes\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/hooks/hooks.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":6},"name":"No tags and a passed step","steps":[{"id":"7","keyword":"When ","keywordType":"Action","location":{"column":5,"line":7},"text":"a step passes"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":9},"name":"No tags and a failed step","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":10},"text":"a step fails"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":12},"name":"No tags and a undefined step","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"a step does not exist"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Scenario","location":{"column":3,"line":16},"name":"With a tag, a failure in the hook and a passed step","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":17},"text":"a step passes"}],"tags":[{"id":"14","location":{"column":3,"line":15},"name":"@some-tag"}]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":20},"name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"id":"16","keyword":"When ","keywordType":"Action","location":{"column":5,"line":21},"text":"a step passes"}],"tags":[{"id":"17","location":{"column":3,"line":19},"name":"@with-attachment"}]}}],"description":" Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Hooks","tags":[]},"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["8"],"id":"20","language":"en","name":"No tags and a passed step","steps":[{"astNodeIds":["7"],"id":"19","text":"a step passes","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"22","language":"en","name":"No tags and a failed step","steps":[{"astNodeIds":["9"],"id":"21","text":"a step fails","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"24","language":"en","name":"No tags and a undefined step","steps":[{"astNodeIds":["11"],"id":"23","text":"a step does not exist","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["15"],"id":"26","language":"en","name":"With a tag, a failure in the hook and a passed step","steps":[{"astNodeIds":["13"],"id":"25","text":"a step passes","type":"Action"}],"tags":[{"astNodeId":"14","name":"@some-tag"}],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"astNodeIds":["18"],"id":"28","language":"en","name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"astNodeIds":["16"],"id":"27","text":"a step passes","type":"Action"}],"tags":[{"astNodeId":"17","name":"@with-attachment"}],"uri":"samples/hooks/hooks.feature"}} +{"stepDefinition":{"id":"2","pattern":{"source":"a step passes","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/hooks/hooks.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"a step fails","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":4},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"1","name":"A named hook","sourceReference":{"location":{"line":8},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"4","sourceReference":{"location":{"line":20},"uri":"samples/hooks/hooks.feature.ts"}}} +{"hook":{"id":"5","sourceReference":{"location":{"line":24},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@some-tag or @some-other-tag"}} +{"hook":{"id":"6","sourceReference":{"location":{"line":28},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@with-attachment"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"33","pickleId":"20","testSteps":[{"hookId":"0","id":"29"},{"hookId":"1","id":"30"},{"id":"31","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"32"}]}} +{"testCase":{"id":"38","pickleId":"22","testSteps":[{"hookId":"0","id":"34"},{"hookId":"1","id":"35"},{"id":"36","pickleStepId":"21","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"37"}]}} +{"testCase":{"id":"43","pickleId":"24","testSteps":[{"hookId":"0","id":"39"},{"hookId":"1","id":"40"},{"id":"41","pickleStepId":"23","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"hookId":"4","id":"42"}]}} +{"testCase":{"id":"49","pickleId":"26","testSteps":[{"hookId":"0","id":"44"},{"hookId":"1","id":"45"},{"id":"46","pickleStepId":"25","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"5","id":"47"},{"hookId":"4","id":"48"}]}} +{"testCase":{"id":"55","pickleId":"28","testSteps":[{"hookId":"0","id":"50"},{"hookId":"1","id":"51"},{"id":"52","pickleStepId":"27","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"6","id":"53"},{"hookId":"4","id":"54"}]}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"33","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"29","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"29","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"30","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"30","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"31","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"31","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"32","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"32","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"57","testCaseId":"38","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"34","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"34","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"35","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"35","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"36","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"36","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in step","type":"Error"},"message":"Exception in step\nsamples/hooks/hooks.feature:10","status":"FAILED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"37","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"37","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"58","testCaseId":"43","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"39","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"40","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"41","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"41","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"42","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"59","testCaseId":"49","timestamp":{"nanos":31000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"44","timestamp":{"nanos":32000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"46","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"47","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in conditional hook","type":"Error"},"message":"Exception in conditional hook\nsamples/hooks/hooks.feature:16","status":"FAILED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"48","timestamp":{"nanos":40000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"48","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":41000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"nanos":42000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"60","testCaseId":"55","timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"50","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"51","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"52","timestamp":{"nanos":48000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"53","timestamp":{"nanos":50000000,"seconds":0}}} +{"attachment":{"body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","testCaseStartedId":"60","testStepId":"53"}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"53","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"54","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"54","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"nanos":54000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":55000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs new file mode 100644 index 000000000..faad8ebb5 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs @@ -0,0 +1,36 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.parameter_types +{ + [Binding] + internal class Parameter_types + { + [StepArgumentTransformation(@"([A-Z]{3})-([A-Z]{3})")] + public Flight FlightConverter(string from, string to) + { + return new Flight + { + From = from, + To = to + }; + } + + [Given("{flight} has been delayed")] + public void GivenFlightHasBeenDelayed(Flight flight) + { + if (flight.From == "LHR" && flight.To == "CDG") { } + else throw new Exception(); + } + } + + public class Flight + { + public string? From { get; internal set; } + public string? To { get; internal set; } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature new file mode 100644 index 000000000..67e099463 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature @@ -0,0 +1,11 @@ +Feature: Parameter Types + Cucumber lets you define your own parameter types, which can be used + in Cucumber Expressions. + + This lets you define a precise domain-specific vocabulary which can be used to + generate a glossary with examples taken from your scenarios. + + Parameter types also enable you to transform strings and tables into different types. + + Scenario: Flight transformer + Given LHR-CDG has been delayed diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson new file mode 100644 index 000000000..b2eddd562 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson @@ -0,0 +1,13 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.\n\n Scenario: Flight transformer\n Given LHR-CDG has been delayed\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/parameter-types/parameter-types.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":10},"name":"Flight transformer","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"LHR-CDG has been delayed"}],"tags":[]}}],"description":" Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/parameter-types/parameter-types.feature"}} +{"pickle":{"astNodeIds":["3"],"id":"5","language":"en","name":"Flight transformer","steps":[{"astNodeIds":["2"],"id":"4","text":"LHR-CDG has been delayed","type":"Context"}],"tags":[],"uri":"samples/parameter-types/parameter-types.feature"}} +{"parameterType":{"id":"0","name":"flight","preferForRegularExpressionMatch":false,"regularExpressions":["([A-Z]{3})-([A-Z]{3})"],"sourceReference":{"location":{"line":8},"uri":"samples/parameter-types/parameter-types.feature.ts"},"useForSnippets":true}} +{"stepDefinition":{"id":"1","pattern":{"source":"{flight} has been delayed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/parameter-types/parameter-types.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"7","pickleId":"5","testSteps":[{"id":"6","pickleStepId":"4","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[],"start":0,"value":"LHR"},{"children":[],"start":4,"value":"CDG"}],"start":0,"value":"LHR-CDG"},"parameterTypeName":"flight"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs new file mode 100644 index 000000000..19104787d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs @@ -0,0 +1,61 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.rules +{ + [Binding] + internal class Rules + { + private int Money; + private Stack Stock = new(); + private string _chocolate = ""; + + [Given("the customer has {int} cents")] + public void GivenTheCustomerHasCents(int p0) + { + Money = p0; + } + + [Given("there are chocolate bars in stock")] + public void GivenThereAreChocolateBarsInStock() + { + Stock = new Stack(); + Stock.Push("Mars"); + } + + [Given("there are no chocolate bars in stock")] + public void GivenThereAreNoChocolateBarsInStock() + { + Stock = new Stack(); + } + + [When("the customer tries to buy a {int} cent chocolate bar")] + public void WhenTheCustomerTriesToBuyACentChocolateBar(int p0) + { + if (Money >= p0) + { + if (!Stock.TryPop(out _chocolate!)) + _chocolate = ""; + } + } + + [Then("the sale should not happen")] + public void ThenTheSaleShouldNotHappen() + { + if (!string.IsNullOrEmpty(_chocolate)) + throw new Exception("Sale should not happen"); + } + + [Then("the sale should happen")] + public void ThenTheSaleShouldHappen() + { + if (string.IsNullOrEmpty(_chocolate)) + throw new Exception("Sale should happen"); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature new file mode 100644 index 000000000..5d576ac70 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature @@ -0,0 +1,29 @@ +Feature: Usage of a `Rule` + You can place scenarios inside rules. This makes it possible to structure Gherkin documents + in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/). + + You can also use the Examples synonym for Scenario to make them even similar. + + Rule: A sale cannot happen if the customer does not have enough money + # Unhappy path + Example: Not enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 125 cent chocolate bar + Then the sale should not happen + + # Happy path + Example: Enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 75 cent chocolate bar + Then the sale should happen + + @some-tag + Rule: a sale cannot happen if there is no stock + # Unhappy path + Example: No chocolates left + Given the customer has 100 cents + And there are no chocolate bars in stock + When the customer tries to buy a 1 cent chocolate bar + Then the sale should not happen diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson new file mode 100644 index 000000000..58839fa63 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson @@ -0,0 +1,47 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/rules/rules.feature"}} +{"gherkinDocument":{"comments":[{"location":{"column":1,"line":8},"text":" # Unhappy path"},{"location":{"column":1,"line":15},"text":" # Happy path"},{"location":{"column":1,"line":24},"text":" # Unhappy path"}],"feature":{"children":[{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"10","keyword":"Example","location":{"column":5,"line":9},"name":"Not enough money","steps":[{"id":"6","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":10},"text":"the customer has 100 cents"},{"id":"7","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":11},"text":"there are chocolate bars in stock"},{"id":"8","keyword":"When ","keywordType":"Action","location":{"column":7,"line":12},"text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"9","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":13},"text":"the sale should not happen"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Example","location":{"column":5,"line":16},"name":"Enough money","steps":[{"id":"11","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":17},"text":"the customer has 100 cents"},{"id":"12","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":18},"text":"there are chocolate bars in stock"},{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":7,"line":19},"text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"14","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":20},"text":"the sale should happen"}],"tags":[]}}],"description":"","id":"16","keyword":"Rule","location":{"column":3,"line":7},"name":"A sale cannot happen if the customer does not have enough money","tags":[]}},{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"21","keyword":"Example","location":{"column":5,"line":25},"name":"No chocolates left","steps":[{"id":"17","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":26},"text":"the customer has 100 cents"},{"id":"18","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":27},"text":"there are no chocolate bars in stock"},{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":7,"line":28},"text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"20","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":29},"text":"the sale should not happen"}],"tags":[]}}],"description":"","id":"23","keyword":"Rule","location":{"column":3,"line":23},"name":"a sale cannot happen if there is no stock","tags":[{"id":"22","location":{"column":3,"line":22},"name":"@some-tag"}]}}],"description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Usage of a `Rule`","tags":[]},"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["10"],"id":"28","language":"en","name":"Not enough money","steps":[{"astNodeIds":["6"],"id":"24","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["7"],"id":"25","text":"there are chocolate bars in stock","type":"Context"},{"astNodeIds":["8"],"id":"26","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action"},{"astNodeIds":["9"],"id":"27","text":"the sale should not happen","type":"Outcome"}],"tags":[],"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["15"],"id":"33","language":"en","name":"Enough money","steps":[{"astNodeIds":["11"],"id":"29","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["12"],"id":"30","text":"there are chocolate bars in stock","type":"Context"},{"astNodeIds":["13"],"id":"31","text":"the customer tries to buy a 75 cent chocolate bar","type":"Action"},{"astNodeIds":["14"],"id":"32","text":"the sale should happen","type":"Outcome"}],"tags":[],"uri":"samples/rules/rules.feature"}} +{"pickle":{"astNodeIds":["21"],"id":"38","language":"en","name":"No chocolates left","steps":[{"astNodeIds":["17"],"id":"34","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["18"],"id":"35","text":"there are no chocolate bars in stock","type":"Context"},{"astNodeIds":["19"],"id":"36","text":"the customer tries to buy a 1 cent chocolate bar","type":"Action"},{"astNodeIds":["20"],"id":"37","text":"the sale should not happen","type":"Outcome"}],"tags":[{"astNodeId":"22","name":"@some-tag"}],"uri":"samples/rules/rules.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"the customer has {int} cents","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":4},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"there are chocolate bars in stock","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":8},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"there are no chocolate bars in stock","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"the customer tries to buy a {int} cent chocolate bar","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"4","pattern":{"source":"the sale should not happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"samples/rules/rules.feature.ts"}}} +{"stepDefinition":{"id":"5","pattern":{"source":"the sale should happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":26},"uri":"samples/rules/rules.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"43","pickleId":"28","testSteps":[{"id":"39","pickleStepId":"24","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"40","pickleStepId":"25","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"41","pickleStepId":"26","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"125"},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"27","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"48","pickleId":"33","testSteps":[{"id":"44","pickleStepId":"29","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"45","pickleStepId":"30","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"46","pickleStepId":"31","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"75"},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"32","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"53","pickleId":"38","testSteps":[{"id":"49","pickleStepId":"34","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"50","pickleStepId":"35","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"51","pickleStepId":"36","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"37","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"43","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"39","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"40","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"41","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"41","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"42","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"55","testCaseId":"48","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"45","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"46","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"53","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"50","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"51","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"52","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":31000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs new file mode 100644 index 000000000..1362f1d5f --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs @@ -0,0 +1,37 @@ +using Reqnroll; +using Reqnroll.UnitTestProvider; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.skipped +{ + [Binding] + internal class Skipped + { + private IUnitTestRuntimeProvider _unitTestRuntimeProvider; + + public Skipped(IUnitTestRuntimeProvider unitTestRuntimeProvider) + { + _unitTestRuntimeProvider = unitTestRuntimeProvider; + } + + [Given("a step that does not skip")] + public void GivenAStepThatDoesNotSkip() + { + } + + [Given("a step that is skipped")] + public void GivenAStepThatSkips() + { + } + + [Given("I skip a step")] + public void GivenISkipAStep() + { + _unitTestRuntimeProvider.TestIgnore("Skipped"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature new file mode 100644 index 000000000..e73a7f8af --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature @@ -0,0 +1,19 @@ +Feature: Skipping scenarios + + Hooks and step definitions are able to signal at runtime that the scenario should + be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED). + + This can be useful in certain situations e.g. the current environment doesn't have + the right conditions for running a particular scenario. + + @ignore + Scenario: Skipping from a Before hook + Given a step that is skipped + + Scenario: Skipping from a step doesn't affect the previous steps + Given a step that does not skip + And I skip a step + + Scenario: Skipping from a step causes the rest of the scenario to be skipped + Given I skip a step + And a step that is skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson new file mode 100644 index 000000000..746921a8b --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson @@ -0,0 +1,33 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Skipping scenarios\n\n Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.\n\n @skip\n Scenario: Skipping from a Before hook\n Given a step that is skipped\n\n Scenario: Skipping from a step doesn't affect the previous steps\n Given a step that does not skip\n And I skip a step\n\n Scenario: Skipping from a step causes the rest of the scenario to be skipped\n Given I skip a step\n And a step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/skipped/skipped.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Skipping from a Before hook","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"a step that is skipped"}],"tags":[{"id":"5","location":{"column":3,"line":9},"name":"@skip"}]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":13},"name":"Skipping from a step doesn't affect the previous steps","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":14},"text":"a step that does not skip"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":15},"text":"I skip a step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":17},"name":"Skipping from a step causes the rest of the scenario to be skipped","steps":[{"id":"10","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":18},"text":"I skip a step"},{"id":"11","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":19},"text":"a step that is skipped"}],"tags":[]}}],"description":" Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Skipping scenarios","tags":[]},"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["6"],"id":"14","language":"en","name":"Skipping from a Before hook","steps":[{"astNodeIds":["4"],"id":"13","text":"a step that is skipped","type":"Context"}],"tags":[{"astNodeId":"5","name":"@skip"}],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["9"],"id":"17","language":"en","name":"Skipping from a step doesn't affect the previous steps","steps":[{"astNodeIds":["7"],"id":"15","text":"a step that does not skip","type":"Context"},{"astNodeIds":["8"],"id":"16","text":"I skip a step","type":"Context"}],"tags":[],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"astNodeIds":["12"],"id":"20","language":"en","name":"Skipping from a step causes the rest of the scenario to be skipped","steps":[{"astNodeIds":["10"],"id":"18","text":"I skip a step","type":"Context"},{"astNodeIds":["11"],"id":"19","text":"a step that is skipped","type":"Context"}],"tags":[],"uri":"samples/skipped/skipped.feature"}} +{"stepDefinition":{"id":"1","pattern":{"source":"a step that does not skip","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/skipped/skipped.feature.ts"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"a step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/skipped/skipped.feature.ts"}}} +{"stepDefinition":{"id":"3","pattern":{"source":"I skip a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":15},"uri":"samples/skipped/skipped.feature.ts"}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":3},"uri":"samples/skipped/skipped.feature.ts"},"tagExpression":"@skip"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"23","pickleId":"14","testSteps":[{"hookId":"0","id":"21"},{"id":"22","pickleStepId":"13","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"26","pickleId":"17","testSteps":[{"id":"24","pickleStepId":"15","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"16","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"29","pickleId":"20","testSteps":[{"id":"27","pickleStepId":"18","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"28","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"30","testCaseId":"23","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"21","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"22","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"22","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"31","testCaseId":"26","timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"25","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"25","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"32","testCaseId":"29","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"27","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"27","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"28","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"28","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"nanos":18000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":19000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs new file mode 100644 index 000000000..b112fcaa9 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs @@ -0,0 +1,23 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.undefined +{ + [Binding] + internal class Undefined + { + [Given("an implemented step")] + public void GivenAnImplementedStep() + { + } + + [Given("a step that will be skipped")] + public void GivenAStepThatWillBeSkipped() + { + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature new file mode 100644 index 000000000..318ebea97 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature @@ -0,0 +1,16 @@ +Feature: Undefined steps + + At runtime, Cucumber may encounter a step in a scenario that it cannot match to a + step definition. In these cases, the scenario is not able to run and so the step status + will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE + + Scenario: An undefined step causes a failure + Given a step that is yet to be defined + + Scenario: Steps before undefined steps are executed + Given an implemented step + And a step that is yet to be defined + + Scenario: Steps after undefined steps are skipped + Given a step that is yet to be defined + And a step that will be skipped diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson new file mode 100644 index 000000000..f15440837 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson @@ -0,0 +1,29 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Undefined steps\n\n At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE\n\n Scenario: An undefined step causes a failure\n Given a step that is yet to be defined\n\n Scenario: Steps before undefined steps are executed\n Given an implemented step\n And a step that is yet to be defined\n\n Scenario: Steps after undefined steps are skipped\n Given a step that is yet to be defined\n And a step that will be skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/undefined/undefined.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":7},"name":"An undefined step causes a failure","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":8},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Steps before undefined steps are executed","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"an implemented step"},{"id":"5","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":12},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":14},"name":"Steps after undefined steps are skipped","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":15},"text":"a step that is yet to be defined"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":16},"text":"a step that will be skipped"}],"tags":[]}}],"description":" At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Undefined steps","tags":[]},"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["3"],"id":"11","language":"en","name":"An undefined step causes a failure","steps":[{"astNodeIds":["2"],"id":"10","text":"a step that is yet to be defined","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["6"],"id":"14","language":"en","name":"Steps before undefined steps are executed","steps":[{"astNodeIds":["4"],"id":"12","text":"an implemented step","type":"Context"},{"astNodeIds":["5"],"id":"13","text":"a step that is yet to be defined","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"astNodeIds":["9"],"id":"17","language":"en","name":"Steps after undefined steps are skipped","steps":[{"astNodeIds":["7"],"id":"15","text":"a step that is yet to be defined","type":"Context"},{"astNodeIds":["8"],"id":"16","text":"a step that will be skipped","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"an implemented step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/undefined/undefined.feature.ts"}}} +{"stepDefinition":{"id":"1","pattern":{"source":"a step that will be skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/undefined/undefined.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"19","pickleId":"11","testSteps":[{"id":"18","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"22","pickleId":"14","testSteps":[{"id":"20","pickleStepId":"12","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"21","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"25","pickleId":"17","testSteps":[{"id":"23","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"24","pickleStepId":"16","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"26","testCaseId":"19","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"26","testStepId":"18","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"26","testStepId":"18","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"26","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"22","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"20","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"20","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"21","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"21","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"25","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"23","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"23","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"24","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"24","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs new file mode 100644 index 000000000..15dacff91 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs @@ -0,0 +1,23 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.unknown_parameter_type +{ + [Binding] + internal class Unknown_parameter_type + { + [Given("{airport} is closed because of a strike")] + public void GivenAirportIsClosedBecauseOfStrike(Airport airport) + { + throw new Exception("Should not be called because airport parameter type has not been defined"); + } + } + + public class Airport + { + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature new file mode 100644 index 000000000..4ce05c974 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature @@ -0,0 +1,6 @@ +Feature: Parameter Types + Cucumber will generate an error message if a step definition registers + an unknown parameter type, but the suite will run. + + Scenario: undefined parameter type + Given CDG is closed because of a strike diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson new file mode 100644 index 000000000..cc9886734 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"1","keyword":"Scenario","location":{"column":3,"line":5},"name":"undefined parameter type","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":6},"text":"CDG is closed because of a strike"}],"tags":[]}}],"description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"pickle":{"astNodeIds":["1"],"id":"3","language":"en","name":"undefined parameter type","steps":[{"astNodeIds":["0"],"id":"2","text":"CDG is closed because of a strike","type":"Context"}],"tags":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"undefinedParameterType":{"expression":"{airport} is closed because of a strike","name":"airport"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"5","pickleId":"3","testSteps":[{"id":"4","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCaseStarted":{"attempt":0,"id":"6","testCaseId":"5","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"6","testStepId":"4","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"6","testStepId":"4","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"6","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7bc6963ae..2e92ffc88 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -26,31 +26,29 @@ Given I have a passing step [TestMethod] [DataRow("minimal")] + [DataRow("cdata")] + [DataRow("pending")] + [DataRow("examples-tables")] + [DataRow("data-tables")] + [DataRow("parameter-types")] + [DataRow("skipped")] + [DataRow("undefined")] + [DataRow("unknown-parameter-type")] + [DataRow("rules")] public void CCKPassingScenarios(string scenarioName) { AddCucumberMessagePlugIn(); - AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - [TestMethod] - [DataRow("pending")] - public void CCKPendingScenarios(string scenarioName) - { - AddCucumberMessagePlugIn(); + scenarioName = scenarioName.Replace("-", "_"); AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); - ShouldAllScenariosPend(); + ConfirmAllTestsRan(null); } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 4464aac06..8ba25ff8d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,24 +10,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 66b7037031a00dab20373b322cf4c3d04dfd60fb Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:48:13 -0500 Subject: [PATCH 107/218] Added Attachments and Hooks bindings. Needs additional work to properly handle binary files. --- .../CCK/attachments/attachments.cs | 19 +++++++ .../CCK/hooks/hooks.cs | 51 +++++++++++++++++++ .../CucumberCompatibilityTestBase.cs | 18 +++++++ .../CucumberCompatibilityTests.cs | 9 +++- ...CucumberMessages.CompatibilityTests.csproj | 4 ++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs new file mode 100644 index 000000000..877a2594d --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.attachments +{ + [Binding] + internal class Attachments + { + [When(@"a JPEG image is attached")] + public void WhenAJPEGImageIsAttached() + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs new file mode 100644 index 000000000..117f2cccd --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -0,0 +1,51 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.hooks +{ + [Binding] + internal class Hooks + { + private readonly IReqnrollOutputHelper reqnrollOutputHelper; + + public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) + { + this.reqnrollOutputHelper = reqnrollOutputHelper; + } + + [When("a step passes")] + public void AStepPasses() + { + } + + [When("a step fails")] + public void AStepFails() + { + throw new Exception("Exception in step"); + } + + // When a step does not exist - no implementation should be generated + + // Hook implementations + [BeforeScenario] + [AfterScenario] + public void ScenarioHook() { } + + [AfterScenario(new string[] { "some-tag", "some-other-tag" })] + public void FailingAfterHook() + { + throw new Exception("Exception in conditional hook"); + } + + [AfterScenario("with-attachment")] + public void PassingAfterHook() + { + reqnrollOutputHelper.AddAttachment("cucumber.svg"); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 419805f39..1f2e26629 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -28,5 +28,23 @@ protected void ShouldAllScenariosPend(int? expectedNrOfTestsSpec = null) int expectedNrOfTests = ConfirmAllTestsRan(expectedNrOfTestsSpec); _vsTestExecutionDriver.LastTestExecutionResult.Pending.Should().Be(expectedNrOfTests, "all tests should pend"); } + + protected void AddBinaryFilesFromResource(string scenarioName, string prefix, Assembly assembly) + { + foreach (var fileName in GetTestBinaryFileNames(scenarioName, prefix, assembly)) + { + var content = _testFileManager.GetTestFileContent(fileName, $"{prefix}.{scenarioName}", assembly); + _projectsDriver.AddFile(fileName, content); + } + } + + protected IEnumerable GetTestBinaryFileNames(string scenarioName, string prefix, Assembly? assembly) + { + var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); + string prefixToRemove = $"{prefix}.{scenarioName}."; + return testAssembly.GetManifestResourceNames() + .Where(rn => !rn.EndsWith(".feature") && !rn.EndsWith(".cs") && !rn.EndsWith(".feature.ndjson") && rn.StartsWith(prefixToRemove)) + .Select(rn => rn.Substring(prefixToRemove.Length)); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 2e92ffc88..29804c65c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -25,10 +25,12 @@ Given I have a passing step } [TestMethod] + //[DataRow("attachments")] [DataRow("minimal")] [DataRow("cdata")] [DataRow("pending")] [DataRow("examples-tables")] + [DataRow("hooks")] [DataRow("data-tables")] [DataRow("parameter-types")] [DataRow("skipped")] @@ -43,12 +45,15 @@ public void CCKPassingScenarios(string scenarioName) AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); ConfirmAllTestsRan(null); + if (scenarioName == "attachments") + { + ShouldAllScenariosPass(); + } } - - } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 8ba25ff8d..ee82ca5de 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,9 +10,11 @@ + + @@ -54,6 +56,7 @@ + @@ -69,6 +72,7 @@ + From 63e073ea9299da138360cba3e76912c339535db5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:06:56 -0500 Subject: [PATCH 108/218] Added Hook support. Fails on undefined steps (but not specific to Hooks). Made change to TestExecutionEngine to add Feature, Scenario, and Step context to the HookBinding events. --- .../CucumberMessageFactory.cs | 119 +++++++++++++----- .../CucumberMessagePublisher.cs | 70 +++++------ ...atureState.cs => FeatureEventProcessor.cs} | 104 +++++++++++---- .../CucumberMesssages/HookStepProcessor.cs | 24 ++++ Reqnroll/CucumberMesssages/IStepProcessor.cs | 10 ++ .../{StepState.cs => PickleStepProcessor.cs} | 24 ++-- ...arioState.cs => ScenarioEventProcessor.cs} | 43 +++++-- .../CucumberMesssages/StepProcessorBase.cs | 23 ++++ Reqnroll/Events/ExecutionEvent.cs | 7 ++ .../Infrastructure/TestExecutionEngine.cs | 2 +- .../CCK/hooks/hooks.cs | 9 +- 11 files changed, 310 insertions(+), 125 deletions(-) rename Reqnroll/CucumberMesssages/{FeatureState.cs => FeatureEventProcessor.cs} (53%) create mode 100644 Reqnroll/CucumberMesssages/HookStepProcessor.cs create mode 100644 Reqnroll/CucumberMesssages/IStepProcessor.cs rename Reqnroll/CucumberMesssages/{StepState.cs => PickleStepProcessor.cs} (74%) rename Reqnroll/CucumberMesssages/{ScenarioState.cs => ScenarioEventProcessor.cs} (60%) create mode 100644 Reqnroll/CucumberMesssages/StepProcessorBase.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 8974ba766..1358dc745 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -11,23 +11,34 @@ namespace Reqnroll.CucumberMesssages { internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(FeatureState featureState, FeatureStartedEvent featureStartedEvent) + public static TestRunStarted ToTestRunStarted(FeatureEventProcessor featureState, FeatureStartedEvent featureStartedEvent) { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } - public static TestRunFinished ToTestRunFinished(FeatureState featureState, FeatureFinishedEvent featureFinishedEvent) + public static TestRunFinished ToTestRunFinished(FeatureEventProcessor featureState, FeatureFinishedEvent featureFinishedEvent) { return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); } - internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); foreach (var stepState in scenarioState.Steps) { - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState); - testSteps.Add(testStep); + switch (stepState) + { + case PickleStepProcessor _: + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as PickleStepProcessor); + testSteps.Add(testStep); + break; + case HookStepProcessor _: + var hookTestStep = CucumberMessageFactory.ToHookTestStep( stepState as HookStepProcessor); + testSteps.Add(hookTestStep); + break; + default: + throw new NotImplementedException(); + } } var testCase = new TestCase ( @@ -37,11 +48,11 @@ internal static TestCase ToTestCase(ScenarioState scenarioState, ScenarioStarted ); return testCase; } - internal static TestCaseStarted ToTestCaseStarted(ScenarioState scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCaseStarted ToTestCaseStarted(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) { return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); } - internal static TestCaseFinished ToTestCaseFinished(ScenarioState scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) { return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); } @@ -51,12 +62,7 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, var expressionType = binding.ExpressionType; var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); - - var methodName = binding.Method.Name; - var className = binding.Method.Type.Name; - var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); - var methodDescription = new JavaMethod(className, className, paramTypes); - var sourceRef = SourceReference.Create(methodDescription); + SourceReference sourceRef = ToSourceRef(binding); var result = new StepDefinition ( @@ -67,21 +73,31 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } - internal static TestStep ToTestStep(ScenarioState scenarioState, StepState stepState) + private static SourceReference ToSourceRef(IBinding binding) + { + var methodName = binding.Method.Name; + var className = binding.Method.Type.Name; + var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); + var methodDescription = new JavaMethod(className, methodName, paramTypes); + var sourceRef = SourceReference.Create(methodDescription); + return sourceRef; + } + + internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) { //TODO: This only works if the step is properly bound. Need to determine what to do otherwise - var args = stepState.StepArguments - .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) - .ToList(); + var args = stepState.StepArguments + .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) + .ToList(); - var result = new TestStep( - null, - stepState.TestStepID, - stepState.PickleStepID, - new List { stepState.StepDefinitionId }, - new List { new StepMatchArgumentsList(args) } - ); + var result = new TestStep( + null, + stepState.TestStepID, + stepState.PickleStepID, + new List { stepState.StepDefinitionId }, + new List { new StepMatchArgumentsList(args) } + ); return result; } @@ -96,7 +112,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), argument.Type); } - internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -104,16 +120,49 @@ internal static TestStepStarted ToTestStepStarted(StepState stepState, StepStart Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(StepState stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(PickleStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, stepState.TestStepID, - ToTestStepResult(stepState, stepFinishedEvent), + ToTestStepResult(stepState), Converters.ToTimestamp(stepFinishedEvent.Timestamp)); } - private static TestStepResult ToTestStepResult(StepState stepState, StepFinishedEvent stepFinishedEvent) + internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) + { + SourceReference sourceRef = ToSourceRef(hookBinding); + + var result = new Hook + ( + iDGenerator.GetNewId(), + null, + sourceRef, + hookBinding.IsScoped ? hookBinding.BindingScope.Tag : null + ); + return result; + } + + internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) + { + // find the Hook message at the Feature level + var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); + var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; + + return new TestStep(hookId, hookStepState.TestStepID, null, new List(), new List()); + } + internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + { + return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + } + + internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + { + return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + } + + + private static TestStepResult ToTestStepResult(StepProcessorBase stepState) { return new TestStepResult( Converters.ToDuration(stepState.Duration), @@ -141,11 +190,21 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat // utility methods public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { - - var signature = stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + string signature = GenerateSignature(stepDefinition); return $"{stepDefinition.SourceExpression}({signature})"; } + public static string CanonicalizeHookBinding(IHookBinding hookBinding) + { + string signature = GenerateSignature(hookBinding); + return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + } + + private static string GenerateSignature(IBinding stepDefinition) + { + return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 5f415cabf..6915f60d1 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -22,7 +22,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private ConcurrentDictionary featureStatesByFeatureName = new(); + private ConcurrentDictionary featureProcessorsByFeatureName = new(); public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -50,22 +50,24 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - var featureState = new FeatureState + var featureProc = new FeatureEventProcessor { Name = featureName, Enabled = enabled }; - if (!featureStatesByFeatureName.TryAdd(featureName, featureState)) + if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) { // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureStatesByFeatureName[featureName]; + var featureState_alreadyrunning = featureProcessorsByFeatureName[featureName]; featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well // None of the rest of this method should be executed @@ -78,41 +80,35 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) if (!enabled) return; - foreach (Envelope e in featureState.ProcessEvent(featureStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(featureStartedEvent, featureName); } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; + var featureProcessor = featureProcessorsByFeatureName[featureName]; - lock (featureState) + lock (featureProcessor) { // Remove the worker thread marker for this thread - featureState.workerThreadMarkers.TryPop(out int result); + featureProcessor.workerThreadMarkers.TryPop(out int result); // Check if there are other threads still working on this feature - if (featureState.workerThreadMarkers.TryPeek(out result)) + if (featureProcessor.workerThreadMarkers.TryPeek(out result)) { // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet return; } - featureState.Finished = true; + featureProcessor.Finished = true; } - if (!featureState.Enabled) + if (!featureProcessor.Enabled) return; - foreach (Envelope e in featureState.ProcessEvent(featureFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(featureFinishedEvent, featureName); - foreach (var message in featureState.Messages) + foreach (var message in featureProcessor.Messages) { broker.Publish(message); } @@ -123,43 +119,37 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - - foreach (Envelope e in featureState.ProcessEvent(scenarioStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(scenarioStartedEvent, featureName); } + private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(scenarioFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(scenarioFinishedEvent, featureName); } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(stepStartedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(stepStartedEvent, featureName); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureState = featureStatesByFeatureName[featureName]; - foreach (Envelope e in featureState.ProcessEvent(stepFinishedEvent)) - { - featureState.Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = featureName, Envelope = e }); - } + ProcessEvent(stepFinishedEvent, featureName); } + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) + { + var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookBindingEvent, featureName); + } + private void ProcessEvent(ExecutionEvent anEvent, string featureName) + { + var featureProcessor = featureProcessorsByFeatureName[featureName]; + featureProcessor.ProcessEvent(anEvent); + } } } diff --git a/Reqnroll/CucumberMesssages/FeatureState.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs similarity index 53% rename from Reqnroll/CucumberMesssages/FeatureState.cs rename to Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 7357d780a..e30faf089 100644 --- a/Reqnroll/CucumberMesssages/FeatureState.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -12,7 +12,7 @@ namespace Reqnroll.CucumberMesssages { - public class FeatureState + public class FeatureEventProcessor { public string Name { get; set; } public bool Enabled { get; set; } //This will be false if the feature could not be pickled @@ -26,24 +26,56 @@ public bool Success { get { - return Enabled && Finished && ScenarioName2StateMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK) ; + return Enabled && Finished && ScenarioName2ScenarioProcessorMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); } } - //ID Generator to use when generating IDs for TestCase messages and beyond + // ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID - // otherwise we'll use a GUID ID generator + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } //Lookup tables - public Dictionary StepDefinitionsByPattern = new(); - public Dictionary PicklesByScenarioName = new(); - - public Dictionary ScenarioName2StateMap = new(); - + // + // These three dictionaries hold the mapping of steps, hooks, and pickles to their IDs + // These should only be produced by the first FeatureStartedEvent that this FeatureEventProcessor receives (it might receive multiple if the scenario is run concurrently) + // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once + public ConcurrentDictionary StepDefinitionsByPattern = new(); + public ConcurrentDictionary HookDefinitionsByPattern = new(); + public ConcurrentDictionary PicklesByScenarioName = new(); + + // Scenario event processors by scenario name; + public Dictionary ScenarioName2ScenarioProcessorMap = new(); + + // The list of Cucumber Messages that are ready to be sent to the broker for distribution to consumers public ConcurrentQueue Messages = new(); + + // A set of markers that represent the worker threads that are currently processing events for this feature. + // Once the last worker thread marker is removed, the Messages are then sent to the broker public ConcurrentStack workerThreadMarkers = new(); + internal void ProcessEvent(ExecutionEvent anEvent) + { + foreach (Envelope e in DispatchEvent(anEvent)) + { + Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = Name, Envelope = e }); + } + } + private IEnumerable DispatchEvent(ExecutionEvent anEvent) + { + return anEvent switch + { + FeatureStartedEvent featureStartedEvent => ProcessEvent(featureStartedEvent), + FeatureFinishedEvent featureFinishedEvent => ProcessEvent(featureFinishedEvent), + ScenarioStartedEvent scenarioStartedEvent => ProcessEvent(scenarioStartedEvent), + ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), + StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), + StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), + HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + _ => throw new NotImplementedException(), + }; + } + internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { yield return Envelope.Create(new Meta( @@ -72,7 +104,7 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv foreach (var pickle in pickles) { - PicklesByScenarioName.Add(pickle.Name, pickle); + PicklesByScenarioName.TryAdd(pickle.Name, pickle); yield return Envelope.Create(pickle); } @@ -83,17 +115,24 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + HookDefinitionsByPattern.TryAdd(hookId, hook.Id); + yield return Envelope.Create(hook); + } } yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); } - internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) { yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); @@ -102,26 +141,21 @@ internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinished internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = new ScenarioState(scenarioStartedEvent.ScenarioContext, this); - ScenarioName2StateMap.Add(scenarioName, scenarioState); + var scenarioEP = new ScenarioEventProcessor(scenarioStartedEvent.ScenarioContext, this); + ScenarioName2ScenarioProcessorMap.Add(scenarioName, scenarioEP); - foreach (var e in scenarioState.ProcessEvent(scenarioStartedEvent)) + foreach (var e in scenarioEP.ProcessEvent(scenarioStartedEvent)) { yield return e; } } - private string ExtractLastID(List pickles) - { - return pickles.Last().Id; - } - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(scenarioFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(scenarioFinishedEvent)) { yield return e; } @@ -130,9 +164,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(stepStartedEvent)) + foreach (var e in scenarioEP.ProcessEvent(stepStartedEvent)) { yield return e; } @@ -141,12 +175,30 @@ internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioState = ScenarioName2StateMap[scenarioName]; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioState.ProcessEvent(stepFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(stepFinishedEvent)) { yield return e; } } + + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + { + var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + { + yield return e; + } + + } + + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs new file mode 100644 index 000000000..a2383daf6 --- /dev/null +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -0,0 +1,24 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMesssages +{ + public class HookStepProcessor : StepProcessorBase + { + public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } + public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + { + } + + public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + { + HookBindingFinishedEvent = hookFinishedEvent; + TestStepID = parentScenario.IdGenerator.GetNewId(); + return Enumerable.Empty(); + } + } + + +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMesssages/IStepProcessor.cs new file mode 100644 index 000000000..667d02467 --- /dev/null +++ b/Reqnroll/CucumberMesssages/IStepProcessor.cs @@ -0,0 +1,10 @@ +namespace Reqnroll.CucumberMesssages +{ + public interface IStepProcessor + { + public string TestStepID { get; set; } + + } + + +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/StepState.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs similarity index 74% rename from Reqnroll/CucumberMesssages/StepState.cs rename to Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 96a5360da..1ae523a10 100644 --- a/Reqnroll/CucumberMesssages/StepState.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -2,7 +2,6 @@ using Reqnroll.Assist; using Reqnroll.Bindings; using Reqnroll.Events; -using System; using System.Collections.Generic; using System.Linq; @@ -14,20 +13,14 @@ public class StepArgument public string Type; } - public class StepState + public class PickleStepProcessor : StepProcessorBase { - private ScenarioState scenarioState; private StepStartedEvent stepStartedEvent; - public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStartedEvent) + public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { - scenarioState = parentScenarioState; - this.stepStartedEvent = stepStartedEvent; - } - public string TestStepID { get; set; } - public string TestCaseStartedID => scenarioState.TestCaseStartedID; public string PickleStepID { get; set; } public bool Bound { get; set; } public string CanonicalizedStepPattern { get; set; } @@ -35,25 +28,22 @@ public StepState(ScenarioState parentScenarioState, StepStartedEvent stepStarted public IStepDefinitionBinding StepDefinition { get; set; } public StepArgument[] StepArguments { get; set; } - public TimeSpan Duration { get; set; } - public ScenarioExecutionStatus Status { get; set; } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { - TestStepID = scenarioState.IdGenerator.GetNewId(); + this.stepStartedEvent = stepStartedEvent; + TestStepID = parentScenario.IdGenerator.GetNewId(); return Enumerable.Empty(); } private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) { - return scenarioState.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + return parentScenario.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; } private string FindPickleStepIDByStepText(string stepText) { - return scenarioState.FeatureState.PicklesByScenarioName[scenarioState.Name].Steps.Where(st => st.Text == stepText).First().Id; + return parentScenario.FeatureState.PicklesByScenarioName[parentScenario.Name].Steps.Where(st => st.Text == stepText).First().Id; } internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) @@ -81,4 +71,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + + } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioState.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs similarity index 60% rename from Reqnroll/CucumberMesssages/ScenarioState.cs rename to Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index bd0189f26..cd6a4c54b 100644 --- a/Reqnroll/CucumberMesssages/ScenarioState.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -9,10 +9,10 @@ namespace Reqnroll.CucumberMesssages { - public class ScenarioState + public class ScenarioEventProcessor { internal readonly IIdGenerator IdGenerator; - internal readonly FeatureState FeatureState; + internal readonly FeatureEventProcessor FeatureState; public string TestCaseStartedID; public string Name { get; set; } @@ -23,17 +23,17 @@ public class ScenarioState // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); - public Dictionary StepsByEvent { get; set; } = new(); - public List Steps + public Dictionary StepsByEvent { get; private set; } = new(); + public List Steps { get { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Select(kvp => kvp.Value).ToList(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value ).ToList(); } } public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } - public ScenarioState(IScenarioContext context, FeatureState featureState) + public ScenarioEventProcessor(IScenarioContext context, FeatureEventProcessor featureState) { IdGenerator = featureState.IDGenerator; FeatureState = featureState; @@ -51,11 +51,24 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + _events.Enqueue(hookBindingFinishedEvent); + var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingFinishedEvent); + StepsByEvent.Add(hookBindingFinishedEvent, step); + return Enumerable.Empty(); + } + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); - var stepState = new StepState(this, stepStartedEvent); + var stepState = new PickleStepProcessor(this); StepsByEvent.Add(stepStartedEvent, stepState); stepState.ProcessEvent(stepStartedEvent); @@ -65,13 +78,18 @@ internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { _events.Enqueue(stepFinishedEvent); - var stepState = StepsByEvent.Values.Last(); + var stepState = FindMatchingStepStartEvent(stepFinishedEvent); stepState.ProcessEvent(stepFinishedEvent); StepsByEvent.Add(stepFinishedEvent, stepState); return Enumerable.Empty(); } + private PickleStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as PickleStepProcessor).LastOrDefault(); + } + internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); @@ -92,11 +110,16 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState, stepStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as PickleStepProcessor, stepStartedEvent)); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState, stepFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as PickleStepProcessor, stepFinishedEvent)); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + var hookStepState = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); break; // add more cases for other event types default: diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs new file mode 100644 index 000000000..b348987ed --- /dev/null +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Reqnroll.CucumberMesssages +{ + public class StepProcessorBase : IStepProcessor + { + public string TestStepID { get; set; } + public string TestCaseStartedID => parentScenario.TestCaseStartedID; + public ScenarioExecutionStatus Status { get; set; } + public TimeSpan Duration { get; set; } + + + + public ScenarioEventProcessor parentScenario; + + public StepProcessorBase(ScenarioEventProcessor parentScenario) + { + this.parentScenario = parentScenario; + } + } + + +} \ No newline at end of file diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index cc6ab3742..60944380d 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -1,5 +1,6 @@ using System; using Reqnroll.Bindings; +using Reqnroll.Infrastructure; namespace Reqnroll.Events { @@ -182,12 +183,18 @@ public class HookBindingFinishedEvent : ExecutionEvent public IHookBinding HookBinding { get; } public TimeSpan Duration { get; } + public IContextManager ContextManager { get; private set; } public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { HookBinding = hookBinding; Duration = duration; } + + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager) : this(hookBinding, duration) + { + ContextManager = contextManager; + } } public interface IExecutionOutputEvent diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index c0af86828..f4996f6b5 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -362,7 +362,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager)); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 117f2cccd..7f2497446 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -32,10 +32,15 @@ public void AStepFails() // Hook implementations [BeforeScenario] + public void BeforeScenarioHook() { } + + [BeforeScenario()] + public void NamedBeforeHook() { } + [AfterScenario] - public void ScenarioHook() { } + public void AfterScenarioHook() { } - [AfterScenario(new string[] { "some-tag", "some-other-tag" })] + [AfterScenario("some-tag or some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); From 5b2940cf24016ad7fc5992294347d8a9c6a04625 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:57:56 -0500 Subject: [PATCH 109/218] Fixed how Undefined steps are handled in the StepEventProcessor and in how they are rendered as TestStep messages. --- .../CucumberMessageFactory.cs | 15 ++++++---- .../CucumberMessagePublisher.cs | 2 ++ .../CucumberMesssages/PickleStepProcessor.cs | 30 ++++++++++--------- .../CucumberCompatibilityTests.cs | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 1358dc745..524f036e2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -85,7 +85,7 @@ private static SourceReference ToSourceRef(IBinding binding) internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) { - //TODO: This only works if the step is properly bound. Need to determine what to do otherwise + bool bound = stepState.StepDefinitionId != null; var args = stepState.StepArguments .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) @@ -95,9 +95,9 @@ internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, Pickle null, stepState.TestStepID, stepState.PickleStepID, - new List { stepState.StepDefinitionId }, - new List { new StepMatchArgumentsList(args) } - ); + bound ? new List { stepState.StepDefinitionId } : new List(), + bound ? new List { new StepMatchArgumentsList(args) } : new List() + ); return result; } @@ -149,7 +149,12 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; - return new TestStep(hookId, hookStepState.TestStepID, null, new List(), new List()); + return new TestStep( + hookId, + hookStepState.TestStepID, + null, + new List(), + new List()); } internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 6915f60d1..b17a13702 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -15,6 +15,7 @@ using System.Reflection; using System.Collections.Concurrent; using System.Linq; +using System.Diagnostics; namespace Reqnroll.CucumberMesssages { @@ -30,6 +31,7 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + //Debugger.Launch(); runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 1ae523a10..57cb04674 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -27,7 +27,7 @@ public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(pa public string StepDefinitionId { get; private set; } public IStepDefinitionBinding StepDefinition { get; set; } - public StepArgument[] StepArguments { get; set; } + public List StepArguments { get; set; } internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { @@ -48,29 +48,31 @@ private string FindPickleStepIDByStepText(string stepText) internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) { - StepDefinition = stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding; - Bound = !(StepDefinition == null || StepDefinition == BindingMatch.NonMatching); + var bindingMatch = stepFinishedEvent.StepContext?.StepInfo?.BindingMatch; + Bound = !(bindingMatch == null || bindingMatch == BindingMatch.NonMatching); - if (Bound) - { - CanonicalizedStepPattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition); - StepDefinitionId = FindStepDefIDByStepPattern(CanonicalizedStepPattern); + StepDefinition = Bound ? bindingMatch.StepBinding : null; + CanonicalizedStepPattern = Bound ? CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition) : ""; + StepDefinitionId = Bound ? FindStepDefIDByStepPattern(CanonicalizedStepPattern) : null; - PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); + PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); - Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; - Status = stepFinishedEvent.StepContext.Status; + Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; + Status = stepFinishedEvent.StepContext.Status; - StepArguments = stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument + StepArguments = Bound ? + stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument { Value = arg.ToString(), Type = arg.GetType().Name - }).ToArray(); - } + }).ToList() + : Enumerable.Empty().ToList(); + + return Enumerable.Empty(); } } - + } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 29804c65c..0a26ec531 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -37,7 +37,7 @@ Given I have a passing step [DataRow("undefined")] [DataRow("unknown-parameter-type")] [DataRow("rules")] - public void CCKPassingScenarios(string scenarioName) + public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); From 5b6eb3c2166110e80777e6cd8fdb1ea6783109a9 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:42:39 -0500 Subject: [PATCH 110/218] Moved NDJSON serialization into Reqnroll from the File Plugin making it centrally available. NdJson files in CCK tests were moved out of assembly resource to be plain project content files. --- .../CucumberMessagEnumConverter.cs | 0 .../CucumberMesssages}/NdjsonSerializer.cs | 0 .../CucumberCompatibilityTestBase.cs | 6 +++ .../CucumberCompatibilityTests.cs | 13 ++++- ...CucumberMessages.CompatibilityTests.csproj | 48 ++++++++++++++----- 5 files changed, 54 insertions(+), 13 deletions(-) rename {Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Reqnroll/CucumberMesssages}/CucumberMessagEnumConverter.cs (100%) rename {Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin => Reqnroll/CucumberMesssages}/NdjsonSerializer.cs (100%) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs similarity index 100% rename from Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs similarity index 100% rename from Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/NdjsonSerializer.cs rename to Reqnroll/CucumberMesssages/NdjsonSerializer.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 1f2e26629..b40613b5c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -46,5 +46,11 @@ protected IEnumerable GetTestBinaryFileNames(string scenarioName, string .Where(rn => !rn.EndsWith(".feature") && !rn.EndsWith(".cs") && !rn.EndsWith(".feature.ndjson") && rn.StartsWith(prefixToRemove)) .Select(rn => rn.Substring(prefixToRemove.Length)); } + + protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix, Assembly? assembly) + { + var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); + return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 0a26ec531..d1bda7c50 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,10 +1,13 @@ +using Cucumber.Messages; +using Io.Cucumber.Messages.Types; +using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using System.Reflection; namespace CucumberMessages.CompatibilityTests { [TestClass] - public class CucumberCompatibilitySmokeTest : CucumberCompatibilityTestBase + public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { [TestMethod] public void SmokeTest() @@ -55,5 +58,13 @@ public void CCKScenarios(string scenarioName) ShouldAllScenariosPass(); } } + + private IEnumerable GetExpectedResults(string scenarioName) + { + var workingDirectory = Assembly.GetExecutingAssembly().GetAssemblyLocation(); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory, $"{scenarioName}.feature.ndjson")); + + foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); + } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index ee82ca5de..25827ab34 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -58,44 +58,68 @@ - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + - + + Always + From 6b7bf9fc54662a8ea00dcba8abe6c213169d5b78 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:09:47 -0500 Subject: [PATCH 111/218] Configuration of File output driven by a json configuration file. --- .../CucumberMessages-config-schema.json | 34 +++++++++++ .../FileSinkConfiguration.cs | 35 +++++++++++ .../FileSinkPlugin.cs | 59 +++++++++++++------ .../CucumberCompatibilityTestBase.cs | 6 ++ .../CucumberCompatibilityTests.cs | 2 + ...CucumberMessages.CompatibilityTests.csproj | 8 ++- .../CucumberMessages.configuration.json | 10 ++++ 7 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json create mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json new file mode 100644 index 000000000..f7fcd8f17 --- /dev/null +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "filSinkEnabled": { + "type": "boolean", + "description": "Indicates whether the program is enabled or not" + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether this destination setting is enabled" + }, + "basePath": { + "type": "string", + "description": "The base path for output files" + }, + "outputDirectory": { + "type": "string", + "description": "The directory path where output should go" + } + }, + "required": [ "enabled", "basePath", "outputDirectory" ] + }, + "minItems": 1, + "description": "Array of destination settings" + } + }, + "required": [ "programEnabled", "destinations" ] +} \ No newline at end of file diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs new file mode 100644 index 000000000..7a56f4233 --- /dev/null +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 7a6165a15..8807fde2a 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -9,32 +9,39 @@ using System.Diagnostics; using Reqnroll.Events; using System.Collections.Concurrent; +using System.Text.Json; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - //TODO: Add support for Reqnroll Configuration to initialize the FileSinkPlugin by specifying the path to the base directory. - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; //Thread safe collections to hold: // 1. Inbound Cucumber Messages - BlockingCollection // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - + private object _lock = new(); private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); + private FileSinkConfiguration? configuration; + private string baseDirectory = ""; public FileSinkPlugin() { - //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + return; + + baseDirectory = ProcessConfiguration(configuration); + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => @@ -45,6 +52,30 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar }; } + private string ProcessConfiguration(FileSinkConfiguration configuration) + { + var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeDestination != null) + { + var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); + if (!Directory.Exists(basePath)) + { + lock(_lock) + { + if (!Directory.Exists(basePath)) + Directory.CreateDirectory(basePath); + } + } + + return basePath; + } + else + { + return Assembly.GetExecutingAssembly().Location; + } + } + private void CloseFileSink(TestRunFinishedEvent @event) { postedMessages.CompleteAdding(); @@ -54,13 +85,6 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - Console.WriteLine("LaunchFileSink called"); - - if (!Directory.Exists(baseDirectory)) - { - Directory.CreateDirectory(baseDirectory); - } - fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } @@ -71,17 +95,13 @@ public void Publish(ReqnrollCucumberMessage message) private async Task ConsumeAndWriteToFiles() { - Console.WriteLine("ConsumeAndWriteToFiles called"); - foreach (var message in postedMessages.GetConsumingEnumerable()) { var featureName = message.CucumberMessageSource; - Console.WriteLine("ConsumeAndWriteToFiles: " + featureName); if (message.Envelope != null) { var cm = Serialize(message.Envelope); - Console.WriteLine("ConsumeAndWriteToFiles: " + cm); await Write(featureName, cm); } else @@ -91,7 +111,6 @@ private async Task ConsumeAndWriteToFiles() } } - private string baseDirectory = Path.Combine("C:\\Users\\clrud\\source\\repos\\scratch", "CucumberMessages"); private bool disposedValue; private string Serialize(Envelope message) @@ -103,7 +122,13 @@ private async Task Write(string featureName, string cucumberMessage) if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + lock (_lock) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + } } await fileStreams[featureName].WriteLineAsync(cucumberMessage); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index b40613b5c..0b3f95052 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -52,5 +52,11 @@ protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); } + + protected void CucumberMessagesAddConfigurationFile(string configFileName) + { + var configFileContent = File.ReadAllText(configFileName); + _projectsDriver.AddFile(configFileName, configFileContent); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d1bda7c50..ea9a43376 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -13,6 +13,7 @@ public class CucumberCompatibilityTests : CucumberCompatibilityTestBase public void SmokeTest() { AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" Feature: Cucumber Messages Smoke Test @@ -43,6 +44,7 @@ Given I have a passing step public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); scenarioName = scenarioName.Replace("-", "_"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 25827ab34..73668f893 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -142,4 +142,10 @@ + + + Always + + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json new file mode 100644 index 000000000..cb6aa6ecb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -0,0 +1,10 @@ +{ + "fileSinkEnabled": true, + "destinations": [ + { + "enabled": true, + "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", + "outputDirectory": "CucumberMessages" + } + ] +} \ No newline at end of file From 494684f5896c305e48091a03f90c8a21dc9d2b01 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:19:12 -0500 Subject: [PATCH 112/218] Eliminated some tracing that is no longer useful for debugging. Clean up of source files. --- Reqnroll/CucumberMesssages/CucumberMessageBroker.cs | 2 -- Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs | 6 +----- Reqnroll/CucumberMesssages/PickleStepProcessor.cs | 4 ---- Reqnroll/CucumberMesssages/StepProcessorBase.cs | 4 ---- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 54411bd83..777cdee28 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -33,7 +33,6 @@ public void Publish(ReqnrollCucumberMessage message) //TODO: find a way to populate this list a single time var registeredSinks = _objectContainer.ResolveAll().ToList(); - _traceListener.WriteTestOutput("Broker publishing to " + registeredSinks.Count + " sinks"); foreach (var sink in registeredSinks) { @@ -49,7 +48,6 @@ public void Complete(string cucumberMessageSource) var registeredSinks = _objectContainer.ResolveAll().ToList(); var _traceListener = _objectContainer.Resolve(); - _traceListener.WriteTestOutput("Broker completing publishing to " + registeredSinks.Count + " sinks"); var completionMessage = new ReqnrollCucumberMessage { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 524f036e2..03018637c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -192,7 +192,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat } - // utility methods + #region utility methods public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) { string signature = GenerateSignature(stepDefinition); @@ -210,6 +210,6 @@ private static string GenerateSignature(IBinding stepDefinition) { return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; } - + #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index b17a13702..1d335b97a 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -42,10 +42,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - - var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput("HookIntoTestThreadExecutionEventPublisher"); - testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); @@ -77,7 +73,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) } var traceListener = objectContainer.Resolve(); - traceListener.WriteTestOutput($"FeatureStartedEventHandler: {featureName}"); + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); if (!enabled) return; diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs index 57cb04674..cfc86abfa 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/PickleStepProcessor.cs @@ -68,11 +68,7 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) }).ToList() : Enumerable.Empty().ToList(); - return Enumerable.Empty(); } - } - - } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index b348987ed..090b5055d 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -9,8 +9,6 @@ public class StepProcessorBase : IStepProcessor public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } - - public ScenarioEventProcessor parentScenario; public StepProcessorBase(ScenarioEventProcessor parentScenario) @@ -18,6 +16,4 @@ public StepProcessorBase(ScenarioEventProcessor parentScenario) this.parentScenario = parentScenario; } } - - } \ No newline at end of file From b86ee1b84714a36201a402f629ecc2875a970e90 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:57:39 -0500 Subject: [PATCH 113/218] Upgrade CucumberMessages nuget package to 26.0.0 --- .../Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- Reqnroll/Reqnroll.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 7dbb46029..9624408fd 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,7 +10,7 @@ - + diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 583dd298c..d38ef61af 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,7 @@ - + From e934f8fa3bfe08dbdf88371eb020d63246207cbf Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:48:22 -0500 Subject: [PATCH 114/218] Added FileExtension to MIMEType map for the attachment Added handling logic. --- .../FileExtensionToMIMETypeMap.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs new file mode 100644 index 000000000..c4deddb92 --- /dev/null +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public static class FileExtensionToMIMETypeMap + { + public static string GetMimeType(string extension) + { + if (ExtensionToMimeType.TryGetValue(extension, out var mimeType)) + return mimeType; + + return "application/octet-stream"; + } + + // Source of this list: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + public static readonly Dictionary ExtensionToMimeType = new Dictionary + { + {".aac", "audio/aac"}, + {".abw", "application/x-abiword"}, + {".apng", "image/apng"}, + {".arc", "application/x-freearc"}, + {".avif", "image/avif"}, + {".avi", "video/x-msvideo"}, + {".azw", "application/vnd.amazon.ebook"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".bz", "application/x-bzip"}, + {".bz2", "application/x-bzip2"}, + {".cda", "application/x-cdf"}, + {".csh", "application/x-csh"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".doc", "application/msword"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".eot", "application/vnd.ms-fontobject"}, + {".epub", "application/epub+zip"}, + {".gz", "application/gzip"}, + {".gif", "image/gif"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".ico", "image/vnd.microsoft.icon"}, + {".ics", "text/calendar"}, + {".jar", "application/java-archive"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "text/javascript"}, + {".json", "application/json"}, + {".jsonld", "application/ld+json"}, + {".mid", "audio/midi"}, + {".midi", "audio/midi"}, + {".mjs", "text/javascript"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mpeg", "video/mpeg"}, + {".mpkg", "application/vnd.apple.installer+xml"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".opus", "audio/ogg"}, + {".otf", "font/otf"}, + {".png", "image/png"}, + {".pdf", "application/pdf"}, + {".php", "application/x-httpd-php"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".rar", "application/vnd.rar"}, + {".rtf", "application/rtf"}, + {".sh", "application/x-sh"}, + {".svg", "image/svg+xml"}, + {".tar", "application/x-tar"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".ts", "video/mp2t"}, + {".ttf", "font/ttf"}, + {".txt", "text/plain"}, + {".vsd", "application/vnd.visio"}, + {".wav", "audio/wav"}, + {".weba", "audio/webm"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, + {".woff", "font/woff"}, + {".woff2", "font/woff2"}, + {".xhtml", "application/xhtml+xml"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xml", "application/xml"}, + {".xul", "application/vnd.mozilla.xul+xml"}, + {".zip", "application/zip"}, + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".7z", "application/x-7z-compressed"} + }; + } +} From c1a42e149e93c89df68bcacd037707678e49e34d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:18:59 -0500 Subject: [PATCH 115/218] AddAttachment and OutputHelper.WriteLine added with Message output as an 'Attachment'. --- .../AttachmentAddedEventWrapper.cs | 18 ++++ .../CucumberMessageFactory.cs | 58 +++++++++--- .../CucumberMessagePublisher.cs | 17 +++- .../CucumberMessageTransformer.cs | 4 +- .../FeatureEventProcessor.cs | 26 ++++++ .../FileExtensionToMIMETypeMap.cs | 2 +- .../CucumberMesssages/HookStepProcessor.cs | 9 +- .../OutputAddedEventWrapper.cs | 18 ++++ .../ScenarioEventProcessor.cs | 90 ++++++++++++++++--- ...pProcessor.cs => ScenarioStepProcessor.cs} | 4 +- Reqnroll/Events/ExecutionEvent.cs | 20 +++++ .../Infrastructure/ReqnrollOutputHelper.cs | 15 +++- .../CCK/attachments/attachments.cs | 36 +++++++- .../CCK/attachments/attachments.feature | 10 +++ .../CucumberCompatibilityTests.cs | 23 +++-- ...CucumberMessages.CompatibilityTests.csproj | 12 +-- .../ReqnrollOutputHelperTests.cs | 3 +- 17 files changed, 311 insertions(+), 54 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs create mode 100644 Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs rename Reqnroll/CucumberMesssages/{PickleStepProcessor.cs => ScenarioStepProcessor.cs} (94%) diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs new file mode 100644 index 000000000..25c2eb53c --- /dev/null +++ b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs @@ -0,0 +1,18 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + internal class AttachmentAddedEventWrapper : ExecutionEvent + { + public AttachmentAddedEventWrapper(AttachmentAddedEvent attachmentAddedEvent, string pickleStepId) + { + AttachmentAddedEvent = attachmentAddedEvent; + PickleStepID = pickleStepId; + } + + public AttachmentAddedEvent AttachmentAddedEvent { get; } + public string PickleStepID { get; } + public string TestCaseStartedID { get; set; } + public string TestCaseStepID { get; set; } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 03018637c..0402af6ad 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -5,7 +5,10 @@ using Reqnroll.Events; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; namespace Reqnroll.CucumberMesssages { @@ -28,12 +31,12 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar { switch (stepState) { - case PickleStepProcessor _: - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as PickleStepProcessor); + case ScenarioStepProcessor _: + var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as ScenarioStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: - var hookTestStep = CucumberMessageFactory.ToHookTestStep( stepState as HookStepProcessor); + var hookTestStep = CucumberMessageFactory.ToHookTestStep(stepState as HookStepProcessor); testSteps.Add(hookTestStep); break; default: @@ -83,7 +86,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, PickleStepProcessor stepState) + internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { bool bound = stepState.StepDefinitionId != null; @@ -97,7 +100,7 @@ internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, Pickle stepState.PickleStepID, bound ? new List { stepState.StepDefinitionId } : new List(), bound ? new List { new StepMatchArgumentsList(args) } : new List() - ); + ); return result; } @@ -112,7 +115,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), argument.Type); } - internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -120,7 +123,7 @@ internal static TestStepStarted ToTestStepStarted(PickleStepProcessor stepState, Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(PickleStepProcessor stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(ScenarioStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, @@ -150,15 +153,15 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; return new TestStep( - hookId, - hookStepState.TestStepID, - null, - new List(), + hookId, + hookStepState.TestStepID, + null, + new List(), new List()); } - internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) { - return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingStartedEvent.Timestamp)); } internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) @@ -166,6 +169,30 @@ internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepPr return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); } + internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) + { + return new Attachment( + Base64EncodeFile(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), + AttachmentContentEncoding.BASE64, + Path.GetFileName(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), + FileExtensionToMIMETypeMap.GetMimeType(Path.GetExtension(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath)), + null, + attachmentAddedEventWrapper.TestCaseStartedID, + attachmentAddedEventWrapper.TestCaseStepID, + null); + } + internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, OutputAddedEventWrapper outputAddedEventWrapper) + { + return new Attachment( + outputAddedEventWrapper.OutputAddedEvent.Text, + AttachmentContentEncoding.IDENTITY, + null, + "text/x.cucumber.log+plain", + null, + outputAddedEventWrapper.TestCaseStartedID, + outputAddedEventWrapper.TestCaseStepID, + null); + } private static TestStepResult ToTestStepResult(StepProcessorBase stepState) { @@ -210,6 +237,11 @@ private static string GenerateSignature(IBinding stepDefinition) { return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; } + public static string Base64EncodeFile(string filePath) + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 1d335b97a..c4bea07ba 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -31,7 +31,6 @@ public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IO } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - //Debugger.Launch(); runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; @@ -49,8 +48,9 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); + testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); + testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } - private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; @@ -143,6 +143,19 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingEvent, featureName); } + + private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) + { + var featureName = attachmentAddedEvent.FeatureName; + ProcessEvent(attachmentAddedEvent, featureName); + } + + private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) + { + ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); + } + + private void ProcessEvent(ExecutionEvent anEvent, string featureName) { var featureProcessor = featureProcessorsByFeatureName[featureName]; diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index 17ccc9d7e..e6e8135a3 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -314,8 +314,8 @@ private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gher return null; } return new Io.Cucumber.Messages.Types.PickleDocString( - pickleDocString.Content, - pickleDocString.MediaType + pickleDocString.MediaType, + pickleDocString.Content ); } diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index e30faf089..30da3786d 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Reqnroll.CucumberMesssages @@ -42,8 +43,11 @@ public bool Success // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once public ConcurrentDictionary StepDefinitionsByPattern = new(); public ConcurrentDictionary HookDefinitionsByPattern = new(); + + //TODO: fix this; there will be multiple Pickles with the same scenario name when executing Example table rows public ConcurrentDictionary PicklesByScenarioName = new(); + //TODO: Fix this for thread-safety; there will be multiple active Scenarios with the same name when executing Example table rows in parallel // Scenario event processors by scenario name; public Dictionary ScenarioName2ScenarioProcessorMap = new(); @@ -72,6 +76,8 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), + OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), }; } @@ -192,7 +198,27 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishe { yield return e; } + } + + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var scenarioName = attachmentAddedEvent.ScenarioName; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + foreach (var e in scenarioEP.ProcessEvent(attachmentAddedEvent)) + { + yield return e; + } + } + + internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) + { + var scenarioName = outputAddedEvent.ScenarioName; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + foreach (var e in scenarioEP.ProcessEvent(outputAddedEvent)) + { + yield return e; + } } private string ExtractLastID(List pickles) diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs index c4deddb92..29d96c701 100644 --- a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -8,7 +8,7 @@ public static class FileExtensionToMIMETypeMap { public static string GetMimeType(string extension) { - if (ExtensionToMimeType.TryGetValue(extension, out var mimeType)) + if (ExtensionToMimeType.TryGetValue(extension.ToLower(), out var mimeType)) return mimeType; return "application/octet-stream"; diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index a2383daf6..7fe99228a 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -7,15 +7,22 @@ namespace Reqnroll.CucumberMesssages { public class HookStepProcessor : StepProcessorBase { + public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { } + public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEvent) + { + TestStepID = parentScenario.IdGenerator.GetNewId(); + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(stepFinishedEvent.HookBinding); + return Enumerable.Empty(); + } + public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; - TestStepID = parentScenario.IdGenerator.GetNewId(); return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs new file mode 100644 index 000000000..bf05e99ae --- /dev/null +++ b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs @@ -0,0 +1,18 @@ +using Reqnroll.Events; + +namespace Reqnroll.CucumberMesssages +{ + internal class OutputAddedEventWrapper : ExecutionEvent + { + internal OutputAddedEvent OutputAddedEvent; + internal string PickleStepID; + internal string TestCaseStepID; + internal string TestCaseStartedID; + + public OutputAddedEventWrapper(OutputAddedEvent outputAddedEvent, string pickleStepId) + { + OutputAddedEvent = outputAddedEvent; + PickleStepID = pickleStepId; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index cd6a4c54b..ad4639c7e 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -4,11 +4,13 @@ using Reqnroll.Bindings; using Reqnroll.Events; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace Reqnroll.CucumberMesssages { + public class ScenarioEventProcessor { internal readonly IIdGenerator IdGenerator; @@ -19,6 +21,9 @@ public class ScenarioEventProcessor public string TestCaseID { get; set; } public string PickleID { get; set; } public Pickle Pickle { get; set; } + private TestCase TestCase; + + private TestCaseStarted TestCaseStarted; // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages private Queue _events = new(); @@ -51,24 +56,38 @@ internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStarted return Enumerable.Empty(); } - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) return Enumerable.Empty(); - _events.Enqueue(hookBindingFinishedEvent); + _events.Enqueue(hookBindingStartedEvent); var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingStartedEvent); + StepsByEvent.Add(hookBindingStartedEvent, step); + return Enumerable.Empty(); + } + + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + _events.Enqueue(hookBindingFinishedEvent); + var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); step.ProcessEvent(hookBindingFinishedEvent); StepsByEvent.Add(hookBindingFinishedEvent, step); return Enumerable.Empty(); } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); + } + internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) { _events.Enqueue(stepStartedEvent); - var stepState = new PickleStepProcessor(this); + var stepState = new ScenarioStepProcessor(this); StepsByEvent.Add(stepStartedEvent, stepState); stepState.ProcessEvent(stepStartedEvent); @@ -84,10 +103,31 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = matchingPickleStep.Id; + + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); + _events.Enqueue(attachmentExecutionEventWrapper); + + return Enumerable.Empty(); + } - private PickleStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as PickleStepProcessor).LastOrDefault(); + var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = matchingPickleStep.Id; + + var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); + _events.Enqueue(outputExecutionEventWrapper); + + return Enumerable.Empty(); + } + + private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) @@ -101,8 +141,10 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish switch (executionEvent) { case ScenarioStartedEvent scenarioStartedEvent: - yield return Envelope.Create(CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent)); + TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); + TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); + yield return Envelope.Create(TestCase); + yield return Envelope.Create(TestCaseStarted); break; case ScenarioFinishedEvent scenarioFinished: ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; @@ -110,16 +152,36 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as PickleStepProcessor, stepStartedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent)); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as PickleStepProcessor, stepFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); + break; + //TODO: this isn't right; shouuld be one hook processor per hook that ran + case HookBindingStartedEvent hookBindingStartedEvent: + var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent)); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepState = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepState as HookStepProcessor, hookBindingFinishedEvent)); + var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepFinishedState as HookStepProcessor, hookBindingFinishedEvent)); + break; + case AttachmentAddedEventWrapper attachmentAddedEventWrapper: + // find the TestCaseStepId and testCaseStartedId + var testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == attachmentAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + var testCaseStartedId = TestCaseStarted.Id; + attachmentAddedEventWrapper.TestCaseStepID = testStepID; + attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); + break; + case OutputAddedEventWrapper outputAddedEventWrapper: + // find the TestCaseStepId and testCaseStartedId + testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == outputAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + testCaseStartedId = TestCaseStarted.Id; + outputAddedEventWrapper.TestCaseStepID = testStepID; + outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); break; // add more cases for other event types default: diff --git a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs similarity index 94% rename from Reqnroll/CucumberMesssages/PickleStepProcessor.cs rename to Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index cfc86abfa..29c71c5a9 100644 --- a/Reqnroll/CucumberMesssages/PickleStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -13,11 +13,11 @@ public class StepArgument public string Type; } - public class PickleStepProcessor : StepProcessorBase + public class ScenarioStepProcessor : StepProcessorBase { private StepStartedEvent stepStartedEvent; - public PickleStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) { } diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index 60944380d..e2d83b446 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -203,20 +203,40 @@ public interface IExecutionOutputEvent public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string Text { get; } + public string FeatureName { get; } + public string ScenarioName { get; } + public string StepText { get; } public OutputAddedEvent(string text) { Text = text; } + + public OutputAddedEvent(string text, string featureName, string scenarioName, string stepText) : this(text) + { + FeatureName = featureName; + ScenarioName = scenarioName; + StepText = stepText; + } } public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } + public string FeatureName { get; } + public string ScenarioName { get; } + public string StepText { get; } public AttachmentAddedEvent(string filePath) { FilePath = filePath; } + + public AttachmentAddedEvent(string filePath, string featureName, string scenarioName, string stepText) : this(filePath) + { + FeatureName = featureName; + ScenarioName = scenarioName; + StepText = stepText; + } } } diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index accb95460..22bb9c82f 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -8,17 +8,23 @@ public class ReqnrollOutputHelper : IReqnrollOutputHelper private readonly ITestThreadExecutionEventPublisher _testThreadExecutionEventPublisher; private readonly ITraceListener _traceListener; private readonly IReqnrollAttachmentHandler _reqnrollAttachmentHandler; + private readonly IContextManager contextManager; - public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler) + public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler, IContextManager contextManager) { _testThreadExecutionEventPublisher = testThreadExecutionEventPublisher; _traceListener = traceListener; _reqnrollAttachmentHandler = reqnrollAttachmentHandler; + this.contextManager = contextManager; } public void WriteLine(string message) { - _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message)); + var featureName = contextManager.FeatureContext.FeatureInfo?.Title; + var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; + var stepText = contextManager.StepContext.StepInfo?.Text; + + _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); _traceListener.WriteToolOutput(message); } @@ -29,7 +35,10 @@ public void WriteLine(string format, params object[] args) public void AddAttachment(string filePath) { - _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath)); + var featureName = contextManager.FeatureContext.FeatureInfo?.Title; + var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; + var stepText = contextManager.StepContext.StepInfo?.Text; + _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); _reqnrollAttachmentHandler.AddAttachment(filePath); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs index 877a2594d..5ab856d0d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs @@ -10,10 +10,44 @@ namespace CucumberMessages.CompatibilityTests.CCK.attachments [Binding] internal class Attachments { + private readonly IReqnrollOutputHelper reqnrollOutputHelper; + + internal Attachments(IReqnrollOutputHelper reqnrollOutputHelper) + { + this.reqnrollOutputHelper = reqnrollOutputHelper; + } + + [When(@"the string {string} is logged")] + public void WhenLogText(string text) + { + reqnrollOutputHelper.WriteLine(text); + } + + [When(@"text with ANSI escapes is logged")] + public void WhenTextWithANSIEscapedIsLogged() + { + reqnrollOutputHelper.WriteLine("This displays a \x1b[31mr\x1b[0m\x1b[91ma\x1b[0m\x1b[33mi\x1b[0m\x1b[32mn\x1b[0m\x1b[34mb\x1b[0m\x1b[95mo\x1b[0m\x1b[35mw\x1b[0m"); + } + [When(@"a JPEG image is attached")] public void WhenAJPEGImageIsAttached() { - throw new NotImplementedException(); + var ext = "jpeg" ; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "attachments", $"cucumber.{ext}"); + + reqnrollOutputHelper.AddAttachment(attachment); + } + + [When(@"a PNG image is attached")] + public void WhenAPNGImageIsAttached() + { + var ext = "png"; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "attachments", $"cucumber.{ext}"); + + reqnrollOutputHelper.AddAttachment(attachment); } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature index 063960e9c..59f9c0987 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature @@ -9,6 +9,8 @@ Feature: Attachments Attachments must have a body and a content type + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Strings can be attached with a media type Beware that some formatters such as @cucumber/react use the media type to determine how to display an attachment. @@ -21,12 +23,16 @@ Feature: Attachments Scenario: Log ANSI coloured text When text with ANSI escapes is logged + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Log JSON When the following string is attached as "application/json": ``` {"message": "The big question", "foo": "bar"} ``` + # Reqnroll does not support outputing arbitrary strings as attachments (that have no filename) + @ignore Scenario: Byte arrays are base64-encoded regardless of media type When an array with 10 bytes is attached as "text/plain" @@ -44,8 +50,12 @@ Feature: Attachments | JPEG | | PNG | + # Reqnroll does not support attaching a file by name and rename in the same operation + @ignore Scenario: Attaching PDFs with a different filename When a PDF document is attached and renamed + # Reqnroll does not support attaching a URL + @ignore Scenario: Attaching URIs When a link to "https://cucumber.io" is attached diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index ea9a43376..93f22226f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -17,11 +17,14 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test - Scenario: Smoke Test - Given I have a passing step + Scenario: Log JSON + When the following string is attached as "application/json": + ``` + {"message": "The big question", "foo": "bar"} + ``` """); - AddPassingStepBinding("Given"); + AddPassingStepBinding("When"); ExecuteTests(); @@ -29,7 +32,7 @@ Given I have a passing step } [TestMethod] - //[DataRow("attachments")] + [DataRow("attachments")] [DataRow("minimal")] [DataRow("cdata")] [DataRow("pending")] @@ -45,6 +48,7 @@ public void CCKScenarios(string scenarioName) { AddCucumberMessagePlugIn(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + AddUtilClassWithFileSystemPath(); scenarioName = scenarioName.Replace("-", "_"); @@ -55,10 +59,13 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); ConfirmAllTestsRan(null); - if (scenarioName == "attachments") - { - ShouldAllScenariosPass(); - } + } + + private void AddUtilClassWithFileSystemPath() + { + string location = AppContext.BaseDirectory; + AddBindingClass( + $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\CCK\"; }} }} "); } private IEnumerable GetExpectedResults(string scenarioName) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 73668f893..b2d5b1598 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -27,9 +27,6 @@ - - - @@ -61,9 +58,6 @@ Always - - - @@ -143,6 +137,12 @@ + + Always + + + Always + Always diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 04d1c1552..8bbc7d653 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -37,8 +37,9 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() _testThreadExecutionEventPublisher = new Mock(); var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); + var contextManager = new Mock(); - return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object); + return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } } } From de40f1f5890e6b6e2011cd4912dc100087f2a84e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:03:13 -0500 Subject: [PATCH 116/218] Support for generating ParameterType Messages from StepArgumentTransformations. --- .../CucumberMessageFactory.cs | 20 ++++++++++++++++++ .../FeatureEventProcessor.cs | 16 ++++++++++++++ .../CCK/data-tables/data_tables.cs | 21 +++++++++++++++++-- .../CCK/parameter-types/parameter_types.cs | 2 +- ...CucumberMessages.CompatibilityTests.csproj | 2 +- 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 0402af6ad..64c989471 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -76,6 +76,26 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } + + internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) + { + var regex = stepTransform.Regex.ToString(); + var name = stepTransform.Name; + var result = new ParameterType + ( + name, + new List + { + regex + }, + true, + true, + iDGenerator.GetNewId(), + ToSourceRef(stepTransform) + ); + return result; + } + private static SourceReference ToSourceRef(IBinding binding) { var methodName = binding.Method.Name; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 30da3786d..2dcc355a5 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -117,6 +117,11 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); if (bindingRegistry.IsValid) { + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } foreach (var binding in bindingRegistry.GetStepDefinitions()) { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); @@ -144,6 +149,17 @@ internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinished yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); } + private string GenerateScenarioKey(ScenarioInfo scenarioInfo) + { + var scenarioArguments = new List(); + foreach (string v in scenarioInfo.Arguments.Values) + { + scenarioArguments.Add(v); + } + return scenarioInfo.Title + + scenarioArguments + + scenarioInfo.CombinedTags; + } internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs index 0e696ae16..5f6a435e6 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; @@ -27,6 +28,7 @@ public void ThenItShouldBe(Table expected) private void TablesEqual(Table expected, Table transposedTable) { + expected = MakeHeaderLess(expected); var ExpectednumRows = expected.Rows.Count; var ExpectednumCols = expected.Rows[0].Count; @@ -49,8 +51,9 @@ private void TablesEqual(Table expected, Table transposedTable) private Table Transpose(Table table) { + Table tempTable = MakeHeaderLess(table); - string[][] matrix = GetStringArray(table.Rows); + string[][] matrix = GetStringArray(tempTable.Rows); var t = TransposeMatrix(matrix); return CreateTable(t); @@ -96,7 +99,7 @@ static Table CreateTable(string[][] matrix) { var columnCount = matrix[0].Length; var headers = Enumerable.Range(0, columnCount).Select(i => $"").ToArray(); - var table = new Table(); + var table = new Table(headers); foreach (var row in matrix) { @@ -106,5 +109,19 @@ static Table CreateTable(string[][] matrix) return table; } } + + private static Table MakeHeaderLess(Table table) + { + // push the header into a new Table as the first row of that table + var header = table.Header; + var tempTable = new Table(Enumerable.Range(0, header.Count).Select(i => $"").ToArray()); + tempTable.AddRow(header.ToArray()); + foreach (var row in table.Rows) + { + tempTable.AddRow(row.Select(kvp => kvp.Value).ToArray()); + } + + return tempTable; + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs index faad8ebb5..27e151de4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs @@ -10,7 +10,7 @@ namespace CucumberMessages.CompatibilityTests.CCK.parameter_types [Binding] internal class Parameter_types { - [StepArgumentTransformation(@"([A-Z]{3})-([A-Z]{3})")] + [StepArgumentTransformation(Name ="flight", Regex = @"([A-Z]{3})-([A-Z]{3})")] public Flight FlightConverter(string from, string to) { return new Flight diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index b2d5b1598..6b52388e4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -63,11 +63,11 @@ Always + Always - From 5db57d578d8dff48cbfc30cc5c0e596960ed9e0f Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:03:59 -0500 Subject: [PATCH 117/218] Working except for the TestStepStarted/Finished for the hook that throws an exception. --- .../CucumberMessagePublisher.cs | 8 +++++ .../FeatureEventProcessor.cs | 12 ++++++++ .../ScenarioEventProcessor.cs | 29 ++++++++++++++----- Reqnroll/Events/ExecutionEvent.cs | 6 ++++ .../Infrastructure/ReqnrollOutputHelper.cs | 4 +-- .../Infrastructure/TestExecutionEngine.cs | 2 +- .../CCK/hooks/hooks.cs | 9 ++++-- ...CucumberMessages.CompatibilityTests.csproj | 5 ++-- 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index c4bea07ba..eabd2afdf 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -47,10 +47,12 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); testThreadEventPublisher.AddHandler(StepStartedEventHandler); testThreadEventPublisher.AddHandler(StepFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; @@ -138,6 +140,12 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) ProcessEvent(stepFinishedEvent, featureName); } + private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + { + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookBindingStartedEvent, featureName); + } + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) { var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 2dcc355a5..604d358d8 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -75,6 +75,7 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), + HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), @@ -205,6 +206,17 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedEvent) + { + var scenarioName = hookStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookStartedEvent)) + { + yield return e; + } + } + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index ad4639c7e..8fbb0aa2b 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Reqnroll.CucumberMesssages @@ -71,6 +72,12 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingS internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { + + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + _events.Enqueue(hookBindingFinishedEvent); var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); step.ProcessEvent(hookBindingFinishedEvent); @@ -105,8 +112,8 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { - var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = matchingPickleStep.Id; + //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); + var pickleStepId = ""; var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); _events.Enqueue(attachmentExecutionEventWrapper); @@ -116,9 +123,10 @@ internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAdded internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) { - var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = matchingPickleStep.Id; + //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); + //var pickleStepId = matchingPickleStep.Id; + var pickleStepId = ""; var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); _events.Enqueue(outputExecutionEventWrapper); @@ -133,6 +141,7 @@ private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepF internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); + TestStepStarted mostRecentTestStepStarted = null; while (_events.Count > 0) { @@ -152,7 +161,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case StepStartedEvent stepStartedEvent: var stepState = StepsByEvent[stepStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent)); + var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent); + mostRecentTestStepStarted = stepStarted; + yield return Envelope.Create(stepStarted); break; case StepFinishedEvent stepFinishedEvent: var stepFinishedState = StepsByEvent[stepFinishedEvent]; @@ -161,7 +172,9 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish //TODO: this isn't right; shouuld be one hook processor per hook that ran case HookBindingStartedEvent hookBindingStartedEvent: var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent)); + var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); + mostRecentTestStepStarted = hookStepStarted; + yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; @@ -169,7 +182,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId - var testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == attachmentAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + var testStepID = mostRecentTestStepStarted.TestStepId; var testCaseStartedId = TestCaseStarted.Id; attachmentAddedEventWrapper.TestCaseStepID = testStepID; attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; @@ -177,7 +190,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish break; case OutputAddedEventWrapper outputAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId - testStepID = TestCase.TestSteps.Where(ts => ts.PickleStepId == outputAddedEventWrapper.PickleStepID).FirstOrDefault().Id; + testStepID = mostRecentTestStepStarted.TestStepId; testCaseStartedId = TestCaseStarted.Id; outputAddedEventWrapper.TestCaseStepID = testStepID; outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e2d83b446..e9aabf778 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -171,11 +171,17 @@ public StepBindingFinishedEvent(IStepDefinitionBinding stepDefinitionBinding, Ti public class HookBindingStartedEvent : ExecutionEvent { public IHookBinding HookBinding { get; } + public IContextManager ContextManager { get; private set; } public HookBindingStartedEvent(IHookBinding hookBinding) { HookBinding = hookBinding; } + + public HookBindingStartedEvent(IHookBinding hookBinding, IContextManager contextManager) : this(hookBinding) + { + ContextManager = contextManager; + } } public class HookBindingFinishedEvent : ExecutionEvent diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index 22bb9c82f..604d08d88 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -22,7 +22,7 @@ public void WriteLine(string message) { var featureName = contextManager.FeatureContext.FeatureInfo?.Title; var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext.StepInfo?.Text; + var stepText = contextManager.StepContext?.StepInfo?.Text; _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); _traceListener.WriteToolOutput(message); @@ -37,7 +37,7 @@ public void AddAttachment(string filePath) { var featureName = contextManager.FeatureContext.FeatureInfo?.Title; var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext.StepInfo?.Text; + var stepText = contextManager.StepContext?.StepInfo?.Text; _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); _reqnrollAttachmentHandler.AddAttachment(filePath); } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index f4996f6b5..c1164a7ee 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -353,7 +353,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin var currentContainer = GetHookContainer(hookType); var arguments = ResolveArguments(hookBinding, currentContainer); - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); try diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 7f2497446..5f43c19c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Diagnostics; namespace CucumberMessages.CompatibilityTests.CCK.hooks { @@ -49,8 +50,12 @@ public void FailingAfterHook() [AfterScenario("with-attachment")] public void PassingAfterHook() { - reqnrollOutputHelper.AddAttachment("cucumber.svg"); - } + Debugger.Launch(); + var ext = "svg"; + var path = FileSystemPath.GetFilePathForAttachments(); + var attachment = Path.Combine(path, "hooks", $"cucumber.{ext}"); + reqnrollOutputHelper.AddAttachment(attachment); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 6b52388e4..c5c455a05 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -33,7 +33,6 @@ - @@ -73,7 +72,6 @@ Always - @@ -143,6 +141,9 @@ Always + + Always + Always From 6704d41bc090f924d4e1a9255223daba570f7163 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:52:34 -0500 Subject: [PATCH 118/218] Hardcode ParameterType Message properties of useForSnippets and preferForRegularExpressionMatch to false (as these aren't used by Reqnroll). --- Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 64c989471..8f40ed7f8 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -88,8 +88,8 @@ internal static ParameterType ToParameterType(IStepArgumentTransformationBinding { regex }, - true, - true, + false, + false, iDGenerator.GetNewId(), ToSourceRef(stepTransform) ); From 66c6654088502d3bebd41a1868dd81f6e7e049ca Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:04:08 -0500 Subject: [PATCH 119/218] First draft of obtaining Product infos from Analytics Provider. Needs CI and Git info. Needs to be refactored. --- .../FeatureEventProcessor.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 604d358d8..43fbb1ca7 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -4,12 +4,14 @@ using Reqnroll.Bindings; using Reqnroll.BoDi; using Reqnroll.Events; +using Reqnroll.Analytics; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; namespace Reqnroll.CucumberMesssages { @@ -85,12 +87,27 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { + //HACK: temporary implementation to obtain information for the Meta message + var analtyicsEventProcessor = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + var envInfo = analtyicsEventProcessor.CreateProjectRunningEvent(""); + var implementation = new Product("Reqnroll", envInfo.ReqnrollVersion); + var runTime = new Product("dotNet", envInfo.TargetFramework); + var os = new Product(envInfo.Platform, envInfo.PlatformDescription); + + var cpu = RuntimeInformation.ProcessArchitecture switch { + Architecture.Arm => new Product("arm", null), + Architecture.Arm64 => new Product("arm64", null), + Architecture.X86 => new Product("x86", null), + Architecture.X64 => new Product("x64", null), + _ => new Product(null, null), + }; + yield return Envelope.Create(new Meta( - Cucumber.Messages.ProtocolVersion.Version, - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), - new Product("placeholder", "placeholder"), + (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], + implementation, + runTime, + os, + cpu, null)); Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); From bb4eb4d854ed1174e642ff719bf850210f88225a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:07:23 -0500 Subject: [PATCH 120/218] Refactored Meta, CI and Git creation to the MessageFactory. Refactored OS and CI information gathering out of AnalyticsEventProvider to EnvironmentWrapper. --- Reqnroll/Analytics/AnalyticsEventProvider.cs | 94 ++----------------- .../CucumberMessageFactory.cs | 66 +++++++++++++ .../FeatureEventProcessor.cs | 23 +---- .../EnvironmentAccess/EnvironmentWrapper.cs | 81 ++++++++++++++++ .../EnvironmentAccess/IEnvironmentWrapper.cs | 10 ++ 5 files changed, 167 insertions(+), 107 deletions(-) diff --git a/Reqnroll/Analytics/AnalyticsEventProvider.cs b/Reqnroll/Analytics/AnalyticsEventProvider.cs index 90a446c3f..301835a22 100644 --- a/Reqnroll/Analytics/AnalyticsEventProvider.cs +++ b/Reqnroll/Analytics/AnalyticsEventProvider.cs @@ -27,11 +27,11 @@ public ReqnrollProjectCompilingEvent CreateProjectCompilingEvent(string msbuildV { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string buildServerName = GetBuildServerName(); - bool isDockerContainer = IsRunningInDockerContainer(); + string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); + string buildServerName = _environmentWrapper.GetBuildServerName(); + bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); string hashedAssemblyName = ToSha256(assemblyName); - string platform = GetOSPlatform(); + string platform = _environmentWrapper.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var compiledEvent = new ReqnrollProjectCompilingEvent( @@ -56,13 +56,13 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string targetFramework = GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; - bool isDockerContainer = IsRunningInDockerContainer(); - string buildServerName = GetBuildServerName(); + string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); + string targetFramework = _environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); + string buildServerName = _environmentWrapper.GetBuildServerName(); string hashedAssemblyName = ToSha256(testAssemblyName); - string platform = GetOSPlatform(); + string platform = _environmentWrapper.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var runningEvent = new ReqnrollProjectRunningEvent( @@ -80,70 +80,6 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly return runningEvent; } - private string GetOSPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Linux"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "OSX"; - } - - throw new InvalidOperationException("Platform cannot be identified"); - } - - private readonly Dictionary buildServerTypes - = new Dictionary { - { "TF_BUILD","Azure Pipelines"}, - { "TEAMCITY_VERSION","TeamCity"}, - { "JENKINS_HOME","Jenkins"}, - { "GITHUB_ACTIONS","GitHub Actions"}, - { "GITLAB_CI","GitLab CI/CD"}, - { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, - { "TRAVIS","Travis CI"}, - { "APPVEYOR","AppVeyor"}, - { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, - { "bamboo_agentId", "Atlassian Bamboo" }, - { "CIRCLECI", "CircleCI" }, - { "GO_PIPELINE_NAME", "GoCD" }, - { "BUDDY", "Buddy" }, - { "NEVERCODE", "Nevercode" }, - { "SEMAPHORE", "SEMAPHORE" }, - { "BROWSERSTACK_USERNAME", "BrowserStack" }, - { "CF_BUILD_ID", "Codefresh" }, - { "TentacleVersion", "Octopus Deploy" }, - - { "CI_NAME", "CodeShip" } - }; - - private string GetBuildServerName() - { - foreach (var buildServerType in buildServerTypes) - { - var envVariable = _environmentWrapper.GetEnvironmentVariable(buildServerType.Key); - if (envVariable is ISuccess) - return buildServerType.Value; - } - return null; - } - - private bool IsRunningInDockerContainer() - { - return _environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; - } - - private string GetReqnrollVersion() - { - return VersionInfo.AssemblyInformationalVersion; - } private string ToSha256(string inputString) { @@ -163,17 +99,5 @@ private string ToSha256(string inputString) return stringBuilder.ToString(); } - private string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - - return null; - } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 8f40ed7f8..9a4cf7c40 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -1,12 +1,16 @@ using Cucumber.Messages; using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; +using Reqnroll.Analytics; using Reqnroll.Bindings; +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; using Reqnroll.Events; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; @@ -262,6 +266,68 @@ public static string Base64EncodeFile(string filePath) byte[] fileBytes = File.ReadAllBytes(filePath); return Convert.ToBase64String(fileBytes); } + + public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) + { + var environmentWrapper = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + + var implementation = new Product("Reqnroll", environmentWrapper.GetReqnrollVersion()); + string targetFramework = environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + + var runTime = new Product("dotNet", targetFramework); + var os = new Product(environmentWrapper.GetOSPlatform(), RuntimeInformation.OSDescription); + + var cpu = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm => new Product("arm", null), + Architecture.Arm64 => new Product("arm64", null), + Architecture.X86 => new Product("x86", null), + Architecture.X64 => new Product("x64", null), + _ => new Product(null, null), + }; + + var ci_name = environmentWrapper.GetBuildServerName(); + + var ci = ToCi(ci_name, environmentWrapper); + + return Envelope.Create(new Meta( + (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], + implementation, + runTime, + os, + cpu, + ci)); + } + + private static Ci ToCi(string ci_name, IEnvironmentWrapper environmentWrapper) + { + //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. + // Until then, these will be hard coded as null + if (String.IsNullOrEmpty(ci_name)) return null; + + var git = ToGit(environmentWrapper); + + return new Ci(ci_name, null, null, git); + } + + private static Git ToGit(IEnvironmentWrapper environmentWrapper) + { + Git git; + var git_url = environmentWrapper.GetEnvironmentVariable("GIT_URL"); + var git_branch = environmentWrapper.GetEnvironmentVariable("GIT_BRANCH"); + var git_commit = environmentWrapper.GetEnvironmentVariable("GIT_COMMIT"); + var git_tag = environmentWrapper.GetEnvironmentVariable("GIT_TAG"); + if (git_url is not ISuccess) git = null; + else + git = new Git + ( + (git_url as ISuccess).Result, + git_branch is ISuccess ? (git_branch as ISuccess).Result : null, + git_commit is ISuccess ? (git_commit as ISuccess).Result : null, + git_tag is ISuccess ? (git_tag as ISuccess).Result : null + ); + return git; + } #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 43fbb1ca7..d9fdeb7af 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -87,28 +87,7 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) { - //HACK: temporary implementation to obtain information for the Meta message - var analtyicsEventProcessor = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - var envInfo = analtyicsEventProcessor.CreateProjectRunningEvent(""); - var implementation = new Product("Reqnroll", envInfo.ReqnrollVersion); - var runTime = new Product("dotNet", envInfo.TargetFramework); - var os = new Product(envInfo.Platform, envInfo.PlatformDescription); - - var cpu = RuntimeInformation.ProcessArchitecture switch { - Architecture.Arm => new Product("arm", null), - Architecture.Arm64 => new Product("arm64", null), - Architecture.X86 => new Product("x86", null), - Architecture.X64 => new Product("x64", null), - _ => new Product(null, null), - }; - - yield return Envelope.Create(new Meta( - (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], - implementation, - runTime, - os, - cpu, - null)); + yield return CucumberMessageFactory.ToMeta(featureStartedEvent); Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 4ac9ebb9f..6bfd79e74 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; using Reqnroll.CommonModels; namespace Reqnroll.EnvironmentAccess @@ -36,5 +39,83 @@ public void SetEnvironmentVariable(string name, string value) } public string GetCurrentDirectory() => Environment.CurrentDirectory; + + public string GetOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "OSX"; + } + + throw new InvalidOperationException("Platform cannot be identified"); + } + + private readonly Dictionary buildServerTypes + = new Dictionary { + { "TF_BUILD","Azure Pipelines"}, + { "TEAMCITY_VERSION","TeamCity"}, + { "JENKINS_HOME","Jenkins"}, + { "GITHUB_ACTIONS","GitHub Actions"}, + { "GITLAB_CI","GitLab CI/CD"}, + { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, + { "TRAVIS","Travis CI"}, + { "APPVEYOR","AppVeyor"}, + { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, + { "bamboo_agentId", "Atlassian Bamboo" }, + { "CIRCLECI", "CircleCI" }, + { "GO_PIPELINE_NAME", "GoCD" }, + { "BUDDY", "Buddy" }, + { "NEVERCODE", "Nevercode" }, + { "SEMAPHORE", "SEMAPHORE" }, + { "BROWSERSTACK_USERNAME", "BrowserStack" }, + { "CF_BUILD_ID", "Codefresh" }, + { "TentacleVersion", "Octopus Deploy" }, + + { "CI_NAME", "CodeShip" } + }; + + public string GetBuildServerName() + { + foreach (var buildServerType in buildServerTypes) + { + var envVariable = GetEnvironmentVariable(buildServerType.Key); + if (envVariable is ISuccess) + return buildServerType.Value; + } + return null; + } + + public bool IsRunningInDockerContainer() + { + return GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + } + + public string GetReqnrollVersion() + { + return VersionInfo.AssemblyInformationalVersion; + } + public string GetNetCoreVersion() + { + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; + } + } } diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs index 5e88ba8bc..d6edfd6bc 100644 --- a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs @@ -13,5 +13,15 @@ public interface IEnvironmentWrapper void SetEnvironmentVariable(string name, string value); string GetCurrentDirectory(); + + string GetOSPlatform(); + + string GetBuildServerName(); + + bool IsRunningInDockerContainer(); + + string GetReqnrollVersion(); + + string GetNetCoreVersion(); } } From f7b6c39d6f9ef1a09fc5708185ec05512af0c89e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:30:38 -0500 Subject: [PATCH 121/218] Exceptions in steps produce Exception Messages. Hooks partially work. --- .../CucumberMessageFactory.cs | 18 +++++++++-- .../CucumberMessagePublisher.cs | 8 +++++ .../FeatureEventProcessor.cs | 30 ++++++++++++++++--- .../ScenarioEventProcessor.cs | 30 +++++++++++++++++-- .../ScenarioStepProcessor.cs | 5 ++++ .../CucumberMesssages/StepProcessorBase.cs | 1 + .../CCK/hooks/hooks.cs | 3 +- .../CucumberCompatibilityTests.cs | 3 +- 8 files changed, 86 insertions(+), 12 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 9a4cf7c40..94699675c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -188,9 +188,9 @@ internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProc return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookBindingFinishedEvent) + internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookFinishedEvent) { - return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookBindingFinishedEvent.Timestamp)); + return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) @@ -224,8 +224,20 @@ private static TestStepResult ToTestStepResult(StepProcessorBase stepState) Converters.ToDuration(stepState.Duration), "", ToTestStepResultStatus(stepState.Status), - null); + ToException(stepState.Exception) + ); + + } + private static Io.Cucumber.Messages.Types.Exception ToException(System.Exception exception) + { + if (exception == null) return null; + + return new Io.Cucumber.Messages.Types.Exception( + exception.GetType().Name, + exception.Message, + exception.StackTrace + ); } private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStatus status) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index eabd2afdf..908aa7254 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -27,6 +27,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { + //Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -49,6 +50,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); + testThreadEventPublisher.AddHandler(HookFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } @@ -152,6 +154,12 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin ProcessEvent(hookBindingEvent, featureName); } + private void HookFinishedEventHandler(HookFinishedEvent hookFinishedEvent) + { + var featureName = hookFinishedEvent.FeatureContext?.FeatureInfo?.Title; + ProcessEvent(hookFinishedEvent, featureName); + } + private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureName; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index d9fdeb7af..18d151c33 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -78,7 +78,8 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), - HookBindingFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), + HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), + HookFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), @@ -213,17 +214,38 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedE } } - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - var scenarioName = hookFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; + var scenarioName = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + foreach (var e in scenarioEP.ProcessEvent(hookBindingFinishedEvent)) { yield return e; } } + internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature + || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) + foreach(var e in Enumerable.Empty()) + { + yield return e; + } + else + { + var scenarioName = hookFinishedEvent.ScenarioContext?.ScenarioInfo?.Title; + var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; + + foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) + { + yield return e; + } + } + } + internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var scenarioName = attachmentAddedEvent.ScenarioName; diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 8fbb0aa2b..3e08097f4 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -85,6 +85,18 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding return Enumerable.Empty(); } + internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature + || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) + return Enumerable.Empty(); + + // The HookFinishedEvent does not tell us which hook(binding) was finished. We'll find out later during replay by tracking the last HookBindingFinishedEvent + _events.Enqueue(hookFinishedEvent); + return Enumerable.Empty(); + } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); @@ -142,6 +154,7 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish { _events.Enqueue(scenarioFinishedEvent); TestStepStarted mostRecentTestStepStarted = null; + StepProcessorBase mostRecentHookStep = null; while (_events.Count > 0) { @@ -169,7 +182,6 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish var stepFinishedState = StepsByEvent[stepFinishedEvent]; yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); break; - //TODO: this isn't right; shouuld be one hook processor per hook that ran case HookBindingStartedEvent hookBindingStartedEvent: var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); @@ -177,8 +189,20 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepFinishedState = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepFinishedState as HookStepProcessor, hookBindingFinishedEvent)); + // Find the hookStep that matches the hookBinding and store it temporarily; to be processed when the hook finished event is processed + mostRecentHookStep = StepsByEvent[hookBindingFinishedEvent]; + break; + case HookFinishedEvent hookFinishedEvent: + // mostRecentHookStep will be null when we've already created a TestStepFinished for the hookBinding as there may be multiple HookFinishedEvents for a Step or Block or Scenario + if (mostRecentHookStep == null) + break; + if (mostRecentHookStep != null && hookFinishedEvent.HookException != null) + { + mostRecentHookStep.Exception = hookFinishedEvent.HookException; + } + var hookStepProcessor = mostRecentHookStep as HookStepProcessor; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor, hookStepProcessor.HookBindingFinishedEvent)); + mostRecentHookStep = null; break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 29c71c5a9..9f53ad4d2 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -60,6 +60,11 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; Status = stepFinishedEvent.StepContext.Status; + if (Status == ScenarioExecutionStatus.TestError && stepFinishedEvent.ScenarioContext.TestError != null) + { + Exception = stepFinishedEvent.ScenarioContext.TestError; + } + StepArguments = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument { diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index 090b5055d..af4834a6f 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -8,6 +8,7 @@ public class StepProcessorBase : IStepProcessor public string TestCaseStartedID => parentScenario.TestCaseStartedID; public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } + public Exception Exception { get; set; } public ScenarioEventProcessor parentScenario; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 5f43c19c0..851e25555 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -15,6 +15,7 @@ internal class Hooks public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) { + //Debugger.Launch(); this.reqnrollOutputHelper = reqnrollOutputHelper; } @@ -50,7 +51,7 @@ public void FailingAfterHook() [AfterScenario("with-attachment")] public void PassingAfterHook() { - Debugger.Launch(); + var ext = "svg"; var path = FileSystemPath.GetFilePathForAttachments(); var attachment = Path.Combine(path, "hooks", $"cucumber.{ext}"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 93f22226f..6ff839517 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -26,6 +26,7 @@ public void SmokeTest() AddPassingStepBinding("When"); + ExecuteTests(); ShouldAllScenariosPass(); @@ -54,7 +55,7 @@ public void CCKScenarios(string scenarioName) AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + //AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); From 18a3d018510f3b5c722575812d1384839c093201 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 31 Aug 2024 11:06:20 -0500 Subject: [PATCH 122/218] Moved back to using HookBindingFinished; eliminated use of HookFinished event. --- .../CucumberMessagePublisher.cs | 7 ---- .../FeatureEventProcessor.cs | 22 ------------ .../CucumberMesssages/HookStepProcessor.cs | 1 + .../ScenarioEventProcessor.cs | 35 +++---------------- Reqnroll/Events/ExecutionEvent.cs | 4 ++- .../Infrastructure/TestExecutionEngine.cs | 12 +++++-- .../CCK/hooks/hooks.cs | 2 +- 7 files changed, 20 insertions(+), 63 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 908aa7254..7f48a76dc 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -50,7 +50,6 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(StepFinishedEventHandler); testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); - testThreadEventPublisher.AddHandler(HookFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); } @@ -154,12 +153,6 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin ProcessEvent(hookBindingEvent, featureName); } - private void HookFinishedEventHandler(HookFinishedEvent hookFinishedEvent) - { - var featureName = hookFinishedEvent.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookFinishedEvent, featureName); - } - private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureName; diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 18d151c33..6f534827c 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -79,7 +79,6 @@ private IEnumerable DispatchEvent(ExecutionEvent anEvent) StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), - HookFinishedEvent hookFinishedEvent => ProcessEvent(hookFinishedEvent), AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), _ => throw new NotImplementedException(), @@ -225,27 +224,6 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding } } - internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature - || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) - foreach(var e in Enumerable.Empty()) - { - yield return e; - } - else - { - var scenarioName = hookFinishedEvent.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookFinishedEvent)) - { - yield return e; - } - } - } - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var scenarioName = attachmentAddedEvent.ScenarioName; diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 7fe99228a..7c17966ce 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -23,6 +23,7 @@ public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEv public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; + Exception = hookFinishedEvent.HookException; return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 3e08097f4..11dae1307 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -34,7 +34,7 @@ public List Steps { get { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value ).ToList(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); } } public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } @@ -85,18 +85,6 @@ internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBinding return Enumerable.Empty(); } - internal IEnumerable ProcessEvent(HookFinishedEvent hookFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookFinishedEvent.HookType == HookType.AfterFeature || hookFinishedEvent.HookType == HookType.BeforeFeature - || hookFinishedEvent.HookType == HookType.BeforeTestRun || hookFinishedEvent.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - - // The HookFinishedEvent does not tell us which hook(binding) was finished. We'll find out later during replay by tracking the last HookBindingFinishedEvent - _events.Enqueue(hookFinishedEvent); - return Enumerable.Empty(); - } - private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); @@ -126,7 +114,7 @@ internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAdded { //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); var pickleStepId = ""; - + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); _events.Enqueue(attachmentExecutionEventWrapper); @@ -147,14 +135,13 @@ internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent) (kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent)(kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); } internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { _events.Enqueue(scenarioFinishedEvent); TestStepStarted mostRecentTestStepStarted = null; - StepProcessorBase mostRecentHookStep = null; while (_events.Count > 0) { @@ -189,20 +176,8 @@ internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinish yield return Envelope.Create(hookStepStarted); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - // Find the hookStep that matches the hookBinding and store it temporarily; to be processed when the hook finished event is processed - mostRecentHookStep = StepsByEvent[hookBindingFinishedEvent]; - break; - case HookFinishedEvent hookFinishedEvent: - // mostRecentHookStep will be null when we've already created a TestStepFinished for the hookBinding as there may be multiple HookFinishedEvents for a Step or Block or Scenario - if (mostRecentHookStep == null) - break; - if (mostRecentHookStep != null && hookFinishedEvent.HookException != null) - { - mostRecentHookStep.Exception = hookFinishedEvent.HookException; - } - var hookStepProcessor = mostRecentHookStep as HookStepProcessor; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor, hookStepProcessor.HookBindingFinishedEvent)); - mostRecentHookStep = null; + var hookStepProcessor = StepsByEvent[hookBindingFinishedEvent]; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); break; case AttachmentAddedEventWrapper attachmentAddedEventWrapper: // find the TestCaseStepId and testCaseStartedId diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e9aabf778..a3f2a0003 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -190,6 +190,7 @@ public class HookBindingFinishedEvent : ExecutionEvent public TimeSpan Duration { get; } public IContextManager ContextManager { get; private set; } + public Exception HookException { get; private set; } public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { @@ -197,9 +198,10 @@ public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) Duration = duration; } - public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager) : this(hookBinding, duration) + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager, Exception hookException = null) : this(hookBinding, duration) { ContextManager = contextManager; + HookException = hookException; } } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index c1164a7ee..af7f87026 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -355,14 +355,22 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); - + Exception exceptionthrown = null; try { await invoker.InvokeBindingAsync(hookBinding, _contextManager, arguments, _testTracer, durationHolder); } + catch (Exception exception) + { + // This exception is caught in order to be able to inform consumers of the HookBindingFinishedEvent; + // This is used by CucumberMessages to include information about the exception in the hook TestStepResult + // The throw; statement ensures that the exception is propagated up to the FireEventsAsync method + exceptionthrown = exception; + throw; + } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager)); + _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager, exceptionthrown)); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 851e25555..011fe1d83 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -42,7 +42,7 @@ public void NamedBeforeHook() { } [AfterScenario] public void AfterScenarioHook() { } - [AfterScenario("some-tag or some-other-tag")] + [AfterScenario(), Scope(Tag = "some-tag or some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); From b96aff1b67f425c250a9281367ca4fd229c3e9a6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:02:04 -0500 Subject: [PATCH 123/218] Hooks now working per CCK pending outcome of bug analysis of TEE. --- .../CucumberMesssages/CucumberMessagePublisher.cs | 2 +- Reqnroll/CucumberMesssages/HookStepProcessor.cs | 2 ++ .../CucumberMesssages/ScenarioEventProcessor.cs | 1 - Reqnroll/Infrastructure/TestExecutionEngine.cs | 3 ++- .../CCK/hooks/hooks.cs | 5 +++-- .../CucumberCompatibilityTests.cs | 14 +++++++++++++- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 7f48a76dc..296727ccb 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -27,7 +27,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - //Debugger.Launch(); + // Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 7c17966ce..649d2f330 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -24,6 +24,8 @@ public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedE { HookBindingFinishedEvent = hookFinishedEvent; Exception = hookFinishedEvent.HookException; + Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; + return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index 11dae1307..b8f8d9836 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -72,7 +72,6 @@ internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingS internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - // At this point we only care about hooks that wrap scenarios or steps if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index af7f87026..aca42938a 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -231,10 +231,11 @@ public virtual async Task OnScenarioEndAsync() { await FireScenarioEventsAsync(HookType.AfterScenario); } - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); } finally { + _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); + _contextManager.CleanupScenarioContext(); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs index 011fe1d83..5b6294857 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs @@ -15,7 +15,6 @@ internal class Hooks public Hooks(IReqnrollOutputHelper reqnrollOutputHelper) { - //Debugger.Launch(); this.reqnrollOutputHelper = reqnrollOutputHelper; } @@ -42,7 +41,9 @@ public void NamedBeforeHook() { } [AfterScenario] public void AfterScenarioHook() { } - [AfterScenario(), Scope(Tag = "some-tag or some-other-tag")] + [AfterScenario()] + [Scope(Tag = "some-tag")] + [Scope(Tag = "some-other-tag")] public void FailingAfterHook() { throw new Exception("Exception in conditional hook"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 6ff839517..2ca8afac3 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -17,6 +17,7 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test + @some-tag Scenario: Log JSON When the following string is attached as "application/json": ``` @@ -25,7 +26,18 @@ public void SmokeTest() """); AddPassingStepBinding("When"); - + AddBindingClass(""" + [Binding] + public class TaggedScenario + { + [AfterScenario()] + [Scope(Tag = "some-tag")] + public void FailingAfterHook() + { + throw new Exception("Exception in conditional hook"); + } + } + """); ExecuteTests(); From 6f88cd037685b30f9cb4c6a2dbeb6bd4030c48c3 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:21:33 -0500 Subject: [PATCH 124/218] Given default Gherkin.Ast.Locations of (0,0) to generated Table elements (headers, rows, cells). --- .../Transformation/IncludeExternalDataTransformation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs index c649d032b..3441f3d5d 100644 --- a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs +++ b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs @@ -85,7 +85,7 @@ private Scenario GetTransformedScenario(Scenario scenario, ExternalDataSpecifica { var exampleRecords = specification.GetExampleRecords(examplesHeaderNames); var exampleRows = exampleRecords.Items - .Select(rec => new Gherkin.Ast.TableRow(null, exampleRecords.Header.Select(h => new TableCell(null, rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) + .Select(rec => new Gherkin.Ast.TableRow(new Location(0, 0), exampleRecords.Header.Select(h => new TableCell(new Location(0, 0), rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) .ToArray(); var examplesBlock = CreateExamplesBlock(exampleRecords.Header, exampleRows, examplesKeyword); @@ -108,8 +108,8 @@ private Examples CreateExamplesBlock(string[] headerNames, Gherkin.Ast.TableRow[ { keyword ??= "External Examples"; var name = "External Examples"; - var tableHeader = new Gherkin.Ast.TableRow(null, headerNames.Select(h => new TableCell(null, h)).ToArray()); - return new Examples(new Tag[0], null, keyword, name, "", tableHeader, exampleRows); + var tableHeader = new Gherkin.Ast.TableRow(new Location(0, 0), headerNames.Select(h => new TableCell(new Location(0, 0), h)).ToArray()); + return new Examples(new Tag[0], new Location(0, 0), keyword, name, "", tableHeader, exampleRows); } } } From 5532d9fc3e4b25a3daeee471912c1f1ab30b30d8 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:42:52 -0500 Subject: [PATCH 125/218] Added a smoke test to prove that ExternalData generated Example tables are properly parsed as DataTables by the Gherkin pickler. --- .../CucumberCompatibilityTests.cs | 120 ++++++++++++++++++ ...CucumberMessages.CompatibilityTests.csproj | 4 + .../products.csv | 4 + 3 files changed, 128 insertions(+) create mode 100644 Tests/CucumberMessages.CompatibilityTests/products.csv diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 2ca8afac3..7220109e9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -2,6 +2,7 @@ using Cucumber.Messages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; +using Reqnroll; using System.Reflection; namespace CucumberMessages.CompatibilityTests @@ -44,6 +45,125 @@ public void FailingAfterHook() ShouldAllScenariosPass(); } + [TestMethod] + public void CucumberMessagesInteropWithExternalData() + { + // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages + AddCucumberMessagePlugIn(); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.0-local"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest + var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.CompatibilityTests", Assembly.GetExecutingAssembly()); + _projectsDriver.AddFile("products.csv", content); + AddFeatureFile(""" + Feature: External Data from CSV file + + @DataSource:products.csv + Scenario: Valid product prices are calculated + The scenario will be treated as a scenario outline with the examples from the CSV file. + Given the customer has put 1 pcs of to the basket + When the basket price is calculated + Then the basket price should be greater than zero + + """); + + AddBindingClass(""" + using System; + using System.Collections.Generic; + using System.Linq; + + namespace Reqnroll.ExternalData.ReqnrollPlugin.IntegrationTest.StepDefinitions + { + [Binding] + public class PricingStepDefinitions + { + class PriceCalculator + { + private readonly Dictionary _basket = new(); + private readonly Dictionary _itemPrices = new(); + + public void AddToBasket(string productName, int quantity) + { + if (!_basket.TryGetValue(productName, out var currentQuantity)) + currentQuantity = 0; + _basket[productName] = currentQuantity + quantity; + } + + public decimal CalculatePrice() + { + return _basket.Sum(bi => GetPrice(bi.Key) * bi.Value); + } + + private decimal GetPrice(string productName) + { + if (_itemPrices.TryGetValue(productName, out var itemPrice)) + return itemPrice; + return 1.5m; + } + + public void SetPrice(string productName, in decimal itemPrice) + { + _itemPrices[productName] = itemPrice; + } + } + + private readonly ScenarioContext _scenarioContext; + private readonly PriceCalculator _priceCalculator = new(); + private decimal _calculatedPrice; + + public PricingStepDefinitions(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [Given(@"the price of (.*) is €(.*)")] + public void GivenThePriceOfProductIs(string productName, decimal itemPrice) + { + _priceCalculator.SetPrice(productName, itemPrice); + } + + [Given(@"the customer has put (.*) pcs of (.*) to the basket")] + public void GivenTheCustomerHasPutPcsOfProductToTheBasket(int quantity, string productName) + { + _priceCalculator.AddToBasket(productName, quantity); + } + + [Given(@"the customer has put a product to the basket")] + public void GivenTheCustomerHasPutAProductToTheBasket() + { + var productName = _scenarioContext.ScenarioInfo.Arguments["product"]?.ToString(); + _priceCalculator.AddToBasket(productName, 1); + } + + [When(@"the basket price is calculated")] + public void WhenTheBasketPriceIsCalculated() + { + _calculatedPrice = _priceCalculator.CalculatePrice(); + } + + [Then(@"the basket price should be greater than zero")] + public void ThenTheBasketPriceShouldBeGreaterThanZero() + { + if (_calculatedPrice <= 0) throw new Exception("Basket price is less than zero: " + _calculatedPrice ); + } + + [Then(@"the basket price should be €(.*)")] + public void ThenTheBasketPriceShouldBe(decimal expectedPrice) + { + if(expectedPrice != _calculatedPrice) throw new Exception("Basket price is not as expected: " + _calculatedPrice + " vs " + expectedPrice); + } + + } + } + + """); + ExecuteTests(); + + ShouldAllScenariosPass(); + + } + [TestMethod] [DataRow("attachments")] [DataRow("minimal")] diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index c5c455a05..1c4df4f41 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -49,6 +49,7 @@ + @@ -109,6 +110,9 @@ + + Always + Always diff --git a/Tests/CucumberMessages.CompatibilityTests/products.csv b/Tests/CucumberMessages.CompatibilityTests/products.csv new file mode 100644 index 000000000..b47bfe36b --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/products.csv @@ -0,0 +1,4 @@ +product,price +Chocolate,2.5 +Apple,1.0 +Orange,1.2 From 6359f4c01d7d9d3b4147f18986df9502a46d847d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:03:36 -0500 Subject: [PATCH 126/218] Ensure that CucumberMessages doesn't run when no Sink is registered. --- .../CucumberMessageBroker.cs | 4 ++ .../CucumberMessagePublisher.cs | 60 +++++++++++++++++-- .../CucumberCompatibilityTests.cs | 18 ++++++ 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 777cdee28..87fe56f4f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -14,6 +14,7 @@ namespace Reqnroll.CucumberMesssages public interface ICucumberMessageBroker { + bool Enabled { get; } void Complete(string cucumberMessageSource); void Publish(ReqnrollCucumberMessage message); } @@ -21,6 +22,9 @@ public interface ICucumberMessageBroker public class CucumberMessageBroker : ICucumberMessageBroker { private IObjectContainer _objectContainer; + + public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; + //private ITraceListener _traceListener; public CucumberMessageBroker(IObjectContainer objectContainer) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 296727ccb..28916baa2 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -24,6 +24,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi private ICucumberMessageBroker broker; private IObjectContainer objectContainer; private ConcurrentDictionary featureProcessorsByFeatureName = new(); + bool Enabled = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { @@ -57,14 +58,23 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - var enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; + + // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; + // if not, then this is disabled for this feature + // if true, then it checks with the broker to confirm that a listener/sink has been registered + Enabled = broker.Enabled; + if (!Enabled) + return; + + var featureEnabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; var featureProc = new FeatureEventProcessor { Name = featureName, - Enabled = enabled + Enabled = featureEnabled }; + // todo: need a lock around this if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) { // This feature has already been started by another thread (executing a different scenario) @@ -78,7 +88,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); - if (!enabled) + if (!featureEnabled) return; ProcessEvent(featureStartedEvent, featureName); @@ -86,6 +96,10 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { + if (!Enabled) + return; + + var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; var featureProcessor = featureProcessorsByFeatureName[featureName]; @@ -119,55 +133,93 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { + if (!Enabled) + return; + + var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(scenarioStartedEvent, featureName); } private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { + if (!Enabled) + return; + + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(scenarioFinishedEvent, featureName); } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { + if (!Enabled) + return; + + var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(stepStartedEvent, featureName); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { + if (!Enabled) + return; + + var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; ProcessEvent(stepFinishedEvent, featureName); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { + if (!Enabled) + return; + + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingStartedEvent, featureName); } private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) { + if (!Enabled) + return; + + var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; ProcessEvent(hookBindingEvent, featureName); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { + if (!Enabled) + return; + + var featureName = attachmentAddedEvent.FeatureName; ProcessEvent(attachmentAddedEvent, featureName); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) { - ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); + if (!Enabled) + return; + + + ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); } private void ProcessEvent(ExecutionEvent anEvent, string featureName) { + if (!Enabled) + return; + + var featureProcessor = featureProcessorsByFeatureName[featureName]; + if (!featureProcessor.Enabled) + return; featureProcessor.ProcessEvent(anEvent); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7220109e9..b38e389ab 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -2,6 +2,7 @@ using Cucumber.Messages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; +using Newtonsoft.Json.Bson; using Reqnroll; using System.Reflection; @@ -10,6 +11,23 @@ namespace CucumberMessages.CompatibilityTests [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { + [TestMethod] + public void NullTest() + { + // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else + + AddFeatureFile(""" + Feature: Cucumber Messages Null Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } [TestMethod] public void SmokeTest() { From e154a376b5a9eda44dd3fd708f916f9eb0c18040 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 08:47:59 -0500 Subject: [PATCH 127/218] Refactored Environment Info away from EnvironmentWrapper --- ...atureFileCodeBehindTaskContainerBuilder.cs | 2 +- Reqnroll/Analytics/AnalyticsEventProvider.cs | 24 ++--- .../CucumberMessageFactory.cs | 69 ++++++------- .../EnvironmentInfoProvider.cs | 97 +++++++++++++++++++ .../EnvironmentAccess/EnvironmentWrapper.cs | 77 --------------- .../IEnvironmentInfoProvider.cs | 11 +++ .../EnvironmentAccess/IEnvironmentWrapper.cs | 10 -- .../DefaultDependencyProvider.cs | 1 + .../Analytics/AnalyticsEventProviderTests.cs | 12 ++- 9 files changed, 167 insertions(+), 136 deletions(-) create mode 100644 Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs create mode 100644 Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs diff --git a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs index fafbf4afa..5ea8ba55d 100644 --- a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs +++ b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs @@ -40,7 +40,7 @@ public IObjectContainer BuildRootContainer( objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); - + objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); diff --git a/Reqnroll/Analytics/AnalyticsEventProvider.cs b/Reqnroll/Analytics/AnalyticsEventProvider.cs index 301835a22..6c1f8dc07 100644 --- a/Reqnroll/Analytics/AnalyticsEventProvider.cs +++ b/Reqnroll/Analytics/AnalyticsEventProvider.cs @@ -13,13 +13,13 @@ namespace Reqnroll.Analytics public class AnalyticsEventProvider : IAnalyticsEventProvider { private readonly IUserUniqueIdStore _userUniqueIdStore; - private readonly IEnvironmentWrapper _environmentWrapper; + private readonly IEnvironmentInfoProvider _environmentInfoProvider; private readonly string _unitTestProvider; - public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentWrapper environmentWrapper) + public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentInfoProvider environmentInfoProvider) { _userUniqueIdStore = userUniqueIdStore; - _environmentWrapper = environmentWrapper; + _environmentInfoProvider = environmentInfoProvider; _unitTestProvider = unitTestProviderConfiguration.UnitTestProvider; } @@ -27,11 +27,11 @@ public ReqnrollProjectCompilingEvent CreateProjectCompilingEvent(string msbuildV { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); - string buildServerName = _environmentWrapper.GetBuildServerName(); - bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); string hashedAssemblyName = ToSha256(assemblyName); - string platform = _environmentWrapper.GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var compiledEvent = new ReqnrollProjectCompilingEvent( @@ -56,13 +56,13 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = _environmentWrapper.GetReqnrollVersion(); - string targetFramework = _environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; - bool isDockerContainer = _environmentWrapper.IsRunningInDockerContainer(); - string buildServerName = _environmentWrapper.GetBuildServerName(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string targetFramework = _environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); string hashedAssemblyName = ToSha256(testAssemblyName); - string platform = _environmentWrapper.GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var runningEvent = new ReqnrollProjectRunningEvent( diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 94699675c..3660b1364 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -254,40 +254,17 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat }; } - - #region utility methods - public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) - { - string signature = GenerateSignature(stepDefinition); - - return $"{stepDefinition.SourceExpression}({signature})"; - } - - public static string CanonicalizeHookBinding(IHookBinding hookBinding) - { - string signature = GenerateSignature(hookBinding); - return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; - } - - private static string GenerateSignature(IBinding stepDefinition) - { - return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; - } - public static string Base64EncodeFile(string filePath) - { - byte[] fileBytes = File.ReadAllBytes(filePath); - return Convert.ToBase64String(fileBytes); - } - public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) { - var environmentWrapper = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + var featureContainer = featureStartedEvent.FeatureContext.FeatureContainer; + var environmentInfoProvider = featureContainer.Resolve(); + var environmentWrapper = featureContainer.Resolve(); - var implementation = new Product("Reqnroll", environmentWrapper.GetReqnrollVersion()); - string targetFramework = environmentWrapper.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + var implementation = new Product("Reqnroll", environmentInfoProvider.GetReqnrollVersion()); + string targetFramework = environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; var runTime = new Product("dotNet", targetFramework); - var os = new Product(environmentWrapper.GetOSPlatform(), RuntimeInformation.OSDescription); + var os = new Product(environmentInfoProvider.GetOSPlatform(), RuntimeInformation.OSDescription); var cpu = RuntimeInformation.ProcessArchitecture switch { @@ -298,9 +275,9 @@ public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) _ => new Product(null, null), }; - var ci_name = environmentWrapper.GetBuildServerName(); + var ci_name = environmentInfoProvider.GetBuildServerName(); - var ci = ToCi(ci_name, environmentWrapper); + var ci = ToCi(ci_name, environmentInfoProvider, environmentWrapper); return Envelope.Create(new Meta( (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], @@ -311,12 +288,12 @@ public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) ci)); } - private static Ci ToCi(string ci_name, IEnvironmentWrapper environmentWrapper) + private static Ci ToCi(string ci_name, IEnvironmentInfoProvider environmentInfoProvider, IEnvironmentWrapper environmentWrapper) { //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. // Until then, these will be hard coded as null if (String.IsNullOrEmpty(ci_name)) return null; - + var git = ToGit(environmentWrapper); return new Ci(ci_name, null, null, git); @@ -340,6 +317,32 @@ private static Git ToGit(IEnvironmentWrapper environmentWrapper) ); return git; } + + #region utility methods + public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + { + string signature = GenerateSignature(stepDefinition); + + return $"{stepDefinition.SourceExpression}({signature})"; + } + + public static string CanonicalizeHookBinding(IHookBinding hookBinding) + { + string signature = GenerateSignature(hookBinding); + return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + } + + private static string GenerateSignature(IBinding stepDefinition) + { + return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + } + public static string Base64EncodeFile(string filePath) + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } + + #endregion } } \ No newline at end of file diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs new file mode 100644 index 000000000..6a9a1cc50 --- /dev/null +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -0,0 +1,97 @@ +using Reqnroll.CommonModels; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace Reqnroll.EnvironmentAccess +{ + public class EnvironmentInfoProvider : IEnvironmentInfoProvider + { + private readonly IEnvironmentWrapper environmentWrapper; + + public EnvironmentInfoProvider(IEnvironmentWrapper environmentWrapper) + { + this.environmentWrapper = environmentWrapper; + } + + public string GetOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "OSX"; + } + + throw new InvalidOperationException("Platform cannot be identified"); + } + + private readonly Dictionary buildServerTypes + = new Dictionary { + { "TF_BUILD","Azure Pipelines"}, + { "TEAMCITY_VERSION","TeamCity"}, + { "JENKINS_HOME","Jenkins"}, + { "GITHUB_ACTIONS","GitHub Actions"}, + { "GITLAB_CI","GitLab CI/CD"}, + { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, + { "TRAVIS","Travis CI"}, + { "APPVEYOR","AppVeyor"}, + { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, + { "bamboo_agentId", "Atlassian Bamboo" }, + { "CIRCLECI", "CircleCI" }, + { "GO_PIPELINE_NAME", "GoCD" }, + { "BUDDY", "Buddy" }, + { "NEVERCODE", "Nevercode" }, + { "SEMAPHORE", "SEMAPHORE" }, + { "BROWSERSTACK_USERNAME", "BrowserStack" }, + { "CF_BUILD_ID", "Codefresh" }, + { "TentacleVersion", "Octopus Deploy" }, + + { "CI_NAME", "CodeShip" } + }; + + public string GetBuildServerName() + { + foreach (var buildServerType in buildServerTypes) + { + var envVariable = environmentWrapper.GetEnvironmentVariable(buildServerType.Key); + if (envVariable is ISuccess) + return buildServerType.Value; + } + return null; + } + + public bool IsRunningInDockerContainer() + { + return environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + } + + public string GetReqnrollVersion() + { + return VersionInfo.AssemblyInformationalVersion; + } + public string GetNetCoreVersion() + { + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; + } + + } +} diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 6bfd79e74..40d9f0ef3 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -40,82 +40,5 @@ public void SetEnvironmentVariable(string name, string value) public string GetCurrentDirectory() => Environment.CurrentDirectory; - public string GetOSPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Linux"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "OSX"; - } - - throw new InvalidOperationException("Platform cannot be identified"); - } - - private readonly Dictionary buildServerTypes - = new Dictionary { - { "TF_BUILD","Azure Pipelines"}, - { "TEAMCITY_VERSION","TeamCity"}, - { "JENKINS_HOME","Jenkins"}, - { "GITHUB_ACTIONS","GitHub Actions"}, - { "GITLAB_CI","GitLab CI/CD"}, - { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, - { "TRAVIS","Travis CI"}, - { "APPVEYOR","AppVeyor"}, - { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, - { "bamboo_agentId", "Atlassian Bamboo" }, - { "CIRCLECI", "CircleCI" }, - { "GO_PIPELINE_NAME", "GoCD" }, - { "BUDDY", "Buddy" }, - { "NEVERCODE", "Nevercode" }, - { "SEMAPHORE", "SEMAPHORE" }, - { "BROWSERSTACK_USERNAME", "BrowserStack" }, - { "CF_BUILD_ID", "Codefresh" }, - { "TentacleVersion", "Octopus Deploy" }, - - { "CI_NAME", "CodeShip" } - }; - - public string GetBuildServerName() - { - foreach (var buildServerType in buildServerTypes) - { - var envVariable = GetEnvironmentVariable(buildServerType.Key); - if (envVariable is ISuccess) - return buildServerType.Value; - } - return null; - } - - public bool IsRunningInDockerContainer() - { - return GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; - } - - public string GetReqnrollVersion() - { - return VersionInfo.AssemblyInformationalVersion; - } - public string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - - return null; - } - } } diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs new file mode 100644 index 000000000..562372ec4 --- /dev/null +++ b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.EnvironmentAccess +{ + public interface IEnvironmentInfoProvider + { + string GetOSPlatform(); + string GetBuildServerName(); + bool IsRunningInDockerContainer(); + string GetReqnrollVersion(); + string GetNetCoreVersion(); + } +} \ No newline at end of file diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs index d6edfd6bc..5e88ba8bc 100644 --- a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs @@ -13,15 +13,5 @@ public interface IEnvironmentWrapper void SetEnvironmentVariable(string name, string value); string GetCurrentDirectory(); - - string GetOSPlatform(); - - string GetBuildServerName(); - - bool IsRunningInDockerContainer(); - - string GetReqnrollVersion(); - - string GetNetCoreVersion(); } } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index d4f0e2d98..8e0810ccf 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -72,6 +72,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); diff --git a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs index 0d28c52c0..33e26f385 100644 --- a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs @@ -20,7 +20,9 @@ public void Should_return_the_build_server_name_in_Compiling_Event() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); environmentMock .Setup(m => m.GetEnvironmentVariable("TF_BUILD")) @@ -36,7 +38,9 @@ public void Should_return_the_build_server_name_in_Running_Event() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); environmentMock .Setup(m => m.GetEnvironmentVariable("TEAMCITY_VERSION")) @@ -52,7 +56,9 @@ public void Should_return_null_for_the_build_server_name_when_not_detected() { var userUniqueIdStoreMock = new Mock(); var environmentMock = new Mock(); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentMock.Object); + var environmentInfoMock = new Mock(environmentMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); var compilingEvent = sut.CreateProjectRunningEvent(null); From fda9d618d695518059ce56469ecade9634e826d0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:55:53 -0500 Subject: [PATCH 128/218] Update nuget dependencies to match Main --- .../Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- .../CucumberMessages.CompatibilityTests.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 9624408fd..4df9424dd 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -11,7 +11,7 @@ - + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 1c4df4f41..31421d204 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,9 +120,9 @@ - - - + + + From 3cf8a8d1341081ba7cd57d9c935ce2bbc5fd0494 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 5 Sep 2024 08:22:01 -0500 Subject: [PATCH 129/218] Establishes cross-reference data structures to find Messages by type and ID. Validates that top-level Messages match by Type and Count. --- .../CucumberCompatibilityTests.cs | 48 ++++- ...CucumberMessages.CompatibilityTests.csproj | 1 + .../CucumberMessagesValidator.cs | 192 ++++++++++++++++++ 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index b38e389ab..818e94ac8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -5,6 +5,8 @@ using Newtonsoft.Json.Bson; using Reqnroll; using System.Reflection; +using FluentAssertions; +using System.Text.Json; namespace CucumberMessages.CompatibilityTests { @@ -209,6 +211,9 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); + var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + validator.ResultShouldPassAllComparisonTests(); + ConfirmAllTestsRan(null); } @@ -221,10 +226,49 @@ private void AddUtilClassWithFileSystemPath() private IEnumerable GetExpectedResults(string scenarioName) { - var workingDirectory = Assembly.GetExecutingAssembly().GetAssemblyLocation(); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory, $"{scenarioName}.feature.ndjson")); + var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); } + + private IEnumerable GetActualResults(string scenarioName) + { + var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; + var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; + var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); + + foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); + } } + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } + } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 31421d204..613faa012 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,6 +120,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs new file mode 100644 index 000000000..4cc8a13eb --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -0,0 +1,192 @@ +using FluentAssertions; +using Io.Cucumber.Messages.Types; +using System.Diagnostics.Eventing.Reader; + +namespace CucumberMessages.CompatibilityTests +{ + internal class CucumberMessagesValidator + { + private IEnumerable actualEnvelopes; + private IEnumerable expectedEnvelopes; + + // cross-reference metadata + private Dictionary> actuals_IDsByType = new(); + private Dictionary> expecteds_IDsByType = new(); + private Dictionary> actuals_elementsByType = new(); + private Dictionary> expecteds_elementsByType = new(); + private Dictionary> actuals_elementsByID = new(); + private Dictionary> expecteds_elementsByID = new(); + + public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) + { + actualEnvelopes = actual; + expectedEnvelopes = expected; + + SetupCrossReferences(actual, actuals_IDsByType, actuals_elementsByType, actuals_elementsByID); + SetupCrossReferences(expected, expecteds_IDsByType, expecteds_elementsByType, expecteds_elementsByID); + + } + private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) + { + foreach (var message in messages) + { + var msg = message.Content(); + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + } + } + private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) + { + if (!IDsByType.ContainsKey(msg.GetType())) + { + IDsByType.Add(msg.GetType(), new HashSet()); + } + IDsByType[msg.GetType()].Add(msg.Id()); + } + + private static void InsertIntoElementsById(object msg, Dictionary> elementsByID) + { + if (!elementsByID.ContainsKey(msg.Id())) + { + elementsByID.Add(msg.Id(), new HashSet()); + } + elementsByID[msg.Id()].Add(msg); + } + + private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) + { + if (!elementsByType.ContainsKey(msg.GetType())) + { + elementsByType.Add(msg.GetType(), new HashSet()); + } + elementsByType[msg.GetType()].Add(msg); + } + + internal void ResultShouldPassAllComparisonTests() + { + ShouldPassBasicStructuralChecks(actualEnvelopes, actualEnvelopes); + } + + internal void ResultShouldPassBasicSanityChecks() + { + throw new NotImplementedException(); + } + internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnumerable expected) + { + actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); + + //todo: modify this to use lists of types from actual and expected and use FluentAssertions directly + foreach (var messageType in MessageExtensions.EnvelopeContentTypes) + { + if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Count().Should().BeGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); + } + } + } + + internal static class MessageExtensions + { + public static List MessagesWithIds = new(){ typeof(Background), + typeof(Examples), + typeof(Hook), + typeof(ParameterType), + typeof(Pickle), + typeof(PickleStep), + typeof(Rule), + typeof(Scenario), + typeof(Step), + typeof(StepDefinition), + typeof(TableRow), + typeof(Tag), + typeof(TestCase), + typeof(TestCaseStarted), + typeof(TestStep) + }; + + internal static bool HasId(this object element) + { + return MessagesWithIds.Contains(element.GetType()); + } + internal static string Id(this object message) + { + return message switch + { + Background bgd => bgd.Id, + Examples ex => ex.Id, + Hook hook => hook.Id, + ParameterType pt => pt.Id, + Pickle p => p.Id, + PickleStep ps => ps.Id, + Rule r => r.Id, + Scenario sc => sc.Id, + Step st => st.Id, + StepDefinition sd => sd.Id, + TableRow tr => tr.Id, + Tag tag => tag.Id, + TestCase tc => tc.Id, + TestCaseStarted tcs => tcs.Id, + TestStep ts => ts.Id, + _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") + }; + } + internal static List EnvelopeContentTypes = new() + { + typeof(Attachment), + typeof(GherkinDocument), + typeof(Hook), + typeof(Meta), + typeof(ParameterType), + typeof(ParseError), + typeof(Pickle), + typeof(Source), + typeof(StepDefinition), + typeof(TestCase), + typeof(TestCaseFinished), + typeof(TestCaseStarted), + typeof(TestRunFinished), + typeof(TestRunStarted), + typeof(TestStepFinished), + typeof(TestStepStarted), + typeof(UndefinedParameterType) + }; + internal static object Content(this Envelope envelope) + { + object? result = null; + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + else if (envelope.Hook != null) { result = envelope.Hook; } + else if (envelope.Meta != null) { result = envelope.Meta; } + else if (envelope.ParameterType != null) { result = envelope.ParameterType; } + else if (envelope.ParseError != null) { result = envelope.ParseError; } + else if (envelope.Pickle != null) { result = envelope.Pickle; } + else if (envelope.Source != null) { result = envelope.Source; } + else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } + else if (envelope.TestCase != null) { result = envelope.TestCase; } + else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } + else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } + return result!; + } + } +} \ No newline at end of file From 6b3debd32cece2c901619f3d6dc65df57ffc0310 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:53:18 -0500 Subject: [PATCH 130/218] Reorganization - fixed namespace naming mistake; Introduced visitor pattern over the Messages types. --- .../Generation/UnitTestFeatureGenerator.cs | 2 +- .../AttachmentAddedEventWrapper.cs | 2 +- .../CucumberMessagEnumConverter.cs | 2 +- .../CucumberMessageBroker.cs | 2 +- .../CucumberMessageExtensions.cs | 95 ++ .../CucumberMessageFactory.cs | 2 +- .../CucumberMessagePublisher.cs | 2 +- .../CucumberMessageTransformer.cs | 2 +- .../CucumberMessageVisitor.cs | 186 ++++ .../CucumberMessage_TraversalVisitorBase.cs | 811 ++++++++++++++++++ .../FeatureEventProcessor.cs | 2 +- .../FeatureLevelCucumberMessages.cs | 2 +- .../FileExtensionToMIMETypeMap.cs | 2 +- .../CucumberMesssages/HookStepProcessor.cs | 2 +- .../ICucumberMessagePublisher.cs | 2 +- .../CucumberMesssages/ICucumberMessageSink.cs | 2 +- .../ICucumberMessageVisitor.cs | 66 ++ Reqnroll/CucumberMesssages/IStepProcessor.cs | 2 +- .../CucumberMesssages/IdGeneratorFactory.cs | 2 +- .../CucumberMesssages/NdjsonSerializer.cs | 2 +- .../OutputAddedEventWrapper.cs | 2 +- .../ReqnrollCucumberMessage.cs | 2 +- .../ScenarioEventProcessor.cs | 2 +- .../ScenarioStepProcessor.cs | 2 +- .../CucumberMesssages/StepProcessorBase.cs | 2 +- Reqnroll/FeatureInfo.cs | 2 +- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTests.cs | 15 +- .../CucumberMessagesValidator.cs | 94 +- 29 files changed, 1184 insertions(+), 129 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs create mode 100644 Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs create mode 100644 Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 1e17b09b3..0b0ca86b6 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -5,7 +5,7 @@ using System.Reflection; using Gherkin.CucumberMessages; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs index 25c2eb53c..44f371407 100644 --- a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs +++ b/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class AttachmentAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs index c5fb0457e..3722e8c4e 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Cucumber.Messages +namespace Reqnroll.CucumberMessages { internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs index 87fe56f4f..485667467 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessageBroker diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs new file mode 100644 index 000000000..6cb1a658c --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs @@ -0,0 +1,95 @@ +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages +{ + public static class CucumberMessageExtensions + { + public static List MessagesWithIds = new(){ typeof(Background), + typeof(Examples), + typeof(Hook), + typeof(ParameterType), + typeof(Pickle), + typeof(PickleStep), + typeof(Rule), + typeof(Scenario), + typeof(Step), + typeof(StepDefinition), + typeof(TableRow), + typeof(Tag), + typeof(TestCase), + typeof(TestCaseStarted), + typeof(TestStep) + }; + + public static bool HasId(this object element) + { + return MessagesWithIds.Contains(element.GetType()); + } + public static string Id(this object message) + { + return message switch + { + Background bgd => bgd.Id, + Examples ex => ex.Id, + Hook hook => hook.Id, + ParameterType pt => pt.Id, + Pickle p => p.Id, + PickleStep ps => ps.Id, + Rule r => r.Id, + Scenario sc => sc.Id, + Step st => st.Id, + StepDefinition sd => sd.Id, + TableRow tr => tr.Id, + Tag tag => tag.Id, + TestCase tc => tc.Id, + TestCaseStarted tcs => tcs.Id, + TestStep ts => ts.Id, + _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") + }; + } + public static List EnvelopeContentTypes = new() + { + typeof(Attachment), + typeof(GherkinDocument), + typeof(Hook), + typeof(Meta), + typeof(ParameterType), + typeof(ParseError), + typeof(Pickle), + typeof(Source), + typeof(StepDefinition), + typeof(TestCase), + typeof(TestCaseFinished), + typeof(TestCaseStarted), + typeof(TestRunFinished), + typeof(TestRunStarted), + typeof(TestStepFinished), + typeof(TestStepStarted), + typeof(UndefinedParameterType) + }; + public static object Content(this Envelope envelope) + { + object result = null; + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + else if (envelope.Hook != null) { result = envelope.Hook; } + else if (envelope.Meta != null) { result = envelope.Meta; } + else if (envelope.ParameterType != null) { result = envelope.ParameterType; } + else if (envelope.ParseError != null) { result = envelope.ParseError; } + else if (envelope.Pickle != null) { result = envelope.Pickle; } + else if (envelope.Source != null) { result = envelope.Source; } + else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } + else if (envelope.TestCase != null) { result = envelope.TestCase; } + else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } + else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } + return result; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 3660b1364..e5b96dd01 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class CucumberMessageFactory { diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index 28916baa2..b6f18124c 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -17,7 +17,7 @@ using System.Linq; using System.Diagnostics; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs index e6e8135a3..c4e019639 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { /// /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace diff --git a/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs b/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs new file mode 100644 index 000000000..c8d1c9274 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages; + +public class CucumberMessageVisitor +{ + public static void Accept(ICucumberMessageVisitor visitor, object message) + { + switch (message) + { + // Existing cases + case Envelope envelope: + visitor.Visit(envelope); + break; + case Attachment attachment: + visitor.Visit(attachment); + break; + case GherkinDocument gherkinDocument: + visitor.Visit(gherkinDocument); + break; + case Feature feature: + visitor.Visit(feature); + break; + case FeatureChild featureChild: + visitor.Visit(featureChild); + break; + case Rule rule: + visitor.Visit(rule); + break; + case RuleChild ruleChild: + visitor.Visit(ruleChild); + break; + case Background background: + visitor.Visit(background); + break; + case Scenario scenario: + visitor.Visit(scenario); + break; + case Examples examples: + visitor.Visit(examples); + break; + case Step step: + visitor.Visit(step); + break; + case TableRow tableRow: + visitor.Visit(tableRow); + break; + case TableCell tableCell: + visitor.Visit(tableCell); + break; + case Tag tag: + visitor.Visit(tag); + break; + case Pickle pickle: + visitor.Visit(pickle); + break; + case PickleStep pickleStep: + visitor.Visit(pickleStep); + break; + case PickleStepArgument pickleStepArgument: + visitor.Visit(pickleStepArgument); + break; + case PickleTable pickleTable: + visitor.Visit(pickleTable); + break; + case PickleTableRow pickleTableRow: + visitor.Visit(pickleTableRow); + break; + case PickleTableCell pickleTableCell: + visitor.Visit(pickleTableCell); + break; + case PickleTag pickleTag: + visitor.Visit(pickleTag); + break; + case TestCase testCase: + visitor.Visit(testCase); + break; + case TestCaseStarted testCaseStarted: + visitor.Visit(testCaseStarted); + break; + case TestCaseFinished testCaseFinished: + visitor.Visit(testCaseFinished); + break; + case TestStep testStep: + visitor.Visit(testStep); + break; + case TestStepStarted testStepStarted: + visitor.Visit(testStepStarted); + break; + case TestStepFinished testStepFinished: + visitor.Visit(testStepFinished); + break; + case TestStepResult testStepResult: + visitor.Visit(testStepResult); + break; + case Hook hook: + visitor.Visit(hook); + break; + case StepDefinition stepDefinition: + visitor.Visit(stepDefinition); + break; + case ParameterType parameterType: + visitor.Visit(parameterType); + break; + case UndefinedParameterType undefinedParameterType: + visitor.Visit(undefinedParameterType); + break; + case SourceReference sourceReference: + visitor.Visit(sourceReference); + break; + case Duration duration: + visitor.Visit(duration); + break; + case Timestamp timestamp: + visitor.Visit(timestamp); + break; + case Io.Cucumber.Messages.Types.Exception exception: + visitor.Visit(exception); + break; + case Meta meta: + visitor.Visit(meta); + break; + case Product product: + visitor.Visit(product); + break; + case Ci ci: + visitor.Visit(ci); + break; + case Git git: + visitor.Visit(git); + break; + case Source source: + visitor.Visit(source); + break; + case Comment comment: + visitor.Visit(comment); + break; + case Io.Cucumber.Messages.Types.DataTable dataTable: + visitor.Visit(dataTable); + break; + case DocString docString: + visitor.Visit(docString); + break; + case Group group: + visitor.Visit(group); + break; + case JavaMethod javaMethod: + visitor.Visit(javaMethod); + break; + case JavaStackTraceElement javaStackTraceElement: + visitor.Visit(javaStackTraceElement); + break; + case Location location: + visitor.Visit(location); + break; + case ParseError parseError: + visitor.Visit(parseError); + break; + case PickleDocString pickleDocString: + visitor.Visit(pickleDocString); + break; + case StepDefinitionPattern stepDefinitionPattern: + visitor.Visit(stepDefinitionPattern); + break; + case StepMatchArgument stepMatchArgument: + visitor.Visit(stepMatchArgument); + break; + case StepMatchArgumentsList stepMatchArgumentsList: + visitor.Visit(stepMatchArgumentsList); + break; + case TestRunStarted testRunStarted: + visitor.Visit(testRunStarted); + break; + case TestRunFinished testRunFinished: + visitor.Visit(testRunFinished); + break; + + default: + throw new ArgumentException($"Unsupported message type:{message.GetType().Name}", nameof(message)); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs new file mode 100644 index 000000000..0bb4ac6d7 --- /dev/null +++ b/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs @@ -0,0 +1,811 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages +{ + public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor + { + private void Accept(object message) + { + if (message != null) CucumberMessageVisitor.Accept(this, message); + } + + public virtual void Visit(Envelope envelope) + { + OnVisiting(envelope); + Accept(envelope.Content()); + OnVisited(envelope); + } + + public virtual void Visit(Attachment attachment) + { + OnVisiting(attachment); + OnVisited(attachment); + } + + public virtual void Visit(GherkinDocument gherkinDocument) + { + OnVisiting(gherkinDocument); + + if (gherkinDocument.Feature != null) + Accept(gherkinDocument.Feature); + + OnVisited(gherkinDocument); + } + + public virtual void Visit(Feature feature) + { + OnVisiting(feature); + foreach (var featureChild in feature.Children) + { + Accept(featureChild); + } + OnVisited(feature); + } + + public virtual void Visit(FeatureChild featureChild) + { + OnVisiting(featureChild); + if (featureChild.Rule != null) + Accept(featureChild.Rule); + else if (featureChild.Background != null) + Accept(featureChild.Background); + else if (featureChild.Scenario != null) + Accept(featureChild.Scenario); + OnVisited(featureChild); + } + + public virtual void Visit(Rule rule) + { + OnVisiting(rule); + foreach (var ruleChild in rule.Children) + { + Accept(ruleChild); + } + foreach (var tag in rule.Tags) + { + Accept(tag); + } + OnVisited(rule); + } + + public virtual void Visit(RuleChild ruleChild) + { + OnVisiting(ruleChild); + if (ruleChild.Background != null) + Accept(ruleChild.Background); + else if (ruleChild.Scenario != null) + Accept(ruleChild.Scenario); + OnVisited(ruleChild); + } + + public virtual void Visit(Background background) + { + OnVisiting(background); + Accept(background.Location); + foreach (var step in background.Steps) + { + Accept(step); + } + OnVisited(background); + } + + public virtual void Visit(Scenario scenario) + { + OnVisiting(scenario); + Accept(scenario.Location); + foreach (var tag in scenario.Tags) + { + Accept(tag); + } + foreach (var step in scenario.Steps) + { + Accept(step); + } + foreach (var example in scenario.Examples) + { + Accept(example); + } + OnVisited(scenario); + } + + public virtual void Visit(Examples examples) + { + OnVisiting(examples); + Accept(examples.Location); + foreach (var tag in examples.Tags) + { + Accept(tag); + } + Accept(examples.TableHeader); + foreach (var tableRow in examples.TableBody) + { + Accept(tableRow); + } + OnVisited(examples); + } + + public virtual void Visit(Step step) + { + OnVisiting(step); + Accept(step.Location); + Accept(step.DocString); + Accept(step.DataTable); + OnVisited(step); + } + + public virtual void Visit(TableRow tableRow) + { + OnVisiting(tableRow); + Accept(tableRow.Location); + foreach (var tableCell in tableRow.Cells) + { + Accept(tableCell); + } + OnVisited(tableRow); + } + + public virtual void Visit(TableCell tableCell) + { + OnVisiting(tableCell); + Accept(tableCell.Location); + OnVisited(tableCell); + } + + public virtual void Visit(Tag tag) + { + OnVisiting(tag); + Accept(tag.Location); + OnVisited(tag); + } + + public virtual void Visit(Pickle pickle) + { + OnVisiting(pickle); + foreach (var pickleStep in pickle.Steps) + { + Accept(pickleStep); + } + foreach (var tag in pickle.Tags) + { + Accept(tag); + } + OnVisited(pickle); + } + + public virtual void Visit(PickleStep pickleStep) + { + OnVisiting(pickleStep); + Accept(pickleStep.Argument); + OnVisited(pickleStep); + } + + public virtual void Visit(PickleStepArgument pickleStepArgument) + { + OnVisiting(pickleStepArgument); + if (pickleStepArgument.DataTable != null) + Accept(pickleStepArgument.DataTable); + else if (pickleStepArgument.DocString != null) + Accept(pickleStepArgument.DocString); + OnVisited(pickleStepArgument); + } + + public virtual void Visit(PickleTable pickleTable) + { + OnVisiting(pickleTable); + foreach (var pickleTableRow in pickleTable.Rows) + { + Accept(pickleTableRow); + } + OnVisited(pickleTable); + } + + public virtual void Visit(PickleTableRow pickleTableRow) + { + OnVisiting(pickleTableRow); + foreach (var pickleTableCell in pickleTableRow.Cells) + { + Accept(pickleTableCell); + } + OnVisited(pickleTableRow); + } + + public virtual void Visit(PickleTableCell pickleTableCell) + { + OnVisiting(pickleTableCell); + OnVisited(pickleTableCell); + } + + public virtual void Visit(PickleTag pickleTag) + { + OnVisiting(pickleTag); + OnVisited(pickleTag); + } + + public virtual void Visit(TestCase testCase) + { + OnVisiting(testCase); + foreach (var step in testCase.TestSteps) + { + Accept(step); + } + OnVisited(testCase); + } + + public virtual void Visit(TestCaseStarted testCaseStarted) + { + OnVisiting(testCaseStarted); + Accept(testCaseStarted.Timestamp); + OnVisited(testCaseStarted); + } + + public virtual void Visit(TestCaseFinished testCaseFinished) + { + OnVisiting(testCaseFinished); + Accept(testCaseFinished.Timestamp); + OnVisited(testCaseFinished); + } + + public virtual void Visit(TestStep testStep) + { + OnVisiting(testStep); + foreach (var argumentList in testStep.StepMatchArgumentsLists) + { + Accept(argumentList); + } + OnVisited(testStep); + } + + public virtual void Visit(TestStepStarted testStepStarted) + { + OnVisiting(testStepStarted); + Accept(testStepStarted.Timestamp); + OnVisited(testStepStarted); + } + + public virtual void Visit(TestStepFinished testStepFinished) + { + OnVisiting(testStepFinished); + Accept(testStepFinished.TestStepResult); + Accept(testStepFinished.Timestamp); + OnVisited(testStepFinished); + } + + public virtual void Visit(TestStepResult testStepResult) + { + OnVisiting(testStepResult); + Accept(testStepResult.Duration); + Accept(testStepResult.Exception); + OnVisited(testStepResult); + } + + public virtual void Visit(Hook hook) + { + OnVisiting(hook); + Accept(hook.SourceReference); + OnVisited(hook); + } + + public virtual void Visit(StepDefinition stepDefinition) + { + OnVisiting(stepDefinition); + Accept(stepDefinition.Pattern); + Accept(stepDefinition.SourceReference); + OnVisited(stepDefinition); + } + + public virtual void Visit(ParameterType parameterType) + { + OnVisiting(parameterType); + Accept(parameterType.SourceReference); + OnVisited(parameterType); + } + + public virtual void Visit(UndefinedParameterType undefinedParameterType) + { + OnVisiting(undefinedParameterType); + OnVisited(undefinedParameterType); + } + + public virtual void Visit(SourceReference sourceReference) + { + OnVisiting(sourceReference); + if (sourceReference.Location != null) Accept(sourceReference.Location); + else if (sourceReference.JavaMethod != null) Accept(sourceReference.JavaMethod); + else if (sourceReference.JavaStackTraceElement != null) Accept(sourceReference.JavaStackTraceElement); + OnVisited(sourceReference); + } + + public virtual void Visit(Duration duration) + { + OnVisiting(duration); + OnVisited(duration); + } + + public virtual void Visit(Timestamp timestamp) + { + OnVisiting(timestamp); + OnVisited(timestamp); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.Exception exception) + { + OnVisiting(exception); + OnVisited(exception); + } + + public virtual void Visit(Meta meta) + { + OnVisiting(meta); + Accept(meta.Implementation); + Accept(meta.Runtime); + Accept(meta.Os); + Accept(meta.Cpu); + Accept(meta.Ci); + OnVisited(meta); + } + + public virtual void Visit(Product product) + { + OnVisiting(product); + OnVisited(product); + } + + public virtual void Visit(Ci ci) + { + OnVisiting(ci); + Accept(ci.Git); + OnVisited(ci); + } + + public virtual void Visit(Git git) + { + OnVisiting(git); + OnVisited(git); + } + + public virtual void Visit(Source source) + { + OnVisiting(source); + OnVisited(source); + } + + public virtual void Visit(Comment comment) + { + OnVisiting(comment); + Accept(comment.Location); + OnVisited(comment); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) + { + OnVisiting(dataTable); + Accept(dataTable.Location); + foreach (var row in dataTable.Rows) + { + Accept(row); + } + OnVisited(dataTable); + } + + public virtual void Visit(DocString docString) + { + OnVisiting(docString); + Accept(docString.Location); + OnVisited(docString); + } + + public virtual void Visit(Group group) + { + OnVisiting(group); + foreach (var child in group.Children) + { + Accept(child); + } + OnVisited(group); + } + + public virtual void Visit(JavaMethod javaMethod) + { + OnVisiting(javaMethod); + OnVisited(javaMethod); + } + + public virtual void Visit(JavaStackTraceElement javaStackTraceElement) + { + OnVisiting(javaStackTraceElement); + OnVisited(javaStackTraceElement); + } + + public virtual void Visit(Location location) + { + OnVisiting(location); + OnVisited(location); + } + + public virtual void Visit(ParseError parseError) + { + OnVisiting(parseError); + Accept(parseError.Source); + OnVisited(parseError); + } + + public virtual void Visit(PickleDocString pickleDocString) + { + OnVisiting(pickleDocString); + OnVisited(pickleDocString); + } + + public virtual void Visit(StepDefinitionPattern stepDefinitionPattern) + { + OnVisiting(stepDefinitionPattern); + OnVisited(stepDefinitionPattern); + } + + public virtual void Visit(StepMatchArgument stepMatchArgument) + { + OnVisiting(stepMatchArgument); + Accept(stepMatchArgument.Group); + OnVisited(stepMatchArgument); + } + + public virtual void Visit(StepMatchArgumentsList stepMatchArgumentsList) + { + OnVisiting(stepMatchArgumentsList); + foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments) + { + Accept(stepMatchArgument); + } + OnVisited(stepMatchArgumentsList); + } + + public virtual void Visit(TestRunStarted testRunStarted) + { + OnVisiting(testRunStarted); + Accept(testRunStarted.Timestamp); + OnVisited(testRunStarted); + } + + public virtual void Visit(TestRunFinished testRunFinished) + { + OnVisiting(testRunFinished); + Accept(testRunFinished.Timestamp); + Accept(testRunFinished.Exception); + OnVisited(testRunFinished); + } + + public virtual void OnVisiting(Attachment attachment) + { } + + public virtual void OnVisited(Attachment attachment) + { } + + public virtual void OnVisiting(Envelope envelope) + { } + + public virtual void OnVisited(Envelope envelope) + { } + + public virtual void OnVisiting(Feature feature) + { } + + public virtual void OnVisited(Feature feature) + { } + + public virtual void OnVisiting(FeatureChild featureChild) + { } + + public virtual void OnVisited(FeatureChild featureChild) + { } + + public virtual void OnVisiting(Examples examples) + { } + + public virtual void OnVisited(Examples examples) + { } + + public virtual void OnVisiting(Step step) + { } + + public virtual void OnVisited(Step step) + { } + + public virtual void OnVisiting(TableRow tableRow) + { } + + public virtual void OnVisited(TableRow tableRow) + { } + + public virtual void OnVisiting(TableCell tableCell) + { } + + public virtual void OnVisited(TableCell tableCell) + { } + + public virtual void OnVisiting(Tag tag) + { } + + public virtual void OnVisited(Tag tag) + { } + + public virtual void OnVisiting(Pickle pickle) + { } + + public virtual void OnVisited(Pickle pickle) + { } + + public virtual void OnVisiting(PickleStep pickleStep) + { } + + public virtual void OnVisited(PickleStep pickleStep) + { } + + public virtual void OnVisiting(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisited(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisiting(PickleTable pickleTable) + { } + + public virtual void OnVisited(PickleTable pickleTable) + { } + + public virtual void OnVisiting(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisited(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisiting(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisited(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisiting(PickleTag pickelTag) + { } + + public virtual void OnVisited(PickleTag pickelTag) + { } + + public virtual void OnVisiting(Rule rule) + { } + + public virtual void OnVisited(Rule rule) + { } + + public virtual void OnVisiting(RuleChild ruleChild) + { } + + public virtual void OnVisited(RuleChild ruleChild) + { } + + public virtual void OnVisiting(Background background) + { } + + public virtual void OnVisited(Background background) + { } + + public virtual void OnVisiting(Scenario scenario) + { } + + public virtual void OnVisited(Scenario scenario) + { } + + public virtual void OnVisiting(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisited(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisiting(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisited(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisiting(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisited(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisiting(TestStep testStep) + { } + + public virtual void OnVisited(TestStep testStep) + { } + + public virtual void OnVisiting(TestStepFinished testStepFinished) + { } + + public virtual void OnVisited(TestStepFinished testStepFinished) + { } + + public virtual void OnVisiting(TestStepStarted testStepStarted) + { } + + public virtual void OnVisited(TestStepStarted testStepStarted) + { } + + public virtual void OnVisiting(TestStepResult testStepResult) + { } + + public virtual void OnVisited(TestStepResult testStepResult) + { } + + public virtual void OnVisiting(TestCase testCase) + { } + + public virtual void OnVisited(TestCase testCase) + { } + + public virtual void OnVisiting(StepDefinition stepDefinition) + { } + + public virtual void OnVisited(StepDefinition stepDefinition) + { } + + public virtual void OnVisiting(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisited(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisiting(ParameterType parameterType) + { } + + public virtual void OnVisited(ParameterType parameterType) + { } + + public virtual void OnVisiting(ParseError parseError) + { } + + public virtual void OnVisited(ParseError parseError) + { } + + public virtual void OnVisiting(Source source) + { } + + public virtual void OnVisited(Source source) + { } + + public virtual void OnVisiting(Hook hook) + { } + + public virtual void OnVisited(Hook hook) + { } + + public virtual void OnVisiting(Meta meta) + { } + + public virtual void OnVisited(Meta meta) + { } + + public virtual void OnVisiting(Ci ci) + { } + + public virtual void OnVisited(Ci ci) + { } + + public virtual void OnVisiting(Comment comment) + { } + + public virtual void OnVisited(Comment comment) + { } + + public virtual void OnVisiting(DocString docString) + { } + + public virtual void OnVisited(DocString docString) + { } + + public virtual void OnVisiting(Duration duration) + { } + + public virtual void OnVisited(Duration duration) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisiting(JavaMethod javaMethod) + { } + + public virtual void OnVisited(JavaMethod javaMethod) + { } + + public virtual void OnVisiting(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisited(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisiting(Location location) + { } + + public virtual void OnVisited(Location location) + { } + + public virtual void OnVisiting(Product product) + { } + + public virtual void OnVisited(Product product) + { } + + public virtual void OnVisiting(SourceReference sourceReference) + { } + + public virtual void OnVisited(SourceReference sourceReference) + { } + + public virtual void OnVisiting(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisited(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisiting(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisited(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisiting(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisited(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisiting(Timestamp timestamp) + { } + + public virtual void OnVisited(Timestamp timestamp) + { } + + public virtual void OnVisiting(Git git) + { } + + public virtual void OnVisited(Git git) + { } + + public virtual void OnVisiting(Group group) + { } + + public virtual void OnVisited(Group group) + { } + + public virtual void OnVisiting(PickleDocString pickleDocString) + { } + + public virtual void OnVisited(PickleDocString pickleDocString) + { } + + public virtual void OnVisiting(TestRunStarted testRunStarted) + { } + + public virtual void OnVisited(TestRunStarted testRunStarted) + { } + + public virtual void OnVisiting(TestRunFinished testRunFinished) + { } + + public virtual void OnVisited(TestRunFinished testRunFinished) + { } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 6f534827c..049968e47 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -13,7 +13,7 @@ using System.Linq; using System.Runtime.InteropServices; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class FeatureEventProcessor { diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs index bdf39518d..5a48bc4b3 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class FeatureLevelCucumberMessages { diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs index 29d96c701..6a6cc6ae0 100644 --- a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs +++ b/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public static class FileExtensionToMIMETypeMap { diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMesssages/HookStepProcessor.cs index 649d2f330..0754bde6f 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/HookStepProcessor.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class HookStepProcessor : StepProcessorBase { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs index 458747f4f..633ad6025 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessagePublisher { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs index 926e2cf1c..2c1219506 100644 --- a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface ICucumberMessageSink { diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs new file mode 100644 index 000000000..c0e1464c6 --- /dev/null +++ b/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs @@ -0,0 +1,66 @@ +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages; + +// This interface is used to support the implementation of an External Vistor pattern against the Cucumber Messages. +// Visitors impmlement this interface and then invoke it using the helper class below. + +public interface ICucumberMessageVisitor +{ + // Existing methods + void Visit(Envelope envelope); + void Visit(Attachment attachment); + void Visit(GherkinDocument gherkinDocument); + void Visit(Feature feature); + void Visit(FeatureChild featureChild); + void Visit(Rule rule); + void Visit(RuleChild ruleChild); + void Visit(Background background); + void Visit(Scenario scenario); + void Visit(Examples examples); + void Visit(Step step); + void Visit(TableRow tableRow); + void Visit(TableCell tableCell); + void Visit(Tag tag); + void Visit(Pickle pickle); + void Visit(PickleStep pickleStep); + void Visit(PickleStepArgument pickleStepArgument); + void Visit(PickleTable pickleTable); + void Visit(PickleTableRow pickleTableRow); + void Visit(PickleTableCell pickleTableCell); + void Visit(PickleTag pickleTag); + void Visit(TestCase testCase); + void Visit(TestCaseStarted testCaseStarted); + void Visit(TestCaseFinished testCaseFinished); + void Visit(TestStep testStep); + void Visit(TestStepStarted testStepStarted); + void Visit(TestStepFinished testStepFinished); + void Visit(TestStepResult testStepResult); + void Visit(Hook hook); + void Visit(StepDefinition stepDefinition); + void Visit(ParameterType parameterType); + void Visit(UndefinedParameterType undefinedParameterType); + void Visit(SourceReference sourceReference); + void Visit(Duration duration); + void Visit(Timestamp timestamp); + void Visit(Io.Cucumber.Messages.Types.Exception exception); + void Visit(Meta meta); + void Visit(Product product); + void Visit(Ci ci); + void Visit(Git git); + void Visit(Source source); + void Visit(Comment comment); + void Visit(Io.Cucumber.Messages.Types.DataTable dataTable); + void Visit(DocString docString); + void Visit(Group group); + void Visit(JavaMethod javaMethod); + void Visit(JavaStackTraceElement javaStackTraceElement); + void Visit(Location location); + void Visit(ParseError parseError); + void Visit(PickleDocString pickleDocString); + void Visit(StepDefinitionPattern stepDefinitionPattern); + void Visit(StepMatchArgument stepMatchArgument); + void Visit(StepMatchArgumentsList stepMatchArgumentsList); + void Visit(TestRunStarted testRunStarted); + void Visit(TestRunFinished testRunFinished); +} diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMesssages/IStepProcessor.cs index 667d02467..02ba5ed1b 100644 --- a/Reqnroll/CucumberMesssages/IStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/IStepProcessor.cs @@ -1,4 +1,4 @@ -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public interface IStepProcessor { diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs index 8ff9bc652..80bfbf10d 100644 --- a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs @@ -1,7 +1,7 @@ using Gherkin.CucumberMessages; using System; using System.Threading; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class IdGeneratorFactory { diff --git a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs index 4ea75d028..17954295a 100644 --- a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMesssages/NdjsonSerializer.cs @@ -2,7 +2,7 @@ using System; using System.Text.Json; -namespace Cucumber.Messages +namespace Reqnroll.CucumberMessages { /// /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs index bf05e99ae..62e5491a5 100644 --- a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs +++ b/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { internal class OutputAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs index d91af932d..12c6a3784 100644 --- a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs +++ b/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs @@ -1,7 +1,7 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class ReqnrollCucumberMessage { diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs index b8f8d9836..d20816a1e 100644 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs @@ -9,7 +9,7 @@ using System.Diagnostics; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class ScenarioEventProcessor diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 9f53ad4d2..13eb9e251 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class StepArgument { diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMesssages/StepProcessorBase.cs index af4834a6f..3638404b0 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMesssages/StepProcessorBase.cs @@ -1,6 +1,6 @@ using System; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class StepProcessorBase : IStepProcessor { diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index dcdbc9cea..5235ec7c7 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,6 +1,6 @@ using System; using System.Globalization; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Tracing; namespace Reqnroll diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 8e0810ccf..61d0c283b 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,7 +16,7 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; namespace Reqnroll.Infrastructure { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 818e94ac8..34f50d93e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,5 +1,5 @@ -using Cucumber.Messages; +using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using Newtonsoft.Json.Bson; @@ -47,19 +47,6 @@ public void SmokeTest() """); AddPassingStepBinding("When"); - AddBindingClass(""" - [Binding] - public class TaggedScenario - { - [AfterScenario()] - [Scope(Tag = "some-tag")] - public void FailingAfterHook() - { - throw new Exception("Exception in conditional hook"); - } - } - """); - ExecuteTests(); ShouldAllScenariosPass(); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 4cc8a13eb..a55c18009 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -1,6 +1,6 @@ using FluentAssertions; +using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; -using System.Diagnostics.Eventing.Reader; namespace CucumberMessages.CompatibilityTests { @@ -80,8 +80,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu { actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); - //todo: modify this to use lists of types from actual and expected and use FluentAssertions directly - foreach (var messageType in MessageExtensions.EnvelopeContentTypes) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) { @@ -100,93 +99,4 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu } } } - - internal static class MessageExtensions - { - public static List MessagesWithIds = new(){ typeof(Background), - typeof(Examples), - typeof(Hook), - typeof(ParameterType), - typeof(Pickle), - typeof(PickleStep), - typeof(Rule), - typeof(Scenario), - typeof(Step), - typeof(StepDefinition), - typeof(TableRow), - typeof(Tag), - typeof(TestCase), - typeof(TestCaseStarted), - typeof(TestStep) - }; - - internal static bool HasId(this object element) - { - return MessagesWithIds.Contains(element.GetType()); - } - internal static string Id(this object message) - { - return message switch - { - Background bgd => bgd.Id, - Examples ex => ex.Id, - Hook hook => hook.Id, - ParameterType pt => pt.Id, - Pickle p => p.Id, - PickleStep ps => ps.Id, - Rule r => r.Id, - Scenario sc => sc.Id, - Step st => st.Id, - StepDefinition sd => sd.Id, - TableRow tr => tr.Id, - Tag tag => tag.Id, - TestCase tc => tc.Id, - TestCaseStarted tcs => tcs.Id, - TestStep ts => ts.Id, - _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") - }; - } - internal static List EnvelopeContentTypes = new() - { - typeof(Attachment), - typeof(GherkinDocument), - typeof(Hook), - typeof(Meta), - typeof(ParameterType), - typeof(ParseError), - typeof(Pickle), - typeof(Source), - typeof(StepDefinition), - typeof(TestCase), - typeof(TestCaseFinished), - typeof(TestCaseStarted), - typeof(TestRunFinished), - typeof(TestRunStarted), - typeof(TestStepFinished), - typeof(TestStepStarted), - typeof(UndefinedParameterType) - }; - internal static object Content(this Envelope envelope) - { - object? result = null; - if (envelope.Attachment != null) { result = envelope.Attachment; } - else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } - else if (envelope.Hook != null) { result = envelope.Hook; } - else if (envelope.Meta != null) { result = envelope.Meta; } - else if (envelope.ParameterType != null) { result = envelope.ParameterType; } - else if (envelope.ParseError != null) { result = envelope.ParseError; } - else if (envelope.Pickle != null) { result = envelope.Pickle; } - else if (envelope.Source != null) { result = envelope.Source; } - else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } - else if (envelope.TestCase != null) { result = envelope.TestCase; } - else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } - else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } - else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } - else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } - else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } - else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } - else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } - return result!; - } - } } \ No newline at end of file From 6a16c0ac49079c3788830d38f590efb8fb75be7d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:52:32 -0500 Subject: [PATCH 131/218] The Validator fully builds out cross-reference tables using the visitor for all element types. --- .../CrossReferenceBuilder.cs | 304 ++++++++++++++++++ .../CucumberMessagesValidator.cs | 22 +- 2 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs new file mode 100644 index 000000000..b28ea5835 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs @@ -0,0 +1,304 @@ +using Io.Cucumber.Messages.Types; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using Reqnroll.CucumberMessages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + internal class CrossReferenceBuilder : CucumberMessage_TraversalVisitorBase + { + private Action buildCrossReferences; + public CrossReferenceBuilder(Action buildCrossReferences) + { + this.buildCrossReferences = buildCrossReferences; + } + + public override void OnVisiting(Attachment attachment) + { + buildCrossReferences(attachment); + base.OnVisiting(attachment); + } + public override void OnVisiting(Background background) + { + buildCrossReferences(background); + base.OnVisiting(background); + } + public override void OnVisiting(Ci ci) + { + buildCrossReferences(ci); + base.OnVisiting(ci); + } + public override void OnVisiting(Comment comment) + { + buildCrossReferences(comment); + base.OnVisiting(comment); + } + public override void OnVisiting(Duration duration) + { + buildCrossReferences(duration); + base.OnVisiting(duration); + } + + public override void OnVisiting(Examples examples) + { + buildCrossReferences(examples); + base.OnVisiting(examples); + } + public override void OnVisiting(Io.Cucumber.Messages.Types.Exception exception) + { + buildCrossReferences(exception); + base.OnVisiting(exception); + } + public override void OnVisiting(GherkinDocument gherkinDocument) + { + buildCrossReferences(gherkinDocument); + base.OnVisiting(gherkinDocument); + } + + public override void OnVisiting(Feature feature) + { + buildCrossReferences(feature); + base.OnVisiting(feature); + } + public override void OnVisiting(FeatureChild featureChild) + { + buildCrossReferences(featureChild); + base.OnVisiting(featureChild); + } + public override void OnVisiting(Git git) + { + buildCrossReferences(git); + base.OnVisiting(git); + } + public override void OnVisiting(Group group) + { + buildCrossReferences(group); + base.OnVisiting(group); + } + public override void OnVisiting(JavaMethod javaMethod) + { + buildCrossReferences(javaMethod); + base.OnVisiting(javaMethod); + } + public override void OnVisiting(JavaStackTraceElement javaStackTraceElement) + { + buildCrossReferences(javaStackTraceElement); + base.OnVisiting(javaStackTraceElement); + } + public override void OnVisiting(Location location) + { + buildCrossReferences(location); + base.OnVisiting(location); + } + public override void OnVisiting(Meta meta) + { + buildCrossReferences(meta); + base.OnVisiting(meta); + } + public override void OnVisiting(ParameterType parameterType) + { + buildCrossReferences(parameterType); + base.OnVisiting(parameterType); + } + public override void OnVisiting(ParseError parseError) + { + buildCrossReferences(parseError); + base.OnVisiting(parseError); + } + public override void OnVisiting(PickleStepArgument pickleStepArgument) + { + buildCrossReferences(pickleStepArgument); + base.OnVisiting(pickleStepArgument); + } + public override void OnVisiting(PickleTable pickleTable) + { + buildCrossReferences(pickleTable); + base.OnVisiting(pickleTable); + } + + public override void OnVisiting(PickleTableRow pickleTableRow) + { + buildCrossReferences(pickleTableRow); + base.OnVisiting(pickleTableRow); + } + public override void OnVisiting(PickleTableCell pickleTableCell) + { + buildCrossReferences(pickleTableCell); + base.OnVisiting(pickleTableCell); + } + public override void OnVisiting(PickleTag pickelTag) + { + buildCrossReferences(pickelTag); + base.OnVisiting(pickelTag); + } + public override void OnVisiting(Product product) + { + buildCrossReferences(product); + base.OnVisiting(product); + } + public override void OnVisiting(Rule rule) + { + buildCrossReferences(rule); + base.OnVisiting(rule); + } + public override void OnVisiting(RuleChild ruleChild) + { + buildCrossReferences(ruleChild); + base.OnVisiting(ruleChild); + } + public override void OnVisiting(Scenario scenario) + { + buildCrossReferences(scenario); + base.OnVisiting(scenario); + } + public override void OnVisiting(Source source) + { + buildCrossReferences(source); + base.OnVisiting(source); + } + public override void OnVisiting(SourceReference sourceReference) + { + buildCrossReferences(sourceReference); + base.OnVisiting(sourceReference); + } + public override void OnVisiting(Step step) + { + buildCrossReferences(step); + base.OnVisiting(step); + } + public override void OnVisiting(StepDefinition stepDefinition) + { + buildCrossReferences(stepDefinition); + base.OnVisiting(stepDefinition); + } + public override void OnVisiting(StepDefinitionPattern stepDefinitionPattern) + { + buildCrossReferences(stepDefinitionPattern); + base.OnVisiting(stepDefinitionPattern); + } + public override void OnVisiting(StepMatchArgument stepMatchArgument) + { + buildCrossReferences(stepMatchArgument); + base.OnVisiting(stepMatchArgument); + } + + public override void OnVisiting(StepMatchArgumentsList stepMatchArgumentsList) + { + buildCrossReferences(stepMatchArgumentsList); + base.OnVisiting(stepMatchArgumentsList); + } + public override void OnVisiting(DocString docString) + { + buildCrossReferences(docString); + base.OnVisiting(docString); + } + + public override void OnVisiting(DataTable dataTable) + { + buildCrossReferences(dataTable); + base.OnVisiting(dataTable); + } + + public override void OnVisiting(TableCell tableCell) + { + buildCrossReferences(tableCell); + base.OnVisiting(tableCell); + } + public override void OnVisiting(TableRow tableRow) + { + buildCrossReferences(tableRow); + base.OnVisiting(tableRow); + } + public override void OnVisiting(Tag tag) + { + buildCrossReferences(tag); + base.OnVisiting(tag); + } + public override void OnVisiting(TestCase testCase) + { + buildCrossReferences(testCase); + base.OnVisiting(testCase); + } + public override void OnVisiting(TestCaseFinished testCaseFinished) + { + buildCrossReferences(testCaseFinished); + base.OnVisiting(testCaseFinished); + } + + public override void OnVisiting(TestRunFinished testRunFinished) + { + buildCrossReferences(testRunFinished); + base.OnVisiting(testRunFinished); + } + + public override void OnVisiting(TestRunStarted testRunStarted) + { + buildCrossReferences(testRunStarted); + base.OnVisiting(testRunStarted); + } + + public override void OnVisiting(TestCaseStarted testCaseStarted) + { + buildCrossReferences(testCaseStarted); + base.OnVisiting(testCaseStarted); + } + public override void OnVisiting(TestStep testStep) + { + buildCrossReferences(testStep); + base.OnVisiting(testStep); + } + public override void OnVisiting(TestStepResult testStepResult) + { + buildCrossReferences(testStepResult); + base.OnVisiting(testStepResult); + } + public override void OnVisiting(TestStepFinished testStepFinished) + { + buildCrossReferences(testStepFinished); + base.OnVisiting(testStepFinished); + } + + public override void OnVisiting(TestStepStarted testStepStarted) + { + buildCrossReferences(testStepStarted); + base.OnVisiting(testStepStarted); + } + + public override void OnVisiting(Hook hook) + { + buildCrossReferences(hook); + base.OnVisiting(hook); + } + + public override void OnVisiting(Pickle pickle) + { + buildCrossReferences(pickle); + base.OnVisiting(pickle); + } + + public override void OnVisiting(PickleStep pickleStep) + { + buildCrossReferences(pickleStep); + base.OnVisiting(pickleStep); + } + public override void OnVisiting(PickleDocString pickleDocString) + { + buildCrossReferences(pickleDocString); + base.OnVisiting(pickleDocString); + } + public override void OnVisiting(Timestamp timestamp) + { + buildCrossReferences(timestamp); + base.OnVisiting(timestamp); + } + public override void OnVisiting(UndefinedParameterType undefinedParameterType) + { + buildCrossReferences(undefinedParameterType); + base.OnVisiting(undefinedParameterType); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index a55c18009..018459326 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -28,16 +28,20 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) { + var xrefBuilder = new CrossReferenceBuilder( msg => + { + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + }); foreach (var message in messages) { var msg = message.Content(); - InsertIntoElementsByType(msg, elementsByType); - - if (msg.HasId()) - { - InsertIntoElementsById(msg, elementsByID); - InsertIntoIDsByType(msg, IDsByType); - } + CucumberMessageVisitor.Accept(xrefBuilder, msg); } } private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) @@ -82,7 +86,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { - if ( actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) { throw new System.Exception($"{messageType} present in the actual but not in the expected."); } @@ -91,7 +95,7 @@ internal void ShouldPassBasicStructuralChecks(IEnumerable actual, IEnu throw new System.Exception($"{messageType} present in the expected but not in the actual."); } if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - { + { actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); } if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) From 689d790f9d09b96bf0720d4166b436f932792e7c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:57:49 -0500 Subject: [PATCH 132/218] Flushing out validator tests. Source comparison complete. Feature top-level properties comparison complete. (Feature Children next) --- .../CucumberCompatibilityTests.cs | 2 + .../CucumberMessagesValidator.cs | 77 +++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 34f50d93e..8c86d0ec5 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -199,6 +199,8 @@ public void CCKScenarios(string scenarioName) ExecuteTests(); var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + validator.ShouldPassBasicStructuralChecks(); + validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); ConfirmAllTestsRan(null); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 018459326..f4ab49814 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -4,7 +4,7 @@ namespace CucumberMessages.CompatibilityTests { - internal class CucumberMessagesValidator + public class CucumberMessagesValidator { private IEnumerable actualEnvelopes; private IEnumerable expectedEnvelopes; @@ -71,19 +71,84 @@ private static void InsertIntoElementsByType(object msg, Dictionary actual, IEnumerable expected) + + private void StepDefinitionsShouldBeComparable() + { + } + + private void PicklesShouldBeComparable() + { + } + + private void GherkinDocumentShouldBeComparable() + { + var actualGherkinDocument = actuals_elementsByType[typeof(GherkinDocument)].First().As(); + var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); + + //check top-level items first + // ignore Uri + // comments should be present in the same order; so a simple list comparison should work + actualGherkinDocument.Comments.Should().BeEquivalentTo(expectedGherkinDocument.Comments, options => options.Including(c => c.Text)); + FeatureShouldBeComparable(actualGherkinDocument.Feature, expectedGherkinDocument.Feature); + } + + private void FeatureShouldBeComparable(Feature actual, Feature expected) + { + // ingore Location elements and Id values + actual.Tags.Should().BeEquivalentTo(expected.Tags, options => options.Including(t => t.Name)); + + // CCK expects only the language code, not the language and culture codes + actual.Language.Split('-')[0].Should().Be(expected.Language); + actual.Name.Should().Be(expected.Name); + actual.Description.Replace("\r\n", "\n").Should().Be(expected.Description.Replace("\r\n", "\n")); + actual.Keyword.Should().Be(expected.Keyword); + // expecting that the children are in the same order + + + } + + private void SourceContentShouldBeIdentical() + { + var actualSource = actuals_elementsByType[typeof(Source)].First().As(); + var expectedSource = expecteds_elementsByType[typeof(Source)].First().As(); + actualSource.Data.Replace("\r\n", "\n").Should().Be(expectedSource.Data.Replace("\r\n", "\n")); + actualSource.MediaType.Should().Be(expectedSource.MediaType); + } + + public void ResultShouldPassBasicSanityChecks() + { + EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + } + + private void EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook() + { + } + + private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() + { + } + + public void ShouldPassBasicStructuralChecks() { + var actual = actualEnvelopes; + var expected = expectedEnvelopes; actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) From 60f3ccf8aee7f29b222b296a03101f57128a6c4d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:02:11 -0500 Subject: [PATCH 133/218] Source and GherkinDoc now using FA. Fixed namespace mispelling in FileSinkPlugin (to match changes made in previous commit). Temp fix: updated plug-in version reference to 2.2.0-local; need a way to fetch this from assembly. --- .../FileSinkPlugin.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 2 +- .../CucumberCompatibilityTests.cs | 2 +- .../CucumberMessagesValidator.cs | 106 +++++++++++++----- ...FluentAssertionsCustomStringComparisons.cs | 29 +++++ ...ionCucumberMessagePropertySelectionRule.cs | 44 ++++++++ 6 files changed, 155 insertions(+), 30 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index 8807fde2a..f2602a1d4 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -1,4 +1,4 @@ -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; using Io.Cucumber.Messages; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 0b3f95052..0fddf4667 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -14,7 +14,7 @@ public class CucumberCompatibilityTestBase : SystemTestBase protected void AddCucumberMessagePlugIn() { - _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.0-local"); + _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.1-local"); } protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 8c86d0ec5..e67ad6564 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -57,7 +57,7 @@ public void CucumberMessagesInteropWithExternalData() { // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages AddCucumberMessagePlugIn(); - _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.0-local"); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.2.0-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index f4ab49814..14044e653 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; +using System.ComponentModel.Design; namespace CucumberMessages.CompatibilityTests { @@ -16,6 +17,7 @@ public class CucumberMessagesValidator private Dictionary> expecteds_elementsByType = new(); private Dictionary> actuals_elementsByID = new(); private Dictionary> expecteds_elementsByID = new(); + private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) { @@ -25,10 +27,69 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options + // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances + .ComparingByValue() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + // Using a custom Property Selector so that we can ignore the following properties (Id, Uri, and Location); these will always be different + .Using(FA_CustomCucumberMessagesPropertySelector) + // Using a custom string comparison to ignore the differences in platform line endings + .Using(new FluentAssertionsCustomStringComparisons()) + .AllowingInfiniteRecursion() + .RespectingRuntimeTypes() + ); } private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) { - var xrefBuilder = new CrossReferenceBuilder( msg => + var xrefBuilder = new CrossReferenceBuilder(msg => { InsertIntoElementsByType(msg, elementsByType); @@ -74,11 +135,11 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); - //check top-level items first - // ignore Uri - // comments should be present in the same order; so a simple list comparison should work - actualGherkinDocument.Comments.Should().BeEquivalentTo(expectedGherkinDocument.Comments, options => options.Including(c => c.Text)); - FeatureShouldBeComparable(actualGherkinDocument.Feature, expectedGherkinDocument.Feature); - } - - private void FeatureShouldBeComparable(Feature actual, Feature expected) - { - // ingore Location elements and Id values - actual.Tags.Should().BeEquivalentTo(expected.Tags, options => options.Including(t => t.Name)); - - // CCK expects only the language code, not the language and culture codes - actual.Language.Split('-')[0].Should().Be(expected.Language); - actual.Name.Should().Be(expected.Name); - actual.Description.Replace("\r\n", "\n").Should().Be(expected.Description.Replace("\r\n", "\n")); - actual.Keyword.Should().Be(expected.Keyword); - // expecting that the children are in the same order - + actualGherkinDocument.Should().BeEquivalentTo(expectedGherkinDocument, options => options + .Using(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(inf => inf.Path.EndsWith("Language")) + .WithTracing()); } + private void SourceContentShouldBeIdentical() { - var actualSource = actuals_elementsByType[typeof(Source)].First().As(); - var expectedSource = expecteds_elementsByType[typeof(Source)].First().As(); - actualSource.Data.Replace("\r\n", "\n").Should().Be(expectedSource.Data.Replace("\r\n", "\n")); - actualSource.MediaType.Should().Be(expectedSource.MediaType); + var actualSource = actuals_elementsByType[typeof(Source)].First() as Source; + var expectedSource = expecteds_elementsByType[typeof(Source)].First() as Source; + + actualSource.Should().BeEquivalentTo(expectedSource, options => options.WithTracing() ); } public void ResultShouldPassBasicSanityChecks() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs new file mode 100644 index 000000000..6b09f6b55 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs @@ -0,0 +1,29 @@ +using FluentAssertions.Equivalency; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests +{ + public class FluentAssertionsCustomStringComparisons : IEqualityComparer + { + public bool Equals(string? x, string? y) + { + if (x == null && y == null) return true; + if (x == null || y == null) return false; + + x = x.Replace("\r\n", "\n"); + y = y.Replace("\r\n", "\n"); + return x.Equals(y); + } + + public int GetHashCode([DisallowNull] string obj) + { + return obj.GetHashCode(); + } + + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs new file mode 100644 index 000000000..cd1477c40 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Cucumber.Messages; +using FluentAssertions; +using FluentAssertions.Equivalency; + +namespace CucumberMessages.CompatibilityTests +{ + public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelectionRule + { + public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes) + { this.CucumberMessageTypes = CucumberMessageTypes; } + + public IEnumerable CucumberMessageTypes { get; } + + public bool IncludesMembers => false; + + public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers, MemberSelectionContext context) + { + if (CucumberMessageTypes.Contains(context.Type)) + { + var propertiesToSelect = new List(); + foreach (var prop in selectedMembers) + { + if (prop.Name != "Id" && prop.Name != "Location" && prop.Name != "Uri" ) + propertiesToSelect.Add(prop); + } + return propertiesToSelect; + } + else + { + return selectedMembers; + } + } + public override string ToString() + { + return "Include only relevant CucumberMessage properties"; + } + + } +} From 633bba577e56c748d42d05da3d5b01bfa41ea6e0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:16:30 -0500 Subject: [PATCH 134/218] Minimal and Hooks now passing Content and GD checks. Temp patch to Cucumber.Messages - with local build. Will need to revert to global nuget package when PR accepted by them. --- Reqnroll/CucumberMesssages/FeatureEventProcessor.cs | 8 +------- .../CucumberCompatibilityTests.cs | 8 ++++++-- .../CucumberMessages.CompatibilityTests.csproj | 1 + ...luentAsssertionCucumberMessagePropertySelectionRule.cs | 1 - 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 049968e47..8a8338527 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -1,17 +1,11 @@ -using Cucumber.Messages; -using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; -using Reqnroll.BoDi; using Reqnroll.Events; -using Reqnroll.Analytics; using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; namespace Reqnroll.CucumberMessages { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index e67ad6564..e6a3c89b2 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -218,7 +218,11 @@ private IEnumerable GetExpectedResults(string scenarioName) var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); - foreach(var json in expectedJsonText) yield return NdjsonSerializer.Deserialize(json); + foreach (var json in expectedJsonText) + { + var e = NdjsonSerializer.Deserialize(json); + yield return e; + }; } private IEnumerable GetActualResults(string scenarioName) @@ -229,7 +233,7 @@ private IEnumerable GetActualResults(string scenarioName) var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); - foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); + foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } } internal class FileSinkConfiguration diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 613faa012..6dcc8cd5e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -120,6 +120,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index cd1477c40..49cd2f0d4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Cucumber.Messages; using FluentAssertions; using FluentAssertions.Equivalency; From 0ed450f6fafbd3e8239d3d1e1d85c5a7bd10426d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:48:49 -0500 Subject: [PATCH 135/218] Source, GD, Pickle, StepDefinition and TestCase now working for Minimal. --- .../ScenarioStepProcessor.cs | 22 +++++++--- .../CucumberCompatibilityTests.cs | 12 ++++++ .../CucumberMessagesValidator.cs | 40 +++++++++++-------- ...ionCucumberMessagePropertySelectionRule.cs | 31 ++++++++++++-- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 13eb9e251..f9549d182 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -2,6 +2,7 @@ using Reqnroll.Assist; using Reqnroll.Bindings; using Reqnroll.Events; +using System; using System.Collections.Generic; using System.Linq; @@ -65,15 +66,26 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) Exception = stepFinishedEvent.ScenarioContext.TestError; } + var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); + var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => SimplifyTypeNames(p.Type.Name)).ToList() : new List(); StepArguments = Bound ? - stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => new StepArgument - { - Value = arg.ToString(), - Type = arg.GetType().Name - }).ToList() + argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); return Enumerable.Empty(); } + + private string SimplifyTypeNames(string name) + { + return name switch + { + "Int32" => "int", + "Long" => "long", + "Double" => "float", + "String" => "string", + "Boolean" => "bool", + _ => name + }; + } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index e6a3c89b2..c1175158d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -7,6 +7,7 @@ using System.Reflection; using FluentAssertions; using System.Text.Json; +using System.ComponentModel; namespace CucumberMessages.CompatibilityTests { @@ -235,6 +236,17 @@ private IEnumerable GetActualResults(string scenarioName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } + + [TestMethod] + public void tempTest() + { + var d1 = new Destination(true, "A", "AO"); + var d2 = new Destination(false, "B", "AO"); + + var a = new List() { d1, d2, d2 }; + var b = new List() { d2, d1 }; + a.Should().BeEquivalentTo(b); + } } internal class FileSinkConfiguration { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 14044e653..b7559f9eb 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -2,6 +2,7 @@ using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using System.ComponentModel.Design; +using FluentAssertions.Execution; namespace CucumberMessages.CompatibilityTests { @@ -79,7 +80,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() .ComparingByMembers() .ComparingByMembers() - // Using a custom Property Selector so that we can ignore the following properties (Id, Uri, and Location); these will always be different + // Using a custom Property Selector so that we can ignore the properties that are not comparable .Using(FA_CustomCucumberMessagesPropertySelector) // Using a custom string comparison to ignore the differences in platform line endings .Using(new FluentAssertionsCustomStringComparisons()) @@ -134,32 +135,44 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); } private void StepDefinitionsShouldBeComparable() { + CompareMessageType(); } private void PicklesShouldBeComparable() { + CompareMessageType(); } private void GherkinDocumentShouldBeComparable() { - var actualGherkinDocument = actuals_elementsByType[typeof(GherkinDocument)].First().As(); - var expectedGherkinDocument = expecteds_elementsByType[typeof(GherkinDocument)].First().As(); + CompareMessageType(); - actualGherkinDocument.Should().BeEquivalentTo(expectedGherkinDocument, options => options + } + + private void CompareMessageType() + { + var actual = actuals_elementsByType[typeof(T)].First().As(); + var expected = expecteds_elementsByType[typeof(T)].First().As(); + + actual.Should().BeEquivalentTo(expected, options => options .Using(ctx => { var actual = ctx.Subject.Split("-")[0]; @@ -168,16 +181,11 @@ private void GherkinDocumentShouldBeComparable() }) .When(inf => inf.Path.EndsWith("Language")) .WithTracing()); - } - private void SourceContentShouldBeIdentical() { - var actualSource = actuals_elementsByType[typeof(Source)].First() as Source; - var expectedSource = expecteds_elementsByType[typeof(Source)].First() as Source; - - actualSource.Should().BeEquivalentTo(expectedSource, options => options.WithTracing() ); + CompareMessageType(); } public void ResultShouldPassBasicSanityChecks() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 49cd2f0d4..8aac867c9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -8,10 +8,35 @@ namespace CucumberMessages.CompatibilityTests { + /// + /// Fluent Asssertion Cucumber Message Property Selection Rule + /// This class is used by Fluent Assertions to override which properties will be compared. + /// These properties will be skipped because they are not comparable across platforms + /// public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelectionRule { - public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes) - { this.CucumberMessageTypes = CucumberMessageTypes; } + // Properties to skip - this is the default set of properties that are not comparable across platforms + // Id: Ids are not assigned in the same order across platforms. + // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds: Ids are not assigned in the same order across platforms. + // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) + // Uri is not always comparable (eg, CCK refers to source file paths in typescript) + // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references + // Seconds and Nanos: time values are not comparable + // Start: Start refers to a column position in source code, which may not be comparable across platforms. + + // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), + // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) + private List PropertiesToSkip = new List() { "Id", "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Seconds", "Nanos", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "Start" }; + + public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) + { + this.CucumberMessageTypes = CucumberMessageTypes; + + if (proportiesToSkip != null) + { + PropertiesToSkip = proportiesToSkip.ToList(); + } + } public IEnumerable CucumberMessageTypes { get; } @@ -24,7 +49,7 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable(); foreach (var prop in selectedMembers) { - if (prop.Name != "Id" && prop.Name != "Location" && prop.Name != "Uri" ) + if (!PropertiesToSkip.Contains(prop.Name)) propertiesToSelect.Add(prop); } return propertiesToSelect; From 113d248dd94c3dad9acecb0e735039791df8723f Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:31:17 -0500 Subject: [PATCH 136/218] All Envelope comparisons working for Minimal. Partial for Hooks. --- .../CucumberCompatibilityTests.cs | 11 -- .../CucumberMessagesValidator.cs | 107 ++++++++++++++---- ...ionCucumberMessagePropertySelectionRule.cs | 11 +- 3 files changed, 94 insertions(+), 35 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index c1175158d..7fac2eb2e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -236,17 +236,6 @@ private IEnumerable GetActualResults(string scenarioName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } - - [TestMethod] - public void tempTest() - { - var d1 = new Destination(true, "A", "AO"); - var d2 = new Destination(false, "B", "AO"); - - var a = new List() { d1, d2, d2 }; - var b = new List() { d2, d1 }; - a.Should().BeEquivalentTo(b); - } } internal class FileSinkConfiguration { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index b7559f9eb..30a48b1bd 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -3,6 +3,7 @@ using Io.Cucumber.Messages.Types; using System.ComponentModel.Design; using FluentAssertions.Execution; +using System.Reflection; namespace CucumberMessages.CompatibilityTests { @@ -20,6 +21,12 @@ public class CucumberMessagesValidator private Dictionary> expecteds_elementsByID = new(); private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; + // Envelope types - these are the top level types in CucumberMessages + // Meta is excluded from the list as there is nothing there for us to compare + private readonly IEnumerable EnvelopeTypes = new Type[] { typeof(Attachment), typeof(GherkinDocument), typeof(Hook), typeof(ParameterType), typeof(Source), + typeof(StepDefinition), typeof(TestCase), typeof(TestCaseFinished), typeof(TestCaseStarted), typeof(TestRunFinished), + typeof(TestRunStarted), typeof(TestStepFinished), typeof(TestStepStarted), typeof(UndefinedParameterType) }; + public CucumberMessagesValidator(IEnumerable actual, IEnumerable expected) { actualEnvelopes = actual; @@ -30,7 +37,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options - // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances + // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances .ComparingByValue() .ComparingByMembers() .ComparingByMembers() @@ -80,12 +87,30 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() .ComparingByMembers() .ComparingByMembers() - // Using a custom Property Selector so that we can ignore the properties that are not comparable + + // Using a custom Property Selector so that we can ignore the properties that are not comparable .Using(FA_CustomCucumberMessagesPropertySelector) - // Using a custom string comparison to ignore the differences in platform line endings - .Using(new FluentAssertionsCustomStringComparisons()) + + // Using a custom string comparison to ignore the differences in platform line endings + .Using((ctx) => + { + var subject = ctx.Subject ?? string.Empty; + var expectation = ctx.Expectation ?? string.Empty; + subject = subject.Replace("\r\n", "\n"); + expectation = expectation.Replace("\r\n", "\n"); + subject.Should().Be(expectation); + }) + .WhenTypeIs() + + // A bit of trickery here to tell FluentAssertions that Timestamps are always equal + // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) + .Using(ctx => 1.Should().Be(1)) + .WhenTypeIs() + .AllowingInfiniteRecursion() - .RespectingRuntimeTypes() + //.RespectingRuntimeTypes() + .ExcludingFields() + .WithStrictOrdering() ); } private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) @@ -135,17 +160,18 @@ private static void InsertIntoElementsByType(object msg, Dictionary(); @@ -169,18 +195,57 @@ private void GherkinDocumentShouldBeComparable() private void CompareMessageType() { - var actual = actuals_elementsByType[typeof(T)].First().As(); - var expected = expecteds_elementsByType[typeof(T)].First().As(); + if (!expecteds_elementsByType.ContainsKey(typeof(T))) + return; - actual.Should().BeEquivalentTo(expected, options => options - .Using(ctx => + HashSet? actuals; + List actual; + List expected; + + if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) { - var actual = ctx.Subject.Split("-")[0]; - var expected = ctx.Expectation.Split("-")[0]; - actual.Should().Be(expected); - }) - .When(inf => inf.Path.EndsWith("Language")) - .WithTracing()); + actual = actuals.OfType().ToList(); + } + else + actual = new List(); + + expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; + + actual.Should().BeEquivalentTo(expected, options => options + .Using>(ctx => + { + if (ctx.SelectedNode.IsRoot) + { + var actualList = ctx.Subject; + var expectedList = ctx.Expectation; + + if (expectedList == null || !expectedList.Any()) + { + return; // If expected is null or empty, we don't need to check anything + } + + actualList.Should().NotBeNull(); + actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, + "actual collection should have at least as many items as expected"); + + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } + } + }) + .WhenTypeIs>() + // Using a custom string comparison that deals with ISO langauge codes when the property name ends with "Language" + .Using(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(inf => inf.Path.EndsWith("Language")) + .WithTracing()); } private void SourceContentShouldBeIdentical() diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 8aac867c9..8ec3541da 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -17,16 +17,21 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec { // Properties to skip - this is the default set of properties that are not comparable across platforms // Id: Ids are not assigned in the same order across platforms. - // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds: Ids are not assigned in the same order across platforms. + // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds, TestStepId, TestCaseStartedId, TestCaseId, WorkerId: Ids are not assigned in the same order across platforms. // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) // Uri is not always comparable (eg, CCK refers to source file paths in typescript) // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references - // Seconds and Nanos: time values are not comparable + // Duration: time values are not comparable // Start: Start refers to a column position in source code, which may not be comparable across platforms. + // FileName: CCK does not provide the file name of attachments but Reqnroll does + // ProtocolVersion, Implementation, Runtime, Cpu, Os, Ci: These properties of the Meta message are not comparable across platforms. // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) - private List PropertiesToSkip = new List() { "Id", "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Seconds", "Nanos", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "Start" }; + private List PropertiesToSkip = new List() { "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", + "Duration", "Start", "FileName", "Message", "Type", "StackTrace", + "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "TestCaseStartedId","TestStepId", "TestCaseId", "WorkerId", + "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci"}; public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) { From 3fa200ec39c827c19dcf3f83a981d2d6c4a46c7d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:20:52 -0500 Subject: [PATCH 137/218] Refactoring and minor improvements --- .../CucumberMessageFactory.cs | 41 +++- .../ScenarioStepProcessor.cs | 15 +- .../CucumberCompatibilityTests.cs | 48 ++--- .../CucumberMessagesValidator.cs | 180 ++++++++++-------- ...ionCucumberMessagePropertySelectionRule.cs | 10 +- 5 files changed, 169 insertions(+), 125 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index e5b96dd01..0ef49c3c9 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -36,7 +36,7 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar switch (stepState) { case ScenarioStepProcessor _: - var testStep = CucumberMessageFactory.ToTestStep(scenarioState, stepState as ScenarioStepProcessor); + var testStep = CucumberMessageFactory.ToPickleTestStep(scenarioState, stepState as ScenarioStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: @@ -65,10 +65,7 @@ internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scena } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { - var bindingSourceText = binding.SourceExpression; - var expressionType = binding.ExpressionType; - var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; - var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + StepDefinitionPattern stepDefinitionPattern = ToStepDefinitionPattern(binding); SourceReference sourceRef = ToSourceRef(binding); var result = new StepDefinition @@ -80,6 +77,14 @@ internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, return result; } + internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBinding binding) + { + var bindingSourceText = binding.SourceExpression; + var expressionType = binding.ExpressionType; + var stepDefinitionPatternType = expressionType switch { StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, _ => StepDefinitionPatternType.REGULAR_EXPRESSION }; + var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + return stepDefinitionPattern; + } internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { @@ -110,7 +115,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) + internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { bool bound = stepState.StepDefinitionId != null; @@ -137,7 +142,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) null, argument.Value ), - argument.Type); + NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); } internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) { @@ -165,7 +170,7 @@ internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) iDGenerator.GetNewId(), null, sourceRef, - hookBinding.IsScoped ? hookBinding.BindingScope.Tag : null + hookBinding.IsScoped ? $"@{hookBinding.BindingScope.Tag}" : null ); return result; } @@ -342,7 +347,25 @@ public static string Base64EncodeFile(string filePath) return Convert.ToBase64String(fileBytes); } - + + private static string NormalizePrimitiveTypeNamesToCucumberTypeNames(string name) + { + return name switch + { + "Int16" => "short", + "Int32" => "int", + "Int64" => "long", + "Single" => "float", + "Double" => "double", + "Byte" => "byte", + "String" => "string", + "Boolean" => "bool", + "Decimal" => "decimal", + "BigInteger" => "biginteger", + _ => name + }; + } + #endregion } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index f9549d182..7e6b53aca 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -67,25 +67,12 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); - var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => SimplifyTypeNames(p.Type.Name)).ToList() : new List(); + var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); StepArguments = Bound ? argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); return Enumerable.Empty(); } - - private string SimplifyTypeNames(string name) - { - return name switch - { - "Int32" => "int", - "Long" => "long", - "Double" => "float", - "String" => "string", - "Boolean" => "bool", - _ => name - }; - } } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7fac2eb2e..9eb24ec4d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using System.Text.Json; using System.ComponentModel; +using Reqnroll.TestProjectGenerator; namespace CucumberMessages.CompatibilityTests { @@ -173,33 +174,33 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) } [TestMethod] - [DataRow("attachments")] - [DataRow("minimal")] - [DataRow("cdata")] - [DataRow("pending")] - [DataRow("examples-tables")] - [DataRow("hooks")] - [DataRow("data-tables")] - [DataRow("parameter-types")] - [DataRow("skipped")] - [DataRow("undefined")] - [DataRow("unknown-parameter-type")] - [DataRow("rules")] - public void CCKScenarios(string scenarioName) + [DataRow("attachments", "Attachments")] + [DataRow("minimal", "minimal")] + [DataRow("cdata","cdata")] + [DataRow("pending", "Pending steps")] + [DataRow("examples-tables", "Examples Tables")] + [DataRow("hooks", "Hooks")] + [DataRow("data-tables", "Data Tables")] + [DataRow("parameter-types", "Parameter Types")] + [DataRow("skipped", "Skipping scenarios")] + [DataRow("undefined", "Undefined steps")] + [DataRow("unknown-parameter-type", "Parameter Types")] + [DataRow("rules", "Usage of a 'Rule'")] + public void CCKScenarios(string testName, string featureNameText) { AddCucumberMessagePlugIn(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); - scenarioName = scenarioName.Replace("-", "_"); + var featureFileName = testName.Replace("-", "_"); - AddFeatureFileFromResource($"{scenarioName}/{scenarioName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{scenarioName}/{scenarioName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - //AddBinaryFilesFromResource($"{scenarioName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + //AddBinaryFilesFromResource($"{testName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); - var validator = new CucumberMessagesValidator(GetActualResults(scenarioName).ToList(), GetExpectedResults(scenarioName).ToList()); + var validator = new CucumberMessagesValidator(GetActualResults(testName, featureNameText).ToList(), GetExpectedResults(testName, featureFileName).ToList()); validator.ShouldPassBasicStructuralChecks(); validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); @@ -214,10 +215,10 @@ private void AddUtilClassWithFileSystemPath() $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\CCK\"; }} }} "); } - private IEnumerable GetExpectedResults(string scenarioName) + private IEnumerable GetExpectedResults(string testName, string featureFileName) { var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{scenarioName}\\{scenarioName}.feature.ndjson")); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{testName}\\{featureFileName}.feature.ndjson")); foreach (var json in expectedJsonText) { @@ -226,13 +227,16 @@ private IEnumerable GetExpectedResults(string scenarioName) }; } - private IEnumerable GetActualResults(string scenarioName) + private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; - var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{scenarioName}.ndjson")); + + // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario + + var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{fileName}.ndjson")); foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 30a48b1bd..eed4580af 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -38,7 +38,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable options // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances - .ComparingByValue() + .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() @@ -91,6 +91,56 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable(ctx => + { + var actual = ctx.Subject.Split("-")[0]; + var expected = ctx.Expectation.Split("-")[0]; + actual.Should().Be(expected); + }) + .When(info => info.Path.EndsWith("Language")) + + // Using special logic to compare regular expression strings (ignoring the differences of the regex anchor characters) + .Using>(ctx => + { + var subjects = ctx.Subject; + var expectations = ctx.Expectation; + subjects.Should().HaveSameCount(expectations); + int count = subjects.Count; + for (int i = 0; i < count; i++) + { + string subject = subjects[i]; + string expectation = expectations[i]; + if ((subject.Length > 0 && subject[0] == '^') || (expectation.Length > 0 && expectation[0] == '^') || + (subject.Length > 0 && subject[subject.Length - 1] == '$') || (expectation.Length > 0 && expectation[expectation.Length - 1] == '$')) + { + // If the first or last character is '^' or '$', remove it before comparing + subject = subject.Length > 0 && subject[0] == '^' ? subject.Substring(1) : subject; + subject = subject.Length > 0 && subject[subject.Length - 1] == '$' ? subject.Substring(0, subject.Length - 1) : subject; + expectation = expectation.Length > 0 && expectation[0] == '^' ? expectation.Substring(1) : expectation; + expectation = expectation.Length > 0 && expectation[expectation.Length - 1] == '$' ? expectation.Substring(0, expectation.Length - 1) : expectation; + } + subject.Should().Be(expectation); + } + }) + .When(info => info.Path.EndsWith("RegularExpressions")) + + // Using special logic to ignore ParameterTypeName except when the value is one of the basic types + .Using((ctx) => + { + if (ctx.Expectation == "string" || ctx.Expectation == "int" || ctx.Expectation == "long" || ctx.Expectation == "double" || ctx.Expectation == "float" + || ctx.Expectation == "short" || ctx.Expectation == "byte" || ctx.Expectation == "biginteger") + { + ctx.Subject.Should().Be(ctx.Expectation); + } + // Any other ParameterTypeName should be ignored, including {word} (no .NET equivalent) and custom type names + else + { + 1.Should().Be(1); + } + }) + .When(info => info.Path.EndsWith("ParameterTypeName")) + // Using a custom string comparison to ignore the differences in platform line endings .Using((ctx) => { @@ -100,7 +150,39 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable() + .When(info => info.Path.EndsWith("Description") || info.Path.EndsWith("Text") || info.Path.EndsWith("Data")) + + // The list of hooks should contain at least as many items as the list of expected hooks + // Because Reqnroll does not support Tag Expressions, these are represented in RnR as multiple Hooks or multiple Tags on Hooks Binding methods + // which result in multiple Hook messages. + .Using>(ctx => + { + if (ctx.SelectedNode.IsRoot) + { + var actualList = ctx.Subject; + var expectedList = ctx.Expectation; + + if (expectedList == null || !expectedList.Any()) + { + return; // If expected is null or empty, we don't need to check anything + } + + actualList.Should().NotBeNull(); + actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, + "actual collection should have at least as many items as expected"); + + // Impossible to compare individual Hook messages (Ids aren't comparable, the Source references aren't compatible, + // and the Scope tags won't line up because the CCK uses tag expressions and RnR does not support them) +/* + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } +*/ } + }) + .WhenTypeIs>() // A bit of trickery here to tell FluentAssertions that Timestamps are always equal // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) @@ -171,28 +253,6 @@ public void ResultShouldPassAllComparisonTests() } } - - private void TestCasesShouldBeComparable() - { - CompareMessageType(); - } - - private void StepDefinitionsShouldBeComparable() - { - CompareMessageType(); - } - - private void PicklesShouldBeComparable() - { - CompareMessageType(); - } - - private void GherkinDocumentShouldBeComparable() - { - CompareMessageType(); - - } - private void CompareMessageType() { if (!expecteds_elementsByType.ContainsKey(typeof(T))) @@ -212,47 +272,9 @@ private void CompareMessageType() expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; actual.Should().BeEquivalentTo(expected, options => options - .Using>(ctx => - { - if (ctx.SelectedNode.IsRoot) - { - var actualList = ctx.Subject; - var expectedList = ctx.Expectation; - - if (expectedList == null || !expectedList.Any()) - { - return; // If expected is null or empty, we don't need to check anything - } - - actualList.Should().NotBeNull(); - actualList.Should().HaveCountGreaterThanOrEqualTo(expectedList.Count, - "actual collection should have at least as many items as expected"); - - foreach (var expectedItem in expectedList) - { - actualList.Should().Contain(actualItem => - AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, - "actual collection should contain an item equivalent to {0}", expectedItem); - } - } - }) - .WhenTypeIs>() - // Using a custom string comparison that deals with ISO langauge codes when the property name ends with "Language" - .Using(ctx => - { - var actual = ctx.Subject.Split("-")[0]; - var expected = ctx.Expectation.Split("-")[0]; - actual.Should().Be(expected); - }) - .When(inf => inf.Path.EndsWith("Language")) .WithTracing()); } - private void SourceContentShouldBeIdentical() - { - CompareMessageType(); - } - public void ResultShouldPassBasicSanityChecks() { EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); @@ -271,25 +293,29 @@ public void ShouldPassBasicStructuralChecks() { var actual = actualEnvelopes; var expected = expectedEnvelopes; - actual.Count().Should().BeGreaterThanOrEqualTo(expected.Count()); - // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) - foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) + using (new AssertionScope()) { - if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the actual but not in the expected."); - } - if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the expected but not in the actual."); - } - if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); + + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) { - actuals_elementsByType[messageType].Count().Should().Be(expecteds_elementsByType[messageType].Count()); + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); } - if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - actuals_elementsByType[messageType].Count().Should().BeGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 8ec3541da..1cd13c029 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -21,17 +21,21 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) // Uri is not always comparable (eg, CCK refers to source file paths in typescript) // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references + // Exception: Exceptions are not comparable // Duration: time values are not comparable + // UseForSnippets: Reqnroll defaults to false always regadless of what is in the CCK // Start: Start refers to a column position in source code, which may not be comparable across platforms. // FileName: CCK does not provide the file name of attachments but Reqnroll does // ProtocolVersion, Implementation, Runtime, Cpu, Os, Ci: These properties of the Meta message are not comparable across platforms. // Line, Column, Seconds and Nanos are skipped, rather than their container types (Location and TimeStamp & Duration, respectively), // because that way we can assert that those container types exist in the actual CucumberMessage (without requiring that the details match the expected CucumberMessage) - private List PropertiesToSkip = new List() { "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", - "Duration", "Start", "FileName", "Message", "Type", "StackTrace", + private List PropertiesToSkip = new List() { + "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Exception", + "Duration", "Start", "FileName", "Message", "Type", "StackTrace", "UseForSnippets", "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "TestCaseStartedId","TestStepId", "TestCaseId", "WorkerId", - "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci"}; + "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci" + }; public FluentAsssertionCucumberMessagePropertySelectionRule(IEnumerable CucumberMessageTypes, IEnumerable? proportiesToSkip = null) { From 6944c499abf912fe9539b9fda580da966bfbca9a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:05:12 -0500 Subject: [PATCH 138/218] Hooks now working. Fixed BASE64 encoding of svg file in CCK. Separate treatment of how hook-related TestStepFinished messages are handled. --- .../CucumberMessageFactory.cs | 14 +++-- .../CucumberMessagePublisher.cs | 13 +---- .../CucumberMessagesValidator.cs | 54 ++++++++++++------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 0ef49c3c9..baed74f65 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; @@ -89,7 +90,7 @@ internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBin internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { var regex = stepTransform.Regex.ToString(); - var name = stepTransform.Name; + var name = stepTransform.Name ?? stepTransform.Method.ReturnType.Name; var result = new ParameterType ( name, @@ -343,8 +344,15 @@ private static string GenerateSignature(IBinding stepDefinition) } public static string Base64EncodeFile(string filePath) { - byte[] fileBytes = File.ReadAllBytes(filePath); - return Convert.ToBase64String(fileBytes); + if (Path.GetExtension(filePath) == ".png" || Path.GetExtension(filePath) == ".jpg") + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } + // else assume its a text file + string text = File.ReadAllText(filePath); + text = text.Replace("\r\n", "\n"); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); } diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index b6f18124c..e9be13919 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -1,20 +1,9 @@ using Reqnroll.BoDi; using Reqnroll.Events; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Io.Cucumber.Messages; -using Io.Cucumber.Messages.Types; using Reqnroll.Tracing; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; -using Reqnroll.Time; -using Cucumber.Messages; -using Reqnroll.Bindings; -using System.Reflection; using System.Collections.Concurrent; -using System.Linq; using System.Diagnostics; namespace Reqnroll.CucumberMessages @@ -28,7 +17,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - // Debugger.Launch(); + Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index eed4580af..a64d5dfcb 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -17,8 +17,8 @@ public class CucumberMessagesValidator private Dictionary> expecteds_IDsByType = new(); private Dictionary> actuals_elementsByType = new(); private Dictionary> expecteds_elementsByType = new(); - private Dictionary> actuals_elementsByID = new(); - private Dictionary> expecteds_elementsByID = new(); + private Dictionary actuals_elementsByID = new(); + private Dictionary expecteds_elementsByID = new(); private readonly FluentAsssertionCucumberMessagePropertySelectionRule FA_CustomCucumberMessagesPropertySelector; // Envelope types - these are the top level types in CucumberMessages @@ -173,14 +173,15 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable - AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, - "actual collection should contain an item equivalent to {0}", expectedItem); - } -*/ } + /* + foreach (var expectedItem in expectedList) + { + actualList.Should().Contain(actualItem => + AssertionExtensions.Should(actualItem).BeEquivalentTo(expectedItem, "").And.Subject == actualItem, + "actual collection should contain an item equivalent to {0}", expectedItem); + } + */ + } }) .WhenTypeIs>() @@ -195,7 +196,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary> elementsByID) + private void SetupCrossReferences(IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) { var xrefBuilder = new CrossReferenceBuilder(msg => { @@ -222,13 +223,9 @@ private static void InsertIntoIDsByType(object msg, Dictionary> elementsByID) + private static void InsertIntoElementsById(object msg, Dictionary elementsByID) { - if (!elementsByID.ContainsKey(msg.Id())) - { - elementsByID.Add(msg.Id(), new HashSet()); - } - elementsByID[msg.Id()].Add(msg); + elementsByID.Add(msg.Id(), msg); } private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) @@ -271,8 +268,27 @@ private void CompareMessageType() expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; - actual.Should().BeEquivalentTo(expected, options => options - .WithTracing()); + if (!(typeof(T) == typeof(TestStepFinished))) + { + actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); + } + else + { + // For TestStepFinished, we will separate out those related to hooks; + // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions + // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses + // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. + // if it has a hookId, it is a hook step + + var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + + actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); + + actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); + } } public void ResultShouldPassBasicSanityChecks() From 4ee7e5b5ccd5bb8a32905f35231841ae49a220ca Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:16:40 -0500 Subject: [PATCH 139/218] Added stack-traces scenario from CCK. FluentAssertion structural comparisons working for Cdata, hooks, minimal, parameter-types, pending, stack-traces, undefined, and unknown-parameter-type. The remaining won't work until handling of DataTables is fixed. Next: ID walking to test referential integrity. --- .../CucumberMessageFactory.cs | 6 ++- .../FeatureEventProcessor.cs | 47 ++++++++++++------- .../CCK/pending/pending.cs | 12 ++--- .../CCK/stack-traces/stack_traces.cs | 19 ++++++++ .../CCK/stack-traces/stack_traces.feature | 9 ++++ .../stack-traces/stack_traces.feature.ndjson | 12 +++++ .../CucumberCompatibilityTests.cs | 3 +- ...CucumberMessages.CompatibilityTests.csproj | 6 +++ .../CucumberMessagesValidator.cs | 10 ++++ 9 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index baed74f65..d77a5b22f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -86,6 +86,10 @@ internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBin var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); return stepDefinitionPattern; } + internal static UndefinedParameterType ToUndefinedParameterType(string expression, string paramName, IIdGenerator iDGenerator) + { + return new UndefinedParameterType(expression, paramName); + } internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { @@ -251,7 +255,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat return status switch { ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, - ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.UNDEFINED, ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs index 8a8338527..710b29ab5 100644 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; namespace Reqnroll.CucumberMessages { @@ -106,29 +107,39 @@ internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEv } var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - if (bindingRegistry.IsValid) + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) { - foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) { - var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); - yield return Envelope.Create(parameterType); + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); + yield return Envelope.Create(undefinedParameterType); } - foreach (var binding in bindingRegistry.GetStepDefinitions()) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); + } - yield return Envelope.Create(stepDefinition); - } + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); - foreach (var hookBinding in bindingRegistry.GetHooks()) - { - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); - var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - HookDefinitionsByPattern.TryAdd(hookId, hook.Id); - yield return Envelope.Create(hook); - } + yield return Envelope.Create(stepDefinition); + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + HookDefinitionsByPattern.TryAdd(hookId, hook.Id); + yield return Envelope.Create(hook); } yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs index 6d2fb8755..9ca0959fe 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs @@ -10,12 +10,6 @@ namespace CucumberMessages.CompatibilityTests.CCK.pending [Binding] internal class Pending { - [Given("an unimplemented pending step")] - public void GivenAnUnimplementedPendingStep() - { - throw new PendingStepException(); - } - [Given("an implemented non-pending step")] public void GivenAnImplementedNonPendingStep() { @@ -27,5 +21,11 @@ public void GivenAnImplementedStepThatIsSkipped() { throw new ApplicationException("This step should not have been executed"); } + + [Given("an unimplemented pending step")] + public void GivenAnUnimplementedPendingStep() + { + throw new PendingStepException(); + } } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs new file mode 100644 index 000000000..c2f9d72c1 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.stack_traces +{ + [Binding] + internal class stack_traces + { + [When(@"a step throws an exception")] + public void WhenAStepThrowsAnException() + { + throw new Exception("BOOM"); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature new file mode 100644 index 000000000..587d50589 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature @@ -0,0 +1,9 @@ +Feature: Stack traces + Stack traces can help you diagnose the source of a bug. + Cucumber provides helpful stack traces that includes the stack frames from the + Gherkin document and remove uninteresting frames by default + + The first line of the stack trace will contain a reference to the feature file. + + Scenario: A failing step + When a step throws an exception diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson new file mode 100644 index 000000000..4bbcea535 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson @@ -0,0 +1,12 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"source":{"data":"Feature: Stack traces\n Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.\n\n Scenario: A failing step\n When a step throws an exception\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/stack-traces/stack-traces.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":8},"name":"A failing step","steps":[{"id":"1","keyword":"When ","keywordType":"Action","location":{"column":5,"line":9},"text":"a step throws an exception"}],"tags":[]}}],"description":" Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Stack traces","tags":[]},"uri":"samples/stack-traces/stack-traces.feature"}} +{"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"A failing step","steps":[{"astNodeIds":["1"],"id":"3","text":"a step throws an exception","type":"Action"}],"tags":[],"uri":"samples/stack-traces/stack-traces.feature"}} +{"stepDefinition":{"id":"0","pattern":{"source":"a step throws an exception","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/stack-traces/stack-traces.feature.ts"}}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"BOOM","type":"Error"},"message":"BOOM\nsamples/stack-traces/stack-traces.feature:9","status":"FAILED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 9eb24ec4d..85316410c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -184,7 +184,8 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("parameter-types", "Parameter Types")] [DataRow("skipped", "Skipping scenarios")] [DataRow("undefined", "Undefined steps")] - [DataRow("unknown-parameter-type", "Parameter Types")] + [DataRow("unknown-parameter-type", "Unknown Parameter Types")] + [DataRow("stack-traces", "Stack traces")] [DataRow("rules", "Usage of a 'Rule'")] public void CCKScenarios(string testName, string featureNameText) { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 6dcc8cd5e..1a8f594c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -20,6 +20,7 @@ + @@ -45,6 +46,8 @@ + + @@ -103,6 +106,9 @@ Always + + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index a64d5dfcb..6a97cc728 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -173,6 +173,7 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable actual, IEnumerable>() + // Groups are nested self-referential objects inside of StepMatchArgument(s). Other Cucumber implementations support a more sophisticated + // version of this structure in which multiple regex capture groups are conveyed inside of a single StepMatchArgument + // For Reqnroll, we will only compare the outermost Group; the only property we care about is the Value. + .Using((ctx) => + { + ctx.Subject.Value.Should().Be(ctx.Expectation.Value); + }) + .WhenTypeIs() + // A bit of trickery here to tell FluentAssertions that Timestamps are always equal // We can't simply omit Timestamp from comparison because then TestRunStarted has nothing else to compare (which causes an error) .Using(ctx => 1.Should().Be(1)) From 4435fc34a9d3b1c1d03091eb3f1cd7cb6cb3c38a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:19:08 -0500 Subject: [PATCH 140/218] Added Ambiguous scenario and corrected error in the Factory in how test step results statuses were mapped. --- .../CucumberMessageFactory.cs | 2 +- .../CCK/ambiguous/ambiguous.cs | 22 +++++++++++++++++++ .../CCK/ambiguous/ambiguous.feature | 9 ++++++++ .../CCK/ambiguous/ambiguous.feature.ndjson | 16 ++++++++++++++ .../CucumberCompatibilityTests.cs | 10 ++++++++- ...CucumberMessages.CompatibilityTests.csproj | 6 +++++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index d77a5b22f..c55847927 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -255,7 +255,7 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat return status switch { ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, - ScenarioExecutionStatus.BindingError => TestStepResultStatus.UNDEFINED, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs new file mode 100644 index 000000000..6529a6a92 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs @@ -0,0 +1,22 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.ambiguous +{ + [Binding] + internal class Ambiguous + { + [Given(@"a step that matches more than one step binding")] + public void FirstMatchingStep() { } + + [Given(@"a step that matches more than one step binding")] + public void SecondMatchingStep() { } + + [Then(@"this step gets skipped because of the prior ambiguous step")] + public void ThirdSkippedStep() { } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature new file mode 100644 index 000000000..0ba800aa1 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature @@ -0,0 +1,9 @@ +Feature: ambiguous + +This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions + +NOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite + +Scenario: Ambiguous + Given a step that matches more than one step binding + Then this step gets skipped because of the prior ambiguous step diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson new file mode 100644 index 000000000..d4afb6c48 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson @@ -0,0 +1,16 @@ +{"meta":{"protocolVersion":"26.0.0","implementation":{"name":"Reqnroll","version":"2.1.1-local+46152309beb5ca95501132a597284bc53db62081"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","data":"Feature: ambiguous\r\n\r\nThis feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite\r\n\r\nScenario: Ambiguous\r\n\tGiven a step that matches more than one step binding\r\n\tThen this step gets skipped because of the prior ambiguous step\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"ambiguous","description":"This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite","children":[{"scenario":{"location":{"line":7,"column":1},"tags":[],"keyword":"Scenario","name":"Ambiguous","description":"","steps":[{"location":{"line":8,"column":2},"keyword":"Given ","keywordType":"Context","text":"a step that matches more than one step binding","id":"0"},{"location":{"line":9,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"this step gets skipped because of the prior ambiguous step","id":"1"}],"examples":[],"id":"2"}}]},"comments":[]}} +{"pickle":{"id":"5","uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","name":"Ambiguous","language":"en-US","steps":[{"astNodeIds":["0"],"id":"3","type":"Context","text":"a step that matches more than one step binding"},{"astNodeIds":["1"],"id":"4","type":"Outcome","text":"this step gets skipped because of the prior ambiguous step"}],"tags":[],"astNodeIds":["2"]}} +{"stepDefinition":{"id":"6","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"FirstMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"7","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"SecondMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"8","pattern":{"source":"this step gets skipped because of the prior ambiguous step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"ThirdSkippedStep","methodParameterTypes":[]}}}} +{"testRunStarted":{"timestamp":{"seconds":1726222400,"nanos":625585200}}} +{"testCase":{"id":"9","pickleId":"5","testSteps":[{"id":"11","pickleStepId":"3","stepDefinitionIds":["6","7"],"stepMatchArgumentsLists":[]},{"id":"12","pickleStepId":"4","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"10","testCaseId":"9","timestamp":{"seconds":1726222400,"nanos":664903000}}} +{"testStepStarted":{"testCaseStartedId":"10","testStepId":"11","timestamp":{"seconds":1726222400,"nanos":666997500}}} +{"testStepFinished":{"testCaseStartedId":"10","testStepId":"11","testStepResult":{"duration":{"seconds":0,"nanos":7889700},"message":"","status":"AMBIGUOUS"},"timestamp":{"seconds":1726222400,"nanos":674887200}}} +{"testStepStarted":{"testCaseStartedId":"10","testStepId":"12","timestamp":{"seconds":1726222400,"nanos":679031100}}} +{"testStepFinished":{"testCaseStartedId":"10","testStepId":"12","testStepResult":{"duration":{"seconds":0,"nanos":817600},"message":"","status":"SKIPPED"},"timestamp":{"seconds":1726222400,"nanos":679848700}}} +{"testCaseFinished":{"testCaseStartedId":"10","timestamp":{"seconds":1726222400,"nanos":688807200},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"seconds":1726222400,"nanos":697037200}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 85316410c..8ec163a94 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -176,7 +176,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [TestMethod] [DataRow("attachments", "Attachments")] [DataRow("minimal", "minimal")] - [DataRow("cdata","cdata")] + [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] [DataRow("examples-tables", "Examples Tables")] [DataRow("hooks", "Hooks")] @@ -209,6 +209,14 @@ public void CCKScenarios(string testName, string featureNameText) ConfirmAllTestsRan(null); } + [TestMethod] + [DataRow("ambiguous", "ambiguous")] + public void NonCCKScenarios(string testName, string featureNameText) + { + CCKScenarios(testName, featureNameText); + } + + private void AddUtilClassWithFileSystemPath() { string location = AppContext.BaseDirectory; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 1a8f594c0..aea923ac8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,6 +10,7 @@ + @@ -26,6 +27,8 @@ + + @@ -56,8 +59,11 @@ + + + Always From e9035357d375d8cc4b8380b5c1cb65ef4a5fe4fc Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:19:23 -0500 Subject: [PATCH 141/218] Added referential integrity validations on Actuals. --- .../CucumberCompatibilityTests.cs | 2 +- .../CucumberMessagesValidator.cs | 360 +++++++++++------- 2 files changed, 217 insertions(+), 145 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 8ec163a94..9a63cc6e9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -203,8 +203,8 @@ public void CCKScenarios(string testName, string featureNameText) var validator = new CucumberMessagesValidator(GetActualResults(testName, featureNameText).ToList(), GetExpectedResults(testName, featureFileName).ToList()); validator.ShouldPassBasicStructuralChecks(); - validator.ResultShouldPassBasicSanityChecks(); validator.ResultShouldPassAllComparisonTests(); + validator.ResultShouldPassSanityChecks(); ConfirmAllTestsRan(null); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 6a97cc728..ef0821001 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -36,6 +36,215 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) + { + var xrefBuilder = new CrossReferenceBuilder(msg => + { + InsertIntoElementsByType(msg, elementsByType); + + if (msg.HasId()) + { + InsertIntoElementsById(msg, elementsByID); + InsertIntoIDsByType(msg, IDsByType); + } + }); + foreach (var message in messages) + { + var msg = message.Content(); + CucumberMessageVisitor.Accept(xrefBuilder, msg); + } + } + private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) + { + if (!IDsByType.ContainsKey(msg.GetType())) + { + IDsByType.Add(msg.GetType(), new HashSet()); + } + IDsByType[msg.GetType()].Add(msg.Id()); + } + + private static void InsertIntoElementsById(object msg, Dictionary elementsByID) + { + elementsByID.Add(msg.Id(), msg); + } + + private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) + { + if (!elementsByType.ContainsKey(msg.GetType())) + { + elementsByType.Add(msg.GetType(), new HashSet()); + } + elementsByType[msg.GetType()].Add(msg); + } + + public void ResultShouldPassAllComparisonTests() + { + var method = typeof(CucumberMessagesValidator).GetMethod(nameof(CompareMessageType), BindingFlags.NonPublic | BindingFlags.Instance); + using (new AssertionScope()) + { + foreach (Type t in EnvelopeTypes) + { + var genMethod = method!.MakeGenericMethod(t); + genMethod.Invoke(this, null); + } + } + } + + private void CompareMessageType() + { + if (!expecteds_elementsByType.ContainsKey(typeof(T))) + return; + + HashSet? actuals; + List actual; + List expected; + + if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) + { + actual = actuals.OfType().ToList(); + } + else + actual = new List(); + + expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; + + if (!(typeof(T) == typeof(TestStepFinished))) + { + actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); + } + else + { + // For TestStepFinished, we will separate out those related to hooks; + // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions + // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses + // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. + // if it has a hookId, it is a hook step + + var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); + var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); + + actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); + + actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); + } + } + + public void ResultShouldPassSanityChecks() + { + EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + TestExecutionStepsShouldProperlyReferenceTestCases(); + TestExecutionMessagesShouldProperlyNest(); + ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + } + + private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() + { + } + + private void TestExecutionStepsShouldProperlyReferenceTestCases() + { + var testCaseIds = actuals_elementsByType[typeof(TestCase)].OfType().Select(tc => tc.Id).ToList(); + + var testCaseStarteds = actuals_elementsByType[typeof(TestCaseStarted)].OfType().ToList(); + testCaseIds.Should().Contain(id => testCaseStarteds.Any(tcs => tcs.TestCaseId == id), "a test case should be referenced by a test case started message"); + + var testCaseStartedIds = testCaseStarteds.Select(tcs => tcs.Id).ToList(); + + var testCaseFinisheds = actuals_elementsByType[typeof(TestCaseFinished)].OfType().ToList(); + var testStepStarteds = actuals_elementsByType[typeof(TestStepStarted)].OfType().ToList(); + var testStepFinisheds = actuals_elementsByType[typeof(TestStepFinished)].OfType().ToList(); + + testCaseStartedIds.Should().Contain(id => testStepStarteds.Any(tss => tss.TestCaseStartedId == id), "a test case started should be referenced by at least one test step started message"); + testCaseStartedIds.Should().Contain(id => testStepFinisheds.Any(tsf => tsf.TestCaseStartedId == id), "a test case started should be referenced by at least one test step finished message"); + testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); + } + + private void TestExecutionMessagesShouldProperlyNest() + { + //walk sequence of messages, using stacks/lists/sets to keep track of nesting + } + + private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook() + { + var testCases = actuals_elementsByType[typeof(TestCase)].OfType(); + foreach (var testCase in testCases) + { + var pickle = testCase.PickleId; + actuals_elementsByID.Should().ContainKey(pickle, "a pickle should be referenced by the test case"); + + var steps = testCase.TestSteps.OfType(); + foreach (var step in steps) + { + if (step.HookId != null) + actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); + + if (step.PickleStepId != null) + actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); + + if (step.StepDefinitionIds != null && step.StepDefinitionIds.Count > 0) + { + foreach (var stepDefinitionId in step.StepDefinitionIds) + actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); + } + } + } + } + + private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() + { + var pickles = actuals_elementsByType[typeof(Pickle)].OfType(); + foreach (var pickle in pickles) + { + var testCases = actuals_elementsByType[typeof(TestCase)].OfType(); + testCases.Should().Contain(tc => tc.PickleId == pickle.Id, "a pickle should be referenced by a test case"); + + var pickleSteps = pickle.Steps.OfType(); + foreach (var pickleStep in pickleSteps) + { + var testSteps = actuals_elementsByType[typeof(TestStep)].OfType(); + testSteps.Should().Contain(ts => ts.PickleStepId == pickleStep.Id, "a pickle step should be referenced by a test step"); + } + } + } + + public void ShouldPassBasicStructuralChecks() + { + var actual = actualEnvelopes; + var expected = expectedEnvelopes; + + using (new AssertionScope()) + { + actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); + + // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) + foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) + { + if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the actual but not in the expected."); + } + if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) + { + throw new System.Exception($"{messageType} present in the expected but not in the actual."); + } + if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + { + actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); + } + if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) + actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); + } + } + } + + private void ArrangeGlobalFluentAssertionOptions() + { AssertionOptions.AssertEquivalencyUsing(options => options // invoking these for each Type in CucumberMessages so that FluentAssertions DOES NOT call .Equal wwhen comparing instances .ComparingByMembers() @@ -143,13 +352,13 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable((ctx) => - { - var subject = ctx.Subject ?? string.Empty; - var expectation = ctx.Expectation ?? string.Empty; - subject = subject.Replace("\r\n", "\n"); - expectation = expectation.Replace("\r\n", "\n"); - subject.Should().Be(expectation); - }) + { + var subject = ctx.Subject ?? string.Empty; + var expectation = ctx.Expectation ?? string.Empty; + subject = subject.Replace("\r\n", "\n"); + expectation = expectation.Replace("\r\n", "\n"); + subject.Should().Be(expectation); + }) .When(info => info.Path.EndsWith("Description") || info.Path.EndsWith("Text") || info.Path.EndsWith("Data")) // The list of hooks should contain at least as many items as the list of expected hooks @@ -206,143 +415,6 @@ public CucumberMessagesValidator(IEnumerable actual, IEnumerable messages, Dictionary> IDsByType, Dictionary> elementsByType, Dictionary elementsByID) - { - var xrefBuilder = new CrossReferenceBuilder(msg => - { - InsertIntoElementsByType(msg, elementsByType); - - if (msg.HasId()) - { - InsertIntoElementsById(msg, elementsByID); - InsertIntoIDsByType(msg, IDsByType); - } - }); - foreach (var message in messages) - { - var msg = message.Content(); - CucumberMessageVisitor.Accept(xrefBuilder, msg); - } - } - private static void InsertIntoIDsByType(object msg, Dictionary> IDsByType) - { - if (!IDsByType.ContainsKey(msg.GetType())) - { - IDsByType.Add(msg.GetType(), new HashSet()); - } - IDsByType[msg.GetType()].Add(msg.Id()); - } - - private static void InsertIntoElementsById(object msg, Dictionary elementsByID) - { - elementsByID.Add(msg.Id(), msg); - } - - private static void InsertIntoElementsByType(object msg, Dictionary> elementsByType) - { - if (!elementsByType.ContainsKey(msg.GetType())) - { - elementsByType.Add(msg.GetType(), new HashSet()); - } - elementsByType[msg.GetType()].Add(msg); - } - public void ResultShouldPassAllComparisonTests() - { - var method = typeof(CucumberMessagesValidator).GetMethod(nameof(CompareMessageType), BindingFlags.NonPublic | BindingFlags.Instance); - using (new AssertionScope()) - { - foreach (Type t in EnvelopeTypes) - { - var genMethod = method!.MakeGenericMethod(t); - genMethod.Invoke(this, null); - } - } - } - - private void CompareMessageType() - { - if (!expecteds_elementsByType.ContainsKey(typeof(T))) - return; - - HashSet? actuals; - List actual; - List expected; - - if (actuals_elementsByType.TryGetValue(typeof(T), out actuals)) - { - actual = actuals.OfType().ToList(); - } - else - actual = new List(); - - expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; - - if (!(typeof(T) == typeof(TestStepFinished))) - { - actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); - } - else - { - // For TestStepFinished, we will separate out those related to hooks; - // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions - // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses - // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. - // if it has a hookId, it is a hook step - - var actual_hookRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); - var actual_stepRelatedTestStepFinished = actual.OfType().Where(tsf => actuals_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); - var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); - var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); - - actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); - - actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); - } - } - - public void ResultShouldPassBasicSanityChecks() - { - EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook(); - EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); - } - - private void EachTestStepShouldProperlyReferToAPickleAndStepDefinitionOrHook() - { - } - - private void EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce() - { - } - - public void ShouldPassBasicStructuralChecks() - { - var actual = actualEnvelopes; - var expected = expectedEnvelopes; - - using (new AssertionScope()) - { - actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); - - // This checks that each top level Envelope content type present in the actual is present in the expected in the same number (except for hooks) - foreach (var messageType in CucumberMessageExtensions.EnvelopeContentTypes) - { - if (actuals_elementsByType.ContainsKey(messageType) && !expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the actual but not in the expected."); - } - if (!actuals_elementsByType.ContainsKey(messageType) && expecteds_elementsByType.ContainsKey(messageType)) - { - throw new System.Exception($"{messageType} present in the expected but not in the actual."); - } - if (messageType != typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - { - actuals_elementsByType[messageType].Should().HaveCount(expecteds_elementsByType[messageType].Count()); - } - if (messageType == typeof(Hook) && actuals_elementsByType.ContainsKey(messageType)) - actuals_elementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(expecteds_elementsByType[messageType].Count()); - } - } - } } } \ No newline at end of file From 1fdcebbb0d2a4e6072ccdb8d351d57add2d7883a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:44:29 -0500 Subject: [PATCH 142/218] Validation test suite functionally complete. --- .../CucumberMessagesValidator.cs | 112 ++++++++++++++++-- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index ef0821001..7bcb01a15 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -136,15 +136,48 @@ private void CompareMessageType() public void ResultShouldPassSanityChecks() { - EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); - EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); - TestExecutionStepsShouldProperlyReferenceTestCases(); - TestExecutionMessagesShouldProperlyNest(); - ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + using (new AssertionScope()) + { + EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook(); + EachPickleAndPickleStepShouldBeReferencedByTestStepsAtLeastOnce(); + TestExecutionStepsShouldProperlyReferenceTestCases(); + TestExecutionMessagesShouldProperlyNest(); + ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected(); + } } private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() { + // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message + + var actualTestStepStarted_TestStepIds = actuals_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); + var expectedTestStepStarteds_TestStepIds = expecteds_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); + + // Making the assumption here that the order of TestStepStarted messages is the same in both Actual and Expected + // pair these up, and walk back to the pickle step text and compare + + actualTestStepStarted_TestStepIds + .Zip(expectedTestStepStarteds_TestStepIds, (a, e) => (a, e)) + .ToList() + .ForEach(t => + { + actuals_elementsByID[t.a].Should().BeAssignableTo(); ; + var actualTS = actuals_elementsByID[t.a] as TestStep; + expecteds_elementsByID[t.e].Should().BeAssignableTo(); ; + var expectedTS = expecteds_elementsByID[t.e] as TestStep; + if (actualTS!.PickleStepId != null && expectedTS!.PickleStepId != null) + { + actuals_elementsByID[actualTS.PickleStepId].Should().BeAssignableTo(); ; + var actualPickleStep = actuals_elementsByID[actualTS.PickleStepId] as PickleStep; + expecteds_elementsByID[expectedTS.PickleStepId].Should().BeAssignableTo(); ; + var expectedPickleStep = expecteds_elementsByID[expectedTS.PickleStepId] as PickleStep; + actualPickleStep!.Text.Should().Be(expectedPickleStep!.Text, $"expecting the text of the pickle step {actualPickleStep.Id} to match that of {expectedPickleStep.Id}"); + } + else + { // confirm that both are null or not null, if one is null, throw an exception + actualTS.PickleStepId.Should().Be(expectedTS!.PickleStepId, "expecting both PickleStepIds to be null or not null"); + } + }); } private void TestExecutionStepsShouldProperlyReferenceTestCases() @@ -167,7 +200,68 @@ private void TestExecutionStepsShouldProperlyReferenceTestCases() private void TestExecutionMessagesShouldProperlyNest() { - //walk sequence of messages, using stacks/lists/sets to keep track of nesting + var ClosedIDs = new List(); + var OpenTestCaseStartedIDs = new List(); + var OpenTestStepIds = new List(); + var numberOfEnvelopes = actualEnvelopes.Count(); + var testRunStartedSeenAtEnvelopeIndex = numberOfEnvelopes + 1; + var testRunFinishedSeenAtEnvelopeIndex = -1; + int currentIndex = 0; + foreach (object msg in actualEnvelopes.Select(e => e.Content())) + { + switch (msg) + { + case TestRunStarted testRunStarted: + testRunStartedSeenAtEnvelopeIndex = currentIndex; + if (testRunFinishedSeenAtEnvelopeIndex != -1) + testRunStartedSeenAtEnvelopeIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestRunStarted events must be before TestRunFinished event"); + break; + case TestRunFinished testRunFinished: + testRunFinishedSeenAtEnvelopeIndex = currentIndex; + testRunFinishedSeenAtEnvelopeIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestRunFinished events must be after TestRunStarted event"); + testRunFinishedSeenAtEnvelopeIndex.Should().Be(numberOfEnvelopes - 1, "TestRunFinished events must be the last event"); + break; + case TestCaseStarted testCaseStarted: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestCaseStarted events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestCaseStarted events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testCaseStarted.Id, "a test case should not be Started twice"); + OpenTestCaseStartedIDs.Add(testCaseStarted.Id); + break; + case TestCaseFinished testCaseFinished: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestCaseFinished events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestCaseFinished events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testCaseFinished.TestCaseStartedId, "a test case should not be Finished twice"); + OpenTestCaseStartedIDs.Should().Contain(testCaseFinished.TestCaseStartedId, "a test case should be Started and active before it is Finished"); + OpenTestCaseStartedIDs.Remove(testCaseFinished.TestCaseStartedId); + ClosedIDs.Add(testCaseFinished.TestCaseStartedId); + OpenTestCaseStartedIDs.Remove(testCaseFinished.TestCaseStartedId); + break; + case TestStepStarted testStepStarted: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestStepStarted events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestStepStarted events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testStepStarted.TestCaseStartedId, "a TestStepStarted event must refer to an active test case"); + OpenTestCaseStartedIDs.Should().Contain(testStepStarted.TestCaseStartedId, "a TestStepStarted event must refer to an active test case"); + OpenTestStepIds.Add(testStepStarted.TestStepId); + break; + case TestStepFinished testStepFinished: + currentIndex.Should().BeGreaterThan(testRunStartedSeenAtEnvelopeIndex, "TestStepFinished events must be after TestRunStarted event"); + if (testRunFinishedSeenAtEnvelopeIndex != -1) + currentIndex.Should().BeLessThan(testRunFinishedSeenAtEnvelopeIndex, "TestStepFinished events must be before TestRunFinished event"); + ClosedIDs.Should().NotContain(testStepFinished.TestCaseStartedId, "a TestStepFinished event must refer to an active test case"); + ClosedIDs.Should().NotContain(testStepFinished.TestStepId, "a TestStepFinished event must refer to an active test step"); + OpenTestCaseStartedIDs.Should().Contain(testStepFinished.TestCaseStartedId, "a TestStepFinished event must refer to an active test case"); + OpenTestStepIds.Should().Contain(testStepFinished.TestStepId, "a TestStepFinished event must refer to an active test step"); + ClosedIDs.Add(testStepFinished.TestStepId); + OpenTestStepIds.Remove(testStepFinished.TestStepId); + break; + default: + break; + } + currentIndex++; + } } private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOrHook() @@ -182,15 +276,15 @@ private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOr foreach (var step in steps) { if (step.HookId != null) - actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); + actuals_elementsByID.Should().ContainKey(step.HookId, "a step references a hook that doesn't exist"); if (step.PickleStepId != null) - actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); + actuals_elementsByID.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); if (step.StepDefinitionIds != null && step.StepDefinitionIds.Count > 0) { foreach (var stepDefinitionId in step.StepDefinitionIds) - actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); + actuals_elementsByID.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); } } } From 2c7fc604c0dffb3d17ccfde237ee5c886edba6cf Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:17:20 -0500 Subject: [PATCH 143/218] Revert "Given default Gherkin.Ast.Locations of (0,0) to generated Table elements (headers, rows, cells)." This reverts commit d9ada246b7b1a2a5bd027febe61942e1c784a393. --- .../Transformation/IncludeExternalDataTransformation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs index 3441f3d5d..c649d032b 100644 --- a/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs +++ b/Plugins/Reqnroll.ExternalData/Reqnroll.ExternalData.ReqnrollPlugin/Transformation/IncludeExternalDataTransformation.cs @@ -85,7 +85,7 @@ private Scenario GetTransformedScenario(Scenario scenario, ExternalDataSpecifica { var exampleRecords = specification.GetExampleRecords(examplesHeaderNames); var exampleRows = exampleRecords.Items - .Select(rec => new Gherkin.Ast.TableRow(new Location(0, 0), exampleRecords.Header.Select(h => new TableCell(new Location(0, 0), rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) + .Select(rec => new Gherkin.Ast.TableRow(null, exampleRecords.Header.Select(h => new TableCell(null, rec.Fields[h].AsString(_featureCultureInfo))).ToArray())) .ToArray(); var examplesBlock = CreateExamplesBlock(exampleRecords.Header, exampleRows, examplesKeyword); @@ -108,8 +108,8 @@ private Examples CreateExamplesBlock(string[] headerNames, Gherkin.Ast.TableRow[ { keyword ??= "External Examples"; var name = "External Examples"; - var tableHeader = new Gherkin.Ast.TableRow(new Location(0, 0), headerNames.Select(h => new TableCell(new Location(0, 0), h)).ToArray()); - return new Examples(new Tag[0], new Location(0, 0), keyword, name, "", tableHeader, exampleRows); + var tableHeader = new Gherkin.Ast.TableRow(null, headerNames.Select(h => new TableCell(null, h)).ToArray()); + return new Examples(new Tag[0], null, keyword, name, "", tableHeader, exampleRows); } } } From e33fd77a02ad893f371937c06ab70823d48fab57 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:18:08 -0500 Subject: [PATCH 144/218] Modified CucumberMessagesConverter to include transformation of the AST to patch in missing Location elements before the AST is transformed to the Cucumber GherkinDocument. This duplicates the GherkinDocumentVisitor and ScenarioTransformation classes from the External Plugin (pending further refactoring). --- Reqnroll.Parser/CucumberMessagesConverter.cs | 4 +- Reqnroll.Parser/GherkinDocumentVisitor.cs | 142 ++++++++++++++++++ ...chMissingLocationElementsTransformation.cs | 51 +++++++ Reqnroll.Parser/ScenarioTransformation.cs | 122 +++++++++++++++ .../CucumberMessagePublisher.cs | 2 +- .../CucumberCompatibilityTests.cs | 2 +- 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 Reqnroll.Parser/GherkinDocumentVisitor.cs create mode 100644 Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs create mode 100644 Reqnroll.Parser/ScenarioTransformation.cs diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index ec9e26af7..9369631cd 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -15,9 +15,11 @@ public CucumberMessagesConverter(IIdGenerator idGenerator) } public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) { + var NullLocationPatcher = new PatchMissingLocationElementsTransformation(); + var gherkinDocumentWithLocation = NullLocationPatcher.TransformDocument(gherkinDocument); var converter = new AstMessagesConverter(_idGenerator); var location = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)); - return converter.ConvertGherkinDocumentToEventArgs(gherkinDocument, location); + return converter.ConvertGherkinDocumentToEventArgs(gherkinDocumentWithLocation, location); } public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) diff --git a/Reqnroll.Parser/GherkinDocumentVisitor.cs b/Reqnroll.Parser/GherkinDocumentVisitor.cs new file mode 100644 index 000000000..4be6eccaa --- /dev/null +++ b/Reqnroll.Parser/GherkinDocumentVisitor.cs @@ -0,0 +1,142 @@ +using System; +using Gherkin.Ast; + +namespace Reqnroll.Parser +{ + abstract class GherkinDocumentVisitor + { + protected virtual void AcceptDocument(ReqnrollDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var featureChild in feature.Children) + { + if (featureChild is Rule rule) AcceptRule(rule); + else if (featureChild is Background background) AcceptBackground(background); + else if (featureChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (featureChild is Scenario scenario) AcceptScenario(scenario); + } + OnFeatureVisited(feature); + } + + protected virtual void AcceptStep(Step step) + { + OnStepVisited(step); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + OnScenarioVisited(scenario); + } + + protected virtual void AcceptScenarioOutline(ScenarioOutline scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var step in scenarioOutline.Steps) + { + AcceptStep(step); + } + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var ruleChild in rule.Children) + { + if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (ruleChild is Scenario scenario) AcceptScenario(scenario); + } + OnRuleVisited(rule); + } + + protected virtual void OnDocumentVisiting(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + } +} diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs new file mode 100644 index 000000000..0107efcd7 --- /dev/null +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -0,0 +1,51 @@ +using Gherkin.Ast; +using Reqnroll.ExternalData.ReqnrollPlugin.Transformation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.Parser +{ + internal class PatchMissingLocationElementsTransformation : ScenarioTransformation + { + protected override Scenario GetTransformedScenario(Scenario scenario) + { + return null; + } + + protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline) + { + if (scenarioOutline.Examples == null || !scenarioOutline.Examples.Any()) + return null; + + var exampleTables = scenarioOutline.Examples; + List transformedExamples = new List(); + + transformedExamples.AddRange(exampleTables.Select(e => PatchLocations(e))); + return new ScenarioOutline( + scenarioOutline.Tags.ToArray(), + PatchLocation(scenarioOutline.Location), + scenarioOutline.Keyword, + scenarioOutline.Name, + scenarioOutline.Description, + scenarioOutline.Steps.ToArray(), + transformedExamples.ToArray()); + } + + private Examples PatchLocations(Examples e) + { + var headerCells = e.TableHeader.Cells; + var tableHeader = new Gherkin.Ast.TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new Gherkin.Ast.TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); + var rows = e.TableBody.Select(r => new Gherkin.Ast.TableRow(PatchLocation(r.Location), r.Cells.Select(c => new Gherkin.Ast.TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); + return new Examples(e.Tags.ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); + } + + private static Location PatchLocation(Location l) + { + return l ?? new Location(0, 0); + } + + + } +} diff --git a/Reqnroll.Parser/ScenarioTransformation.cs b/Reqnroll.Parser/ScenarioTransformation.cs new file mode 100644 index 000000000..627577d78 --- /dev/null +++ b/Reqnroll.Parser/ScenarioTransformation.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gherkin.Ast; + +namespace Reqnroll.Parser +{ + abstract class ScenarioTransformation : GherkinDocumentVisitor + { + protected ReqnrollDocument _sourceDocument; + private ReqnrollDocument _transformedDocument; + private ReqnrollFeature _transformedFeature; + private bool _hasTransformedScenarioInFeature = false; + private bool _hasTransformedScenarioInCurrentRule = false; + private readonly List _featureChildren = new(); + private readonly List _ruleChildren = new(); + private List _currentChildren; + + public ReqnrollDocument TransformDocument(ReqnrollDocument document) + { + Reset(); + AcceptDocument(document); + return _transformedDocument ?? document; + } + + private void Reset() + { + _sourceDocument = null; + _transformedDocument = null; + _transformedFeature = null; + _featureChildren.Clear(); + _ruleChildren.Clear(); + _hasTransformedScenarioInFeature = false; + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _featureChildren; + } + + protected abstract Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline); + protected abstract Scenario GetTransformedScenario(Scenario scenario); + + protected override void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + var transformedScenarioOutline = GetTransformedScenarioOutline(scenarioOutline); + OnScenarioVisitedInternal(scenarioOutline, transformedScenarioOutline); + } + + protected override void OnScenarioVisited(Scenario scenario) + { + var transformedScenario = GetTransformedScenario(scenario); + OnScenarioVisitedInternal(scenario, transformedScenario); + } + + private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedScenario) + { + if (transformedScenario == null) + { + _currentChildren.Add(scenario); + return; + } + + _hasTransformedScenarioInFeature = true; + _hasTransformedScenarioInCurrentRule = true; + _currentChildren.Add(transformedScenario); + } + + protected override void OnBackgroundVisited(Background background) + { + _featureChildren.Add(background); + } + + protected override void OnRuleVisiting(Rule rule) + { + _ruleChildren.Clear(); + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _ruleChildren; + } + + protected override void OnRuleVisited(Rule rule) + { + _currentChildren = _featureChildren; + if (_hasTransformedScenarioInCurrentRule) + { + var transformedRule = new Rule( + rule.Tags?.ToArray() ?? Array.Empty(), + rule.Location, + rule.Keyword, + rule.Name, + rule.Description, + _ruleChildren.ToArray()); + _featureChildren.Add(transformedRule); + } + else + { + _featureChildren.Add(rule); + } + } + + protected override void OnFeatureVisited(Feature feature) + { + if (_hasTransformedScenarioInFeature) + _transformedFeature = new ReqnrollFeature( + feature.Tags?.ToArray() ?? Array.Empty(), + feature.Location, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + _featureChildren.ToArray()); + } + + protected override void OnDocumentVisiting(ReqnrollDocument document) + { + _sourceDocument = document; + } + + protected override void OnDocumentVisited(ReqnrollDocument document) + { + if (_transformedFeature != null) + _transformedDocument = new ReqnrollDocument(_transformedFeature, document.Comments.ToArray(), document.DocumentLocation); + } + } +} diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs index e9be13919..7bd7a086f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs @@ -17,7 +17,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) { - Debugger.Launch(); + //Debugger.Launch(); broker = CucumberMessageBroker; } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 9a63cc6e9..3505f4d02 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -59,7 +59,7 @@ public void CucumberMessagesInteropWithExternalData() { // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages AddCucumberMessagePlugIn(); - _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.2.0-local"); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest From febac6b6e92fa6c6a7d72c73fa7c9084c1d2043a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:59:17 -0500 Subject: [PATCH 145/218] Fixed: HookTestStep should have argument properties that are null. Fixed: copy/paste error of using statement in Location Element transformer --- Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs | 1 - Reqnroll/CucumberMesssages/CucumberMessageFactory.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 0107efcd7..87daa06ab 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -1,5 +1,4 @@ using Gherkin.Ast; -using Reqnroll.ExternalData.ReqnrollPlugin.Transformation; using System; using System.Collections.Generic; using System.Linq; diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index c55847927..49913a9dc 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -190,8 +190,8 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) hookId, hookStepState.TestStepID, null, - new List(), - new List()); + null, + null); } internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) { From d23c0764603dcebba5b28619951dcc81949217b6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:23:23 -0500 Subject: [PATCH 146/218] Enhanced BindingException with sub-class AmbiguousBindingException to carry along the candidate BindingMatches that cause the ambiguity. These are used in the Step tracker and message factory to populate the array of StepDef Ids that cause the ambiguity. --- .../CucumberMessageFactory.cs | 5 +- .../ScenarioStepProcessor.cs | 8 +++ Reqnroll/ErrorHandling/BindingException.cs | 49 +++++++++++++++++++ Reqnroll/ErrorHandling/ErrorProvider.cs | 10 ++-- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs index 49913a9dc..4afbb55ab 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs @@ -122,7 +122,8 @@ private static SourceReference ToSourceRef(IBinding binding) internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) { - bool bound = stepState.StepDefinitionId != null; + bool bound = stepState.Bound; + bool ambiguous = stepState.Ambiguous; var args = stepState.StepArguments .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) @@ -132,7 +133,7 @@ internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, null, stepState.TestStepID, stepState.PickleStepID, - bound ? new List { stepState.StepDefinitionId } : new List(), + bound ? new List { stepState.StepDefinitionId } : ambiguous ? new List(stepState.AmbiguousStepDefinitions) : new List(), bound ? new List { new StepMatchArgumentsList(args) } : new List() ); diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 7e6b53aca..7a779844a 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Reqnroll.CucumberMessages { @@ -26,6 +27,8 @@ public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base( public bool Bound { get; set; } public string CanonicalizedStepPattern { get; set; } public string StepDefinitionId { get; private set; } + public IEnumerable AmbiguousStepDefinitions { get; set; } + public bool Ambiguous { get { return AmbiguousStepDefinitions != null && AmbiguousStepDefinitions.Count() > 0;} } public IStepDefinitionBinding StepDefinition { get; set; } public List StepArguments { get; set; } @@ -64,6 +67,11 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) if (Status == ScenarioExecutionStatus.TestError && stepFinishedEvent.ScenarioContext.TestError != null) { Exception = stepFinishedEvent.ScenarioContext.TestError; + if (Exception is AmbiguousBindingException) + { + AmbiguousStepDefinitions = new List(((AmbiguousBindingException)Exception).Matches.Select(m => + FindStepDefIDByStepPattern(CucumberMessageFactory.CanonicalizeStepDefinitionPattern(m.StepBinding)))); + } } var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); diff --git a/Reqnroll/ErrorHandling/BindingException.cs b/Reqnroll/ErrorHandling/BindingException.cs index 308708cd6..3bb5fe4cd 100644 --- a/Reqnroll/ErrorHandling/BindingException.cs +++ b/Reqnroll/ErrorHandling/BindingException.cs @@ -1,4 +1,7 @@ +using Reqnroll.Bindings; using System; +using System.Collections; +using System.Collections.Generic; using System.Runtime.Serialization; // the exceptions are part of the public API, keep them in Reqnroll namespace @@ -25,4 +28,50 @@ protected BindingException( { } } + + [Serializable] + public class AmbiguousBindingException : BindingException + { + public IEnumerable Matches { get; private set; } + + public AmbiguousBindingException() + { + } + + public AmbiguousBindingException(string message) : base(message) + { + } + + public AmbiguousBindingException(string message, Exception inner) : base(message, inner) + { + } + + public AmbiguousBindingException(string message, IEnumerable matches) : base(message) + { + Matches = new List(matches); + } + + protected AmbiguousBindingException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + Matches = (List)info.GetValue("Matches", typeof(List)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + base.GetObjectData(info, context); + info.AddValue("Matches", Matches); + } + } } \ No newline at end of file diff --git a/Reqnroll/ErrorHandling/ErrorProvider.cs b/Reqnroll/ErrorHandling/ErrorProvider.cs index ce6731d3f..b1dc709cb 100644 --- a/Reqnroll/ErrorHandling/ErrorProvider.cs +++ b/Reqnroll/ErrorHandling/ErrorProvider.cs @@ -59,17 +59,19 @@ public Exception GetParameterCountError(BindingMatch match, int expectedParamete public Exception GetAmbiguousMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( - $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + return new AmbiguousBindingException( + $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetAmbiguousBecauseParamCheckMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( + return new AmbiguousBindingException( "Multiple step definitions found, but none of them have matching parameter count and type for step " - + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetNoMatchBecauseOfScopeFilterError(List matches, StepInstance stepInstance) From b5731512059e593b8e1a885fa18dc720491ea616 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:22:50 -0500 Subject: [PATCH 147/218] Modified how step arguments are handled to prevent step DataTable and DocString from being rendered as StepMatchArguments. This allows for the DataTables CCK scenario to pass. --- Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs index 7a779844a..d13f26267 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs @@ -74,9 +74,10 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) } } + var IsInputDataTableOrDocString = stepFinishedEvent.StepContext.StepInfo.Table != null || stepFinishedEvent.StepContext.StepInfo.MultilineText != null; var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); - StepArguments = Bound ? + StepArguments = Bound && !IsInputDataTableOrDocString ? argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); From 87fce658f6f542747813e61a1d60a2241e68d730 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:53:08 -0500 Subject: [PATCH 148/218] Large (partial) refactor. Modified code gen so that pickleID is provided to each test method at runtime (as a parameter for row tests, hard-coded by code-gen for regular tests). PickleStepIds are retrieved at run-time (given the PickleId and sequence of steps taken). The PickleID is stored in ScenarioInfo and the pickleStepId is provided to the Given/When/Then. These are wired into the relevant ExecutionEvents. --- .../Generation/ScenarioPartHelper.cs | 6 +- .../Generation/UnitTestFeatureGenerator.cs | 17 +++- .../Generation/UnitTestMethodGenerator.cs | 85 ++++++++++++++++--- .../FeatureLevelCucumberMessages.cs | 11 ++- Reqnroll/CucumberMesssages/PickleJar.cs | 65 ++++++++++++++ Reqnroll/ITestRunner.cs | 10 +-- .../Infrastructure/ITestExecutionEngine.cs | 2 +- .../Infrastructure/TestExecutionEngine.cs | 4 +- Reqnroll/ScenarioInfo.cs | 8 +- Reqnroll/StepInfo.cs | 4 +- Reqnroll/TestRunner.cs | 20 ++--- .../CucumberCompatibilityTestBase.cs | 10 +-- .../CucumberCompatibilityTests.cs | 53 ++++++++++++ 13 files changed, 250 insertions(+), 45 deletions(-) create mode 100644 Reqnroll/CucumberMesssages/PickleJar.cs diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 307b7d8a6..c3f215e05 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using Gherkin.Ast; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Parser; @@ -11,6 +12,8 @@ namespace Reqnroll.Generator.Generation { public class ScenarioPartHelper { + private const string BRINE = "PickleJar"; + private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly CodeDomHelper _codeDomHelper; private int _tableCounter; @@ -92,7 +95,8 @@ public void GenerateStep(TestClassGenerationContext generationContext, List())); return; } // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object @@ -245,17 +257,20 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); - // Create a FeatureLevelCucumberMessages object and add it to featureInfo var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), new CodePrimitiveExpression(featureSourceMessageString), new CodePrimitiveExpression(featureGherkinDocumentMessageString), new CodePrimitiveExpression(featurePickleMessagesString)); + CucumberMessagesInitializeMethod.Statements.Add( new CodeAssignStatement( new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), featureLevelCucumberMessagesExpression)); + ///At runtime, pull the PickleStepIds from the pickleJar for the given runtime pickleID; partition them out by Background, RuleBackground, and Scenario steps + ///keep an index of which step is being generated and use that to index in to the PickleStepIds at runtime ... eg "stepIds[codeprimitive(stepIndex)]" + // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( null, diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 252c6ca34..8e8136eed 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -5,6 +5,8 @@ using System.Linq; using Gherkin.Ast; using Reqnroll.Configuration; +using Reqnroll.CucumberMesssages; +using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; @@ -15,6 +17,9 @@ namespace Reqnroll.Generator.Generation { public class UnitTestMethodGenerator { + private const string PICKLEJAR = "PICKLEJAR"; + private const string PICKLEID_PARAMETER_NAME = "@pickleId"; + private const string PICKLEID_VARIABLE_NAME = "m_pickleId"; private const string IGNORE_TAG = "@Ignore"; private const string TESTRUNNER_FIELD = "testRunner"; private readonly CodeDomHelper _codeDomHelper; @@ -22,6 +27,8 @@ public class UnitTestMethodGenerator private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; + private PickleJar _pickleJar; + public UnitTestMethodGenerator(IUnitTestGeneratorProvider unitTestGeneratorProvider, IDecoratorRegistry decoratorRegistry, CodeDomHelper codeDomHelper, ScenarioPartHelper scenarioPartHelper, ReqnrollConfiguration reqnrollConfiguration) { @@ -39,13 +46,14 @@ IEnumerable GetScenarioDefinitionsOfRule(IEnume .Where(child => child is not Background) .Select(sd => new ScenarioDefinitionInFeatureFile(sd, feature, rule)); - return + return GetScenarioDefinitionsOfRule(feature.Children, null) .Concat(feature.Children.OfType().SelectMany(rule => GetScenarioDefinitionsOfRule(rule.Children, rule))); } public void CreateUnitTests(ReqnrollFeature feature, TestClassGenerationContext generationContext) { + _pickleJar = generationContext.CustomData[PICKLEJAR] as PickleJar; foreach (var scenarioDefinition in GetScenarioDefinitions(feature)) { CreateUnitTest(generationContext, scenarioDefinition); @@ -66,6 +74,7 @@ private void CreateUnitTest(TestClassGenerationContext generationContext, Scenar else { GenerateTest(generationContext, scenarioDefinitionInFeatureFile); + _pickleJar.NextPickle(); } } @@ -87,7 +96,7 @@ private void GenerateScenarioOutlineTest(TestClassGenerationContext generationCo GenerateScenarioOutlineExamplesAsIndividualMethods(scenarioOutline, generationContext, scenarioOutlineTestMethod, paramToIdentifier); } - GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier); + GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier, true); } private void GenerateTest(TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile) @@ -129,7 +138,9 @@ private void GenerateTestBody( TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile, CodeMemberMethod testMethod, - CodeExpression additionalTagsExpression = null, ParameterSubstitution paramToIdentifier = null) + CodeExpression additionalTagsExpression = null, + ParameterSubstitution paramToIdentifier = null, + bool pickleIdIncludedInParameters = false) { var scenarioDefinition = scenarioDefinitionInFeatureFile.ScenarioDefinition; var feature = scenarioDefinitionInFeatureFile.Feature; @@ -190,6 +201,9 @@ private void GenerateTestBody( AddVariableForArguments(testMethod, paramToIdentifier); + AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, generationContext.CustomData[PICKLEJAR] as PickleJar); + AddVariableForPickleStepSequence(testMethod); + testMethod.Statements.Add( new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(ScenarioInfo)), "scenarioInfo", new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(ScenarioInfo)), @@ -197,7 +211,8 @@ private void GenerateTestBody( new CodePrimitiveExpression(scenarioDefinition.Description), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), - inheritedTagsExpression))); + inheritedTagsExpression, + new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -206,6 +221,41 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } + private void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + { + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), PICKLEID_VARIABLE_NAME, + pickleIdIncludedInParameters ? + new CodeVariableReferenceExpression(PICKLEID_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleJar.CurrentPickleId)); + testMethod.Statements.Add(pickleIdVariable); + } + + private void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) + { + // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME, + // Right side of the assignment (property access chain) + + new CodeMethodInvokeExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(TESTRUNNER_FIELD), + "FeatureContext" + ), + "FeatureInfo" + ), + "FeatureCucumberMessages" + ), + "PickleJar" + ), + "PickleStepSequenceFor", + new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)) + ); + + testMethod.Statements.Add(pickleStepSequence); + } private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { var tagVariable = new CodeVariableDeclarationStatement(typeof(string[]), GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME, tagsExpression); @@ -272,6 +322,9 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex foreach (var scenarioStep in scenario.Steps) { _scenarioPartHelper.GenerateStep(generationContext, statementsWhenScenarioIsExecuted, scenarioStep, paramToIdentifier); + statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(new CodeMethodInvokeExpression( + new CodeVariableReferenceExpression(PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME), + "NextStep"))); } var tagsOfScenarioVariableReferenceExpression = new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME); @@ -322,7 +375,7 @@ internal void GenerateScenarioCleanupMethodCall(TestClassGenerationContext gener testMethod.Statements.Add(expression); } - + private CodeMethodInvokeExpression CreateTestRunnerSkipScenarioCall() { return new CodeMethodInvokeExpression( @@ -362,7 +415,9 @@ private void GenerateScenarioOutlineExamplesAsIndividualMethods( foreach (var example in exampleSet.TableBody.Select((r, i) => new { Row = r, Index = i })) { var variantName = useFirstColumnAsName ? example.Row.Cells.First().Value : string.Format("Variant {0}", example.Index); - GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, exampleSet.Tags.ToArray(), variantName); + var currentPickleId = _pickleJar.CurrentPickleId; + GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, currentPickleId, exampleSet.Tags.ToArray(), variantName); + _pickleJar.NextPickle(); } exampleSetIndex++; @@ -377,8 +432,11 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex { foreach (var row in examples.TableBody) { - var arguments = row.Cells.Select(c => c.Value); + var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleJar.CurrentPickleId }); + _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); + + _pickleJar.NextPickle(); } } } @@ -434,12 +492,12 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont testMethod.Name = string.Format(GeneratorConstants.TEST_NAME_FORMAT, scenarioOutline.Name.ToIdentifier()); _codeDomHelper.MarkCodeMemberMethodAsAsync(testMethod); - + foreach (var pair in paramToIdentifier) { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), @"@pickleId")); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } @@ -452,16 +510,17 @@ private void GenerateScenarioOutlineTestVariant( string exampleSetTitle, string exampleSetIdentifier, Gherkin.Ast.TableRow row, + string pickleId, Tag[] exampleSetTags, string variantName) { var testMethod = CreateTestMethod(generationContext, scenarioOutline, exampleSetTags, variantName, exampleSetIdentifier); - + //call test implementation with the params var argumentExpressions = row.Cells.Select(paramCell => new CodePrimitiveExpression(paramCell.Value)).Cast().ToList(); - + argumentExpressions.Add(new CodePrimitiveExpression(pickleId)); argumentExpressions.Add(_scenarioPartHelper.GetStringArrayExpression(exampleSetTags)); - + var statements = new List(); using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, scenarioOutline.Location)) @@ -475,7 +534,7 @@ private void GenerateScenarioOutlineTestVariant( statements.Add(new CodeExpressionStatement(callTestMethodExpression)); } - + testMethod.Statements.AddRange(statements.ToArray()); //_linePragmaHandler.AddLineDirectiveHidden(testMethod.Statements); diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs index 5a48bc4b3..07a15b569 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs @@ -1,4 +1,5 @@ -using System; +using Reqnroll.CucumberMesssages; +using System; using System.Collections.Generic; using System.Text; @@ -11,10 +12,12 @@ public FeatureLevelCucumberMessages(string source, string gkerkinDocument, strin Source = source; GherkinDocument = gkerkinDocument; Pickles = pickles; + PickleJar = new PickleJar(pickles); } - public string Source { get; set; } - public string GherkinDocument { get; set; } - public string Pickles { get; set; } + public string Source { get; } + public string GherkinDocument { get; } + public string Pickles { get; } + public PickleJar PickleJar { get; } } } diff --git a/Reqnroll/CucumberMesssages/PickleJar.cs b/Reqnroll/CucumberMesssages/PickleJar.cs new file mode 100644 index 000000000..f8bd2e5a9 --- /dev/null +++ b/Reqnroll/CucumberMesssages/PickleJar.cs @@ -0,0 +1,65 @@ +using Gherkin.CucumberMessages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.CucumberMesssages +{ + public class PickleJar + { + public const string PICKLEJAR_VARIABLE_NAME = "m_pickleJar"; + + public int _PickleCounter = 0; + public IEnumerable Pickles { get; set; } + + //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) + //{ } + public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } + public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } + + public PickleJar(IEnumerable pickles, int pickleCounter, int pickleStepCounter) + { + Pickles = pickles; + _PickleCounter = pickleCounter; + } + + public string CurrentPickleId { get { return Pickles.ElementAt(_PickleCounter).Id; } } + public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } + + public IEnumerable PickleStepIdsFor(string pickleId) + { + return Pickles.Where(p => p.Id == pickleId).SelectMany(p => p.Steps.Select(s => s.Id)).ToArray(); + } + public PickleStepSequence PickleStepSequenceFor(string pickleId) + { + return new PickleStepSequence(Pickles.Where(p => p.Id == pickleId).First()); + } + + public void NextPickle() + { + _PickleCounter++; + } + } + + + public class PickleStepSequence + { + public static string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; + public Pickle CurrentPickle { get; } + + private int _PickleStepCounter; + + public PickleStepSequence(Gherkin.CucumberMessages.Types.Pickle pickle) + { + CurrentPickle = pickle; + _PickleStepCounter = 0; + } + public void NextStep() + { + _PickleStepCounter++; + } + public string CurrentPickleStepId { get { return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; } } + + } +} diff --git a/Reqnroll/ITestRunner.cs b/Reqnroll/ITestRunner.cs index e949a177f..6c7537e06 100644 --- a/Reqnroll/ITestRunner.cs +++ b/Reqnroll/ITestRunner.cs @@ -30,11 +30,11 @@ public interface ITestRunner void SkipScenario(); - Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); - Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null); + Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); + Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null); void Pending(); } diff --git a/Reqnroll/Infrastructure/ITestExecutionEngine.cs b/Reqnroll/Infrastructure/ITestExecutionEngine.cs index fdb15e429..e3686428c 100644 --- a/Reqnroll/Infrastructure/ITestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/ITestExecutionEngine.cs @@ -22,7 +22,7 @@ public interface ITestExecutionEngine void OnScenarioSkipped(); - Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg); + Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId); void Pending(); } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index aca42938a..aeb94c22f 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -622,12 +622,12 @@ private async Task ConvertArg(object value, IBindingType typeToConvertTo #region Given-When-Then - public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg) + public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId = null) { StepDefinitionType stepDefinitionType = stepDefinitionKeyword == StepDefinitionKeyword.And || stepDefinitionKeyword == StepDefinitionKeyword.But ? GetCurrentBindingType() : (StepDefinitionType) stepDefinitionKeyword; - _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg)); + _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); try diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 4abd241a0..0247f7e54 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -36,13 +36,19 @@ public class ScenarioInfo /// public string Description { get; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null) + /// + /// The PickleId of the Scenario when exported as a Cucumber Message "pickle". + /// + public string PickleId { get; } + + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null) { Title = title; Description = description; Tags = tags ?? Array.Empty(); Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); + PickleId = pickleId; } } } \ No newline at end of file diff --git a/Reqnroll/StepInfo.cs b/Reqnroll/StepInfo.cs index 3cbb3bc87..75b435e52 100644 --- a/Reqnroll/StepInfo.cs +++ b/Reqnroll/StepInfo.cs @@ -12,13 +12,15 @@ public class StepInfo public BindingMatch BindingMatch { get; set; } public StepInstance StepInstance { get; set; } + public string PickleStepId { get; } - public StepInfo(StepDefinitionType stepDefinitionType, string text, Table table, string multilineText) + public StepInfo(StepDefinitionType stepDefinitionType, string text, Table table, string multilineText, string pickleStepId = null) { StepDefinitionType = stepDefinitionType; Text = text; Table = table; MultilineText = multilineText; + PickleStepId = pickleStepId; } diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index 82c5bff7a..dbbe282dc 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -73,29 +73,29 @@ public async Task OnTestRunEndAsync() await _executionEngine.OnTestRunEndAsync(); } - public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg, pickleStepId); } - public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) + public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg); + await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg, pickleStepId); } public void Pending() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 0fddf4667..4f7fa2d1c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -11,6 +11,10 @@ namespace CucumberMessages.CompatibilityTests { public class CucumberCompatibilityTestBase : SystemTestBase { + protected override void TestCleanup() + { + // TEMPORARY: this is in place so that SystemTestBase.TestCleanup does not run (which deletes the generated code) + } protected void AddCucumberMessagePlugIn() { @@ -47,12 +51,6 @@ protected IEnumerable GetTestBinaryFileNames(string scenarioName, string .Select(rn => rn.Substring(prefixToRemove.Length)); } - protected string GetExpectedNDJSONFileContent(string scenarioName, string prefix, Assembly? assembly) - { - var testAssembly = assembly ?? Assembly.GetExecutingAssembly(); - return _testFileManager.GetTestFileContent($"{scenarioName}.feature.ndjson", $"{prefix}.{scenarioName}", testAssembly); - } - protected void CucumberMessagesAddConfigurationFile(string configFileName) { var configFileContent = File.ReadAllText(configFileName); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 3505f4d02..d9cb9cece 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -9,6 +9,8 @@ using System.Text.Json; using System.ComponentModel; using Reqnroll.TestProjectGenerator; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Reqnroll.TestProjectGenerator.Driver; namespace CucumberMessages.CompatibilityTests { @@ -54,6 +56,57 @@ public void SmokeTest() ShouldAllScenariosPass(); } + [TestMethod] + public void SmokeOutlineTest() + { + AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] + public void SmokeOutlineTestAsMethods() + { + var _configurationFileDriver = GetServiceSafe(); + _configurationFileDriver.SetIsRowTestsAllowed(false); + + AddCucumberMessagePlugIn(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test As Methods + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] public void CucumberMessagesInteropWithExternalData() { From 6358e8e4009cda1020ecce40fcced8e78ea2a265 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:11:51 -0500 Subject: [PATCH 149/218] Refactored scenario tracking into TestCase tracking --- .../FileSinkPlugin.cs | 59 +++- ...mberMessage.FileSink.ReqnrollPlugin.csproj | 2 +- .../Generation/ScenarioPartHelper.cs | 2 +- .../Generation/UnitTestFeatureGenerator.cs | 1 - .../Generation/UnitTestMethodGenerator.cs | 2 +- .../AttachmentAddedEventWrapper.cs | 0 .../CucumberMessagEnumConverter.cs | 0 .../CucumberMessageBroker.cs | 16 +- .../CucumberMessageExtensions.cs | 0 .../CucumberMessageFactory.cs | 40 +-- .../CucumberMessagePublisher.cs | 150 ++++----- .../CucumberMessageTransformer.cs | 0 .../CucumberMessageVisitor.cs | 0 .../CucumberMessage_TraversalVisitorBase.cs | 0 .../FeatureLevelCucumberMessages.cs | 2 +- Reqnroll/CucumberMessages/FeatureTracker.cs | 104 +++++++ .../FileExtensionToMIMETypeMap.cs | 0 .../HookStepProcessor.cs | 13 +- .../ICucumberMessagePublisher.cs | 0 .../ICucumberMessageSink.cs | 0 .../ICucumberMessageVisitor.cs | 0 .../IStepProcessor.cs | 0 .../IdGeneratorFactory.cs | 0 .../NdjsonSerializer.cs | 0 .../OutputAddedEventWrapper.cs | 0 .../PickleJar.cs | 2 +- .../ReqnrollCucumberMessage.cs | 0 .../StepProcessorBase.cs | 8 +- .../TestCaseCucumberMessageTracker.cs | 291 ++++++++++++++++++ .../TestStepProcessor.cs} | 22 +- .../FeatureEventProcessor.cs | 259 ---------------- .../ScenarioEventProcessor.cs | 205 ------------ Reqnroll/Events/ExecutionEvent.cs | 19 +- Reqnroll/FeatureInfo.cs | 3 +- .../Infrastructure/ReqnrollOutputHelper.cs | 12 +- Reqnroll/Reqnroll.csproj | 2 +- .../CucumberCompatibilityTests.cs | 8 +- ...CucumberMessages.CompatibilityTests.csproj | 2 +- 38 files changed, 567 insertions(+), 657 deletions(-) rename Reqnroll/{CucumberMesssages => CucumberMessages}/AttachmentAddedEventWrapper.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessagEnumConverter.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageBroker.cs (76%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageExtensions.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageFactory.cs (86%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessagePublisher.cs (50%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageTransformer.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessageVisitor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/CucumberMessage_TraversalVisitorBase.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/FeatureLevelCucumberMessages.cs (94%) create mode 100644 Reqnroll/CucumberMessages/FeatureTracker.cs rename Reqnroll/{CucumberMesssages => CucumberMessages}/FileExtensionToMIMETypeMap.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/HookStepProcessor.cs (58%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessagePublisher.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessageSink.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ICucumberMessageVisitor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/IStepProcessor.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/IdGeneratorFactory.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/NdjsonSerializer.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/OutputAddedEventWrapper.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/PickleJar.cs (98%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/ReqnrollCucumberMessage.cs (100%) rename Reqnroll/{CucumberMesssages => CucumberMessages}/StepProcessorBase.cs (56%) create mode 100644 Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs rename Reqnroll/{CucumberMesssages/ScenarioStepProcessor.cs => CucumberMessages/TestStepProcessor.cs} (77%) delete mode 100644 Reqnroll/CucumberMesssages/FeatureEventProcessor.cs delete mode 100644 Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs index f2602a1d4..d95de1804 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs @@ -10,6 +10,8 @@ using Reqnroll.Events; using System.Collections.Concurrent; using System.Text.Json; +using Reqnroll.Tracing; +using Reqnroll.BoDi; [assembly: RuntimePlugin(typeof(FileSinkPlugin))] @@ -28,25 +30,24 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private readonly ConcurrentDictionary fileStreams = new(); private FileSinkConfiguration? configuration; private string baseDirectory = ""; + private Lazy? traceListener; + private ITraceListener? trace => traceListener?.Value; + private IObjectContainer? objectContainer; public FileSinkPlugin() { + traceListener = new Lazy(() => objectContainer!.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) - return; - - baseDirectory = ProcessConfiguration(configuration); - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + objectContainer = args.ObjectContainer; testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; @@ -61,23 +62,26 @@ private string ProcessConfiguration(FileSinkConfiguration configuration) var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); if (!Directory.Exists(basePath)) { - lock(_lock) + lock (_lock) { if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath); } } - + trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); return basePath; } else { - return Assembly.GetExecutingAssembly().Location; + var location = Assembly.GetExecutingAssembly().Location; + trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); + return location; } } private void CloseFileSink(TestRunFinishedEvent @event) { + trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; @@ -85,11 +89,23 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + return; + } + + baseDirectory = ProcessConfiguration(configuration); + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } public void Publish(ReqnrollCucumberMessage message) { + var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; + trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -102,16 +118,17 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } } - private bool disposedValue; private string Serialize(Envelope message) { @@ -119,26 +136,34 @@ private string Serialize(Envelope message) } private async Task Write(string featureName, string cucumberMessage) { - - if (!fileStreams.ContainsKey(featureName)) + try { - lock (_lock) + if (!fileStreams.ContainsKey(featureName)) { - if (!fileStreams.ContainsKey(featureName)) + lock (_lock) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } } } + trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); + } + catch (System.Exception ex) + { + trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); } - await fileStreams[featureName].WriteLineAsync(cucumberMessage); } private void CloseFeatureStream(string featureName) { + trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); fileStreams[featureName].Close(); fileStreams.TryRemove(featureName, out var _); } - + private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj index 4df9424dd..3b49d3ad1 100644 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj @@ -10,7 +10,7 @@ - + diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index c3f215e05..3dd6839a8 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -4,7 +4,7 @@ using System.Text.RegularExpressions; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.Generator.CodeDom; using Reqnroll.Parser; diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 51fec3125..1142bba12 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -7,7 +7,6 @@ using Gherkin.CucumberMessages; using Reqnroll.Configuration; using Reqnroll.CucumberMessages; -using Reqnroll.CucumberMesssages; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 8e8136eed..0d8794649 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; diff --git a/Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs similarity index 100% rename from Reqnroll/CucumberMesssages/AttachmentAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs similarity index 76% rename from Reqnroll/CucumberMesssages/CucumberMessageBroker.cs rename to Reqnroll/CucumberMessages/CucumberMessageBroker.cs index 485667467..7b4804104 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs @@ -16,7 +16,7 @@ public interface ICucumberMessageBroker { bool Enabled { get; } void Complete(string cucumberMessageSource); - void Publish(ReqnrollCucumberMessage message); + void Publish(ReqnrollCucumberMessage featureMessages); } public class CucumberMessageBroker : ICucumberMessageBroker @@ -25,22 +25,20 @@ public class CucumberMessageBroker : ICucumberMessageBroker public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; - //private ITraceListener _traceListener; + private Lazy> RegisteredSinks; public CucumberMessageBroker(IObjectContainer objectContainer) { _objectContainer = objectContainer; + RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); } public void Publish(ReqnrollCucumberMessage message) { var _traceListener = _objectContainer.Resolve(); - //TODO: find a way to populate this list a single time - var registeredSinks = _objectContainer.ResolveAll().ToList(); - - foreach (var sink in registeredSinks) + foreach (var sink in RegisteredSinks.Value) { - _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}"); + _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}: {message.Envelope.Content()}"); sink.Publish(message); } @@ -49,8 +47,6 @@ public void Publish(ReqnrollCucumberMessage message) // using an empty CucumberMessage to indicate completion public void Complete(string cucumberMessageSource) { - var registeredSinks = _objectContainer.ResolveAll().ToList(); - var _traceListener = _objectContainer.Resolve(); var completionMessage = new ReqnrollCucumberMessage @@ -58,7 +54,7 @@ public void Complete(string cucumberMessageSource) CucumberMessageSource = cucumberMessageSource }; - foreach (var sink in registeredSinks) + foreach (var sink in RegisteredSinks.Value) { _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); diff --git a/Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMessages/CucumberMessageExtensions.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageExtensions.cs rename to Reqnroll/CucumberMessages/CucumberMessageExtensions.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/CucumberMessageFactory.cs similarity index 86% rename from Reqnroll/CucumberMesssages/CucumberMessageFactory.cs rename to Reqnroll/CucumberMessages/CucumberMessageFactory.cs index 4afbb55ab..b0a1a33ec 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageFactory.cs @@ -19,25 +19,25 @@ namespace Reqnroll.CucumberMessages { internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(FeatureEventProcessor featureState, FeatureStartedEvent featureStartedEvent) + public static TestRunStarted ToTestRunStarted(FeatureStartedEvent featureStartedEvent) { return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); } - public static TestRunFinished ToTestRunFinished(FeatureEventProcessor featureState, FeatureFinishedEvent featureFinishedEvent) + public static TestRunFinished ToTestRunFinished(bool testRunStatus, FeatureFinishedEvent testRunFinishedEvent) { - return new TestRunFinished(null, featureState.Success, Converters.ToTimestamp(featureFinishedEvent.Timestamp), null); + return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(testRunFinishedEvent.Timestamp), null); } - internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); - foreach (var stepState in scenarioState.Steps) + foreach (var stepState in testCaseTracker.Steps) { switch (stepState) { - case ScenarioStepProcessor _: - var testStep = CucumberMessageFactory.ToPickleTestStep(scenarioState, stepState as ScenarioStepProcessor); + case TestStepProcessor _: + var testStep = CucumberMessageFactory.ToPickleTestStep(testCaseTracker, stepState as TestStepProcessor); testSteps.Add(testStep); break; case HookStepProcessor _: @@ -50,19 +50,19 @@ internal static TestCase ToTestCase(ScenarioEventProcessor scenarioState, Scenar } var testCase = new TestCase ( - scenarioState.TestCaseID, - scenarioState.PickleID, + testCaseTracker.TestCaseId, + testCaseTracker.PickleId, testSteps ); return testCase; } - internal static TestCaseStarted ToTestCaseStarted(ScenarioEventProcessor scenarioState, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCaseStarted ToTestCaseStarted(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { - return new TestCaseStarted(0, scenarioState.TestCaseStartedID, scenarioState.TestCaseID, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); + return new TestCaseStarted(0, testCaseTracker.TestCaseStartedId, testCaseTracker.TestCaseId, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); } - internal static TestCaseFinished ToTestCaseFinished(ScenarioEventProcessor scenarioState, ScenarioFinishedEvent scenarioFinishedEvent) + internal static TestCaseFinished ToTestCaseFinished(TestCaseCucumberMessageTracker testCaseTracker, ScenarioFinishedEvent scenarioFinishedEvent) { - return new TestCaseFinished(scenarioState.TestCaseStartedID, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); + return new TestCaseFinished(testCaseTracker.TestCaseStartedId, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); } internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) { @@ -120,7 +120,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToPickleTestStep(ScenarioEventProcessor scenarioState, ScenarioStepProcessor stepState) + internal static TestStep ToPickleTestStep(TestCaseCucumberMessageTracker tracker, TestStepProcessor stepState) { bool bound = stepState.Bound; bool ambiguous = stepState.Ambiguous; @@ -150,7 +150,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); } - internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(TestStepProcessor stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -158,7 +158,7 @@ internal static TestStepStarted ToTestStepStarted(ScenarioStepProcessor stepStat Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(ScenarioStepProcessor stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(TestStepProcessor stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, @@ -185,7 +185,7 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) { // find the Hook message at the Feature level var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); - var hookId = hookStepState.parentScenario.FeatureState.HookDefinitionsByPattern[hookCacheKey]; + var hookId = hookStepState.ParentTestCase.StepDefinitionsByPattern[hookCacheKey]; return new TestStep( hookId, @@ -204,7 +204,7 @@ internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepPr return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } - internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, AttachmentAddedEventWrapper attachmentAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, AttachmentAddedEventWrapper attachmentAddedEventWrapper) { return new Attachment( Base64EncodeFile(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), @@ -216,7 +216,7 @@ internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProc attachmentAddedEventWrapper.TestCaseStepID, null); } - internal static Attachment ToAttachment(ScenarioEventProcessor scenarioEventProcessor, OutputAddedEventWrapper outputAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, OutputAddedEventWrapper outputAddedEventWrapper) { return new Attachment( outputAddedEventWrapper.OutputAddedEvent.Text, @@ -340,7 +340,7 @@ public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding st public static string CanonicalizeHookBinding(IHookBinding hookBinding) { string signature = GenerateSignature(hookBinding); - return $"{hookBinding.Method.Type.Name}.{hookBinding.Method.Name}({signature})"; + return $"{hookBinding.Method.Type.FullName}.{hookBinding.Method.Name}({signature})"; } private static string GenerateSignature(IBinding stepDefinition) diff --git a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs similarity index 50% rename from Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 7bd7a086f..4ea98657f 100644 --- a/Reqnroll/CucumberMesssages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -5,6 +5,9 @@ using Reqnroll.UnitTestProvider; using System.Collections.Concurrent; using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.Linq; namespace Reqnroll.CucumberMessages { @@ -12,7 +15,8 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private ICucumberMessageBroker broker; private IObjectContainer objectContainer; - private ConcurrentDictionary featureProcessorsByFeatureName = new(); + private ConcurrentDictionary StartedFeatures = new(); + private ConcurrentDictionary testCaseTrackersById = new(); bool Enabled = false; public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) @@ -46,6 +50,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { + var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; @@ -53,81 +58,75 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) // if true, then it checks with the broker to confirm that a listener/sink has been registered Enabled = broker.Enabled; if (!Enabled) - return; - - var featureEnabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source == null ? false : true; - - var featureProc = new FeatureEventProcessor { - Name = featureName, - Enabled = featureEnabled - }; + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: Broker is disabled for {featureName}."); + return; + } - // todo: need a lock around this - if (!featureProcessorsByFeatureName.TryAdd(featureName, featureProc)) + if (StartedFeatures.ContainsKey(featureName)) { - // This feature has already been started by another thread (executing a different scenario) - var featureState_alreadyrunning = featureProcessorsByFeatureName[featureName]; - featureState_alreadyrunning.workerThreadMarkers.Push(1); // add a marker that this thread is active as well - - // None of the rest of this method should be executed + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName} already started"); return; } - var traceListener = objectContainer.Resolve(); traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); - - if (!featureEnabled) - return; - - ProcessEvent(featureStartedEvent, featureName); + var ft = new FeatureTracker(featureStartedEvent); + if (StartedFeatures.TryAdd(featureName, ft)) + { + foreach (var msg in ft.StaticMessages) + { + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + } } private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { + var traceListener = objectContainer.Resolve(); if (!Enabled) + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Broker is disabled for {featureFinishedEvent.FeatureContext.FeatureInfo.Title}."); return; - - + } var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; - var featureProcessor = featureProcessorsByFeatureName[featureName]; + var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); - lock (featureProcessor) + // IF all TestCaseCucumberMessageTrackers are done, then send the messages to the CucumberMessageBroker + if (featureTestCases.All(tc => tc.Finished)) { - // Remove the worker thread marker for this thread - featureProcessor.workerThreadMarkers.TryPop(out int result); - - // Check if there are other threads still working on this feature - if (featureProcessor.workerThreadMarkers.TryPeek(out result)) - { - // There are other threads still working on this feature, so we won't publish the TestRunFinished message just yet - return; - } - featureProcessor.Finished = true; + var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); + var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + broker.Complete(featureName); } - - - if (!featureProcessor.Enabled) - return; - - ProcessEvent(featureFinishedEvent, featureName); - - foreach (var message in featureProcessor.Messages) + else { - broker.Publish(message); + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Error: {featureTestCases.Count(tc => !tc.Finished)} test cases not marked as finished for Feature {featureName}. TestRunFinished event will not be sent."); } - broker.Complete(featureName); + // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? + } private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { if (!Enabled) return; - - + var traceListener = objectContainer.Resolve(); var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(scenarioStartedEvent, featureName); + var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); + testCaseTrackersById.TryAdd(id, tccmt); + tccmt.ProcessEvent(scenarioStartedEvent); + } + else + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker not available"); + throw new ApplicationException("FeatureTracker not available"); + } } private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) @@ -135,9 +134,13 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished if (!Enabled) return; + var tccmt = testCaseTrackersById[scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(scenarioFinishedEvent); - var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(scenarioFinishedEvent, featureName); + foreach (var msg in tccmt.TestCaseCucumberMessages()) + { + broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + } } private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) @@ -145,9 +148,8 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) if (!Enabled) return; - - var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(stepStartedEvent, featureName); + var tccmt = testCaseTrackersById[stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(stepStartedEvent); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) @@ -155,9 +157,8 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (!Enabled) return; - - var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; - ProcessEvent(stepFinishedEvent, featureName); + var tccmt = testCaseTrackersById[stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(stepFinishedEvent); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) @@ -165,19 +166,17 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - - var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookBindingStartedEvent, featureName); + var tccmt = testCaseTrackersById[hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(hookBindingStartedEvent); } - private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingEvent) + private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { if (!Enabled) return; - - var featureName = hookBindingEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; - ProcessEvent(hookBindingEvent, featureName); + var tccmt = testCaseTrackersById[hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(hookBindingFinishedEvent); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) @@ -185,9 +184,8 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (!Enabled) return; - - var featureName = attachmentAddedEvent.FeatureName; - ProcessEvent(attachmentAddedEvent, featureName); + var tccmt = testCaseTrackersById[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(attachmentAddedEvent); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) @@ -195,22 +193,8 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (!Enabled) return; - - ProcessEvent(outputAddedEvent, outputAddedEvent.FeatureName); - } - - - private void ProcessEvent(ExecutionEvent anEvent, string featureName) - { - if (!Enabled) - return; - - - var featureProcessor = featureProcessorsByFeatureName[featureName]; - if (!featureProcessor.Enabled) - return; - - featureProcessor.ProcessEvent(anEvent); + var tccmt = testCaseTrackersById[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + tccmt.ProcessEvent(outputAddedEvent); } } } diff --git a/Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/CucumberMessageTransformer.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageTransformer.cs rename to Reqnroll/CucumberMessages/CucumberMessageTransformer.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/CucumberMessageVisitor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/CucumberMessageVisitor.cs diff --git a/Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs similarity index 100% rename from Reqnroll/CucumberMesssages/CucumberMessage_TraversalVisitorBase.cs rename to Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs diff --git a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs similarity index 94% rename from Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs rename to Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs index 07a15b569..12fb81ac1 100644 --- a/Reqnroll/CucumberMesssages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs @@ -1,4 +1,4 @@ -using Reqnroll.CucumberMesssages; +using Reqnroll.CucumberMessages; using System; using System.Collections.Generic; using System.Text; diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs new file mode 100644 index 000000000..0b6ac7f37 --- /dev/null +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -0,0 +1,104 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Reqnroll.CucumberMessages +{ + public class FeatureTracker + { + internal IEnumerable StaticMessages; + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + public IIdGenerator IDGenerator { get; set; } + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + internal Dictionary StepDefinitionsByPattern = new(); + public string FeatureName { get; set; } + public bool Enabled { get; private set; } + + public FeatureTracker(FeatureStartedEvent featureStartedEvent) + { + FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + Enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles == null ? false : true; + PreProcessEvent(featureStartedEvent); + } + internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) + { + // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow + StaticMessages = GenerateStaticMessages(featureStartedEvent).ToList(); + } + private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) + { + yield return CucumberMessageFactory.ToMeta(featureStartedEvent); + + Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + yield return Envelope.Create(messageSource); + + + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + yield return Envelope.Create(gherkinDocument); + + + var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + + string lastID = ExtractLastID(pickles); + IDGenerator = IdGeneratorFactory.Create(lastID); + + foreach (var pickle in pickles) + { + yield return Envelope.Create(pickle); + } + + var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) + { + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) + { + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); + yield return Envelope.Create(undefinedParameterType); + } + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + + yield return Envelope.Create(stepDefinition); + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + StepDefinitionsByPattern.Add(hookId, hook.Id); + yield return Envelope.Create(hook); + } + + yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(featureStartedEvent)); + } + private string ExtractLastID(List pickles) + { + return pickles.Last().Id; + } + + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs similarity index 100% rename from Reqnroll/CucumberMesssages/FileExtensionToMIMETypeMap.cs rename to Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs diff --git a/Reqnroll/CucumberMesssages/HookStepProcessor.cs b/Reqnroll/CucumberMessages/HookStepProcessor.cs similarity index 58% rename from Reqnroll/CucumberMesssages/HookStepProcessor.cs rename to Reqnroll/CucumberMessages/HookStepProcessor.cs index 0754bde6f..80a7fc77d 100644 --- a/Reqnroll/CucumberMesssages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMessages/HookStepProcessor.cs @@ -9,24 +9,21 @@ public class HookStepProcessor : StepProcessorBase { public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } - public HookStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public HookStepProcessor(TestCaseCucumberMessageTracker tracker) : base(tracker) { } - public IEnumerable ProcessEvent(HookBindingStartedEvent stepFinishedEvent) + public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { - TestStepID = parentScenario.IdGenerator.GetNewId(); - HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(stepFinishedEvent.HookBinding); - return Enumerable.Empty(); + TestStepID = ParentTestCase.IDGenerator.GetNewId(); + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); } - public IEnumerable ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + public void ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { HookBindingFinishedEvent = hookFinishedEvent; Exception = hookFinishedEvent.HookException; Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; - - return Enumerable.Empty(); } } diff --git a/Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/ICucumberMessageSink.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessageSink.cs rename to Reqnroll/CucumberMessages/ICucumberMessageSink.cs diff --git a/Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ICucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs diff --git a/Reqnroll/CucumberMesssages/IStepProcessor.cs b/Reqnroll/CucumberMessages/IStepProcessor.cs similarity index 100% rename from Reqnroll/CucumberMesssages/IStepProcessor.cs rename to Reqnroll/CucumberMessages/IStepProcessor.cs diff --git a/Reqnroll/CucumberMesssages/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/IdGeneratorFactory.cs similarity index 100% rename from Reqnroll/CucumberMesssages/IdGeneratorFactory.cs rename to Reqnroll/CucumberMessages/IdGeneratorFactory.cs diff --git a/Reqnroll/CucumberMesssages/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/NdjsonSerializer.cs similarity index 100% rename from Reqnroll/CucumberMesssages/NdjsonSerializer.cs rename to Reqnroll/CucumberMessages/NdjsonSerializer.cs diff --git a/Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs similarity index 100% rename from Reqnroll/CucumberMesssages/OutputAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs diff --git a/Reqnroll/CucumberMesssages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs similarity index 98% rename from Reqnroll/CucumberMesssages/PickleJar.cs rename to Reqnroll/CucumberMessages/PickleJar.cs index f8bd2e5a9..df6b70b02 100644 --- a/Reqnroll/CucumberMesssages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace Reqnroll.CucumberMesssages +namespace Reqnroll.CucumberMessages { public class PickleJar { diff --git a/Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs similarity index 100% rename from Reqnroll/CucumberMesssages/ReqnrollCucumberMessage.cs rename to Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs diff --git a/Reqnroll/CucumberMesssages/StepProcessorBase.cs b/Reqnroll/CucumberMessages/StepProcessorBase.cs similarity index 56% rename from Reqnroll/CucumberMesssages/StepProcessorBase.cs rename to Reqnroll/CucumberMessages/StepProcessorBase.cs index 3638404b0..c1d3ec601 100644 --- a/Reqnroll/CucumberMesssages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMessages/StepProcessorBase.cs @@ -5,16 +5,16 @@ namespace Reqnroll.CucumberMessages public class StepProcessorBase : IStepProcessor { public string TestStepID { get; set; } - public string TestCaseStartedID => parentScenario.TestCaseStartedID; + public string TestCaseStartedID => ParentTestCase.TestCaseStartedId; public ScenarioExecutionStatus Status { get; set; } public TimeSpan Duration { get; set; } public Exception Exception { get; set; } - public ScenarioEventProcessor parentScenario; + public TestCaseCucumberMessageTracker ParentTestCase; - public StepProcessorBase(ScenarioEventProcessor parentScenario) + public StepProcessorBase(TestCaseCucumberMessageTracker parentScenario) { - this.parentScenario = parentScenario; + ParentTestCase = parentScenario; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs new file mode 100644 index 000000000..faeb018d5 --- /dev/null +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -0,0 +1,291 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.Events; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; + +namespace Reqnroll.CucumberMessages +{ + /// + /// This class is used to track the execution of Test Cases + /// There will be one instance of this class per gherkin Pickle/TestCase. It will track info from both Feature-level and Scenario-level Execution Events for a single Test Case + /// + public class TestCaseCucumberMessageTracker + { + public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) + { + FeatureName = featureTracker.FeatureName; + Enabled = featureTracker.Enabled; + IDGenerator = featureTracker.IDGenerator; + StepDefinitionsByPattern = featureTracker.StepDefinitionsByPattern; + } + + // Feature FeatureInfo and Pickle ID make up a unique identifier for tracking execution of Test Cases + public string FeatureName { get; set; } + public string PickleId { get; set; } = String.Empty; + public string TestCaseTrackerId { get { return FeatureName + PickleId; } } + public string TestCaseId { get; set; } + public string TestCaseStartedId { get; private set; } + + // When this class is first created (on FeatureStarted), it will not yet be assigned a Scenario/Pickle; + // When a Scenario is started, the Publisher will assign the Scenario to the first UnAssigned TestCaseCucumberMessageTracker it finds + // This property will indicate that state + public bool IsUnassigned { get { return PickleId == String.Empty; } } + + public bool Enabled { get; set; } //This will be false if the feature could not be pickled + + public bool Finished { get; set; } + + + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + public IIdGenerator IDGenerator { get; set; } + + public Dictionary StepsById { get; private set; } = new(); + public Dictionary StepsByEvent { get; private set; } = new(); + public List Steps + { + get + { + return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); + } + } + public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } + + + // This queue holds ExecutionEvents that will be processed in stage 2 + private Queue _events = new(); + + internal IEnumerable StaticMessages; + + // During Post-Processing, this is used to track the most recent TestStepStarted event so that Attachments and Output events can be associated with it + private TestStepStarted mostRecentTestStepStarted; + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + internal Dictionary StepDefinitionsByPattern = new(); + + // Processing of events is handled in two stages. + // Stage 1: As events are recieved, critical information needed right away is extracted and stored in the TestCaseCucumberMessageTracker + // The event is then stored in a queue for processing in stage 2 + // Stage 2: When TestRunFinished is recieved, the messages are processed to generate Cucumber Messages and then sent in a single batch to the broker + internal void ProcessEvent(ExecutionEvent anEvent) + { + _events.Enqueue(anEvent); + if (Enabled) InvokePreProcessEvent(anEvent); + } + private void InvokePreProcessEvent(ExecutionEvent anEvent) + { + switch (anEvent) + { + case FeatureStartedEvent featureStartedEvent: + PreProcessEvent(featureStartedEvent); + break; + case FeatureFinishedEvent featureFinishedEvent: + PreProcessEvent(featureFinishedEvent); + break; + case ScenarioStartedEvent scenarioStartedEvent: + PreProcessEvent(scenarioStartedEvent); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + PreProcessEvent(scenarioFinishedEvent); + break; + case StepStartedEvent stepStartedEvent: + PreProcessEvent(stepStartedEvent); + break; + case StepFinishedEvent stepFinishedEvent: + PreProcessEvent(stepFinishedEvent); + break; + case HookBindingStartedEvent hookBindingStartedEvent: + PreProcessEvent(hookBindingStartedEvent); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + PreProcessEvent(hookBindingFinishedEvent); + break; + case AttachmentAddedEvent attachmentAddedEvent: + PreProcessEvent(attachmentAddedEvent); + break; + case OutputAddedEvent outputAddedEvent: + PreProcessEvent(outputAddedEvent); + break; + default: + throw new NotImplementedException($"Event type {anEvent.GetType().Name} is not supported."); + } + } + + public IEnumerable TestCaseCucumberMessages() + { + // Stage 2 + return _events.Select(e => InvokePostProcessEvent(e)).SelectMany(x => x); + } + + private IEnumerable InvokePostProcessEvent(ExecutionEvent anEvent) + { + return anEvent switch + { + FeatureStartedEvent featureStartedEvent => PostProcessEvent(featureStartedEvent), + FeatureFinishedEvent featureFinishedEvent => PostProcessEvent(featureFinishedEvent), + ScenarioStartedEvent scenarioStartedEvent => PostProcessEvent(scenarioStartedEvent), + ScenarioFinishedEvent scenarioFinishedEvent => PostProcessEvent(scenarioFinishedEvent), + StepStartedEvent stepStartedEvent => PostProcessEvent(stepStartedEvent), + StepFinishedEvent stepFinishedEvent => PostProcessEvent(stepFinishedEvent), + HookBindingStartedEvent hookBindingStartedEvent => PostProcessEvent(hookBindingStartedEvent), + HookBindingFinishedEvent hookBindingFinishedEvent => PostProcessEvent(hookBindingFinishedEvent), + AttachmentAddedEvent attachmentAddedEvent => PostProcessEvent(attachmentAddedEvent), + AttachmentAddedEventWrapper attachmentAddedEventWrapper => PostProcessEvent(attachmentAddedEventWrapper), + OutputAddedEvent outputAddedEvent => PostProcessEvent(outputAddedEvent), + OutputAddedEventWrapper outputAddedEventWrapper => PostProcessEvent(outputAddedEventWrapper), + _ => throw new NotImplementedException($"Event type {anEvent.GetType().Name} is not supported."), + }; + } + internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) + { + } + + internal IEnumerable PostProcessEvent(FeatureStartedEvent featureStartedEvent) + { + return Enumerable.Empty(); + } + + internal void PreProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + } + internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + return Enumerable.Empty(); + } + + internal void PreProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + PickleId = scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = TestCaseTrackerId; + TestCaseId = IDGenerator.GetNewId(); + TestCaseStartedId = IDGenerator.GetNewId(); + } + internal IEnumerable PostProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); + var TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); + yield return Envelope.Create(TestCase); + yield return Envelope.Create(TestCaseStarted); + } + + internal void PreProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + Finished = true; + } + internal IEnumerable PostProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + ScenarioExecutionStatus = scenarioFinishedEvent.ScenarioContext.ScenarioExecutionStatus; + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinishedEvent)); + } + + internal void PreProcessEvent(StepStartedEvent stepStartedEvent) + { + var stepState = new TestStepProcessor(this); + + stepState.ProcessEvent(stepStartedEvent); + StepsById.Add(stepState.PickleStepID, stepState); + StepsByEvent.Add(stepStartedEvent, stepState); + } + internal IEnumerable PostProcessEvent(StepStartedEvent stepStartedEvent) + { + var stepState = StepsById[stepStartedEvent.StepContext.StepInfo.PickleStepId]; + var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as TestStepProcessor, stepStartedEvent); + mostRecentTestStepStarted = stepStarted; + yield return Envelope.Create(stepStarted); + } + internal void PreProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; + stepState.ProcessEvent(stepFinishedEvent); + StepsByEvent.Add(stepFinishedEvent, stepState); + } + internal IEnumerable PostProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepState as TestStepProcessor, stepFinishedEvent)); + } + + internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) + return; + var step = new HookStepProcessor(this); + step.ProcessEvent(hookBindingStartedEvent); + StepsByEvent.Add(hookBindingStartedEvent, step); + + } + internal IEnumerable PostProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; + var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); + mostRecentTestStepStarted = hookStepStarted; + yield return Envelope.Create(hookStepStarted); + } + + internal void PreProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps + if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature + || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + return; + + var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); + step.ProcessEvent(hookBindingFinishedEvent); + StepsByEvent.Add(hookBindingFinishedEvent, step); + } + internal IEnumerable PostProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + var hookStepProcessor = FindMatchingHookStartedEvent(hookBindingFinishedEvent); + + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); + } + internal void PreProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, ""); + _events.Enqueue(attachmentExecutionEventWrapper); + } + internal IEnumerable PostProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + return Enumerable.Empty(); + } + internal IEnumerable PostProcessEvent(AttachmentAddedEventWrapper attachmentAddedEventWrapper) + { + attachmentAddedEventWrapper.TestCaseStepID = mostRecentTestStepStarted.TestStepId; + attachmentAddedEventWrapper.TestCaseStartedID = mostRecentTestStepStarted.TestCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); + + } + internal void PreProcessEvent(OutputAddedEvent outputAddedEvent) + { + var pickleStepId = ""; + var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); + _events.Enqueue(outputExecutionEventWrapper); + } + + internal IEnumerable PostProcessEvent(OutputAddedEvent outputAddedEvent) + { + return Enumerable.Empty(); + } + + internal IEnumerable PostProcessEvent(OutputAddedEventWrapper outputAddedEventWrapper) + { + outputAddedEventWrapper.TestCaseStepID = mostRecentTestStepStarted.TestStepId; ; + outputAddedEventWrapper.TestCaseStartedID = mostRecentTestStepStarted.TestCaseStartedId; + yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); + } + private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); + } + + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs b/Reqnroll/CucumberMessages/TestStepProcessor.cs similarity index 77% rename from Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs rename to Reqnroll/CucumberMessages/TestStepProcessor.cs index d13f26267..dce2ad41c 100644 --- a/Reqnroll/CucumberMesssages/ScenarioStepProcessor.cs +++ b/Reqnroll/CucumberMessages/TestStepProcessor.cs @@ -15,11 +15,11 @@ public class StepArgument public string Type; } - public class ScenarioStepProcessor : StepProcessorBase + public class TestStepProcessor : StepProcessorBase { private StepStartedEvent stepStartedEvent; - public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base(parentScenarioState) + public TestStepProcessor(TestCaseCucumberMessageTracker parentTracker) : base(parentTracker) { } @@ -33,24 +33,19 @@ public ScenarioStepProcessor(ScenarioEventProcessor parentScenarioState) : base( public List StepArguments { get; set; } - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) + internal void ProcessEvent(StepStartedEvent stepStartedEvent) { this.stepStartedEvent = stepStartedEvent; - TestStepID = parentScenario.IdGenerator.GetNewId(); - return Enumerable.Empty(); + TestStepID = ParentTestCase.IDGenerator.GetNewId(); + PickleStepID = stepStartedEvent.StepContext.StepInfo.PickleStepId; } private string FindStepDefIDByStepPattern(string canonicalizedStepPattern) { - return parentScenario.FeatureState.StepDefinitionsByPattern[canonicalizedStepPattern]; + return ParentTestCase.StepDefinitionsByPattern[canonicalizedStepPattern]; } - private string FindPickleStepIDByStepText(string stepText) - { - return parentScenario.FeatureState.PicklesByScenarioName[parentScenario.Name].Steps.Where(st => st.Text == stepText).First().Id; - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) + internal void ProcessEvent(StepFinishedEvent stepFinishedEvent) { var bindingMatch = stepFinishedEvent.StepContext?.StepInfo?.BindingMatch; Bound = !(bindingMatch == null || bindingMatch == BindingMatch.NonMatching); @@ -59,8 +54,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) CanonicalizedStepPattern = Bound ? CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinition) : ""; StepDefinitionId = Bound ? FindStepDefIDByStepPattern(CanonicalizedStepPattern) : null; - PickleStepID = FindPickleStepIDByStepText(stepFinishedEvent.StepContext.StepInfo.Text); - Duration = stepFinishedEvent.Timestamp - stepStartedEvent.Timestamp; Status = stepFinishedEvent.StepContext.Status; @@ -81,7 +74,6 @@ internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() : Enumerable.Empty().ToList(); - return Enumerable.Empty(); } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs b/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs deleted file mode 100644 index 710b29ab5..000000000 --- a/Reqnroll/CucumberMesssages/FeatureEventProcessor.cs +++ /dev/null @@ -1,259 +0,0 @@ -using Gherkin.CucumberMessages; -using Io.Cucumber.Messages.Types; -using Reqnroll.Bindings; -using Reqnroll.Events; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Reqnroll.CucumberMessages -{ - public class FeatureEventProcessor - { - public string Name { get; set; } - public bool Enabled { get; set; } //This will be false if the feature could not be pickled - - // These two flags are used to avoid duplicate messages being sent when Scenarios within Features are run concurrently - // and multiple FeatureStartedEvent and FeatureFinishedEvent events are fired - public bool Started { get; set; } - public bool Finished { get; set; } - - public bool Success - { - get - { - return Enabled && Finished && ScenarioName2ScenarioProcessorMap.Values.All(s => s.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); - } - } - - // ID Generator to use when generating IDs for TestCase messages and beyond - // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID - // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. - public IIdGenerator IDGenerator { get; set; } - - //Lookup tables - // - // These three dictionaries hold the mapping of steps, hooks, and pickles to their IDs - // These should only be produced by the first FeatureStartedEvent that this FeatureEventProcessor receives (it might receive multiple if the scenario is run concurrently) - // therefore these are ConcurrentDictionary and we us the TryAdd method on them to only add each mapping once - public ConcurrentDictionary StepDefinitionsByPattern = new(); - public ConcurrentDictionary HookDefinitionsByPattern = new(); - - //TODO: fix this; there will be multiple Pickles with the same scenario name when executing Example table rows - public ConcurrentDictionary PicklesByScenarioName = new(); - - //TODO: Fix this for thread-safety; there will be multiple active Scenarios with the same name when executing Example table rows in parallel - // Scenario event processors by scenario name; - public Dictionary ScenarioName2ScenarioProcessorMap = new(); - - // The list of Cucumber Messages that are ready to be sent to the broker for distribution to consumers - public ConcurrentQueue Messages = new(); - - // A set of markers that represent the worker threads that are currently processing events for this feature. - // Once the last worker thread marker is removed, the Messages are then sent to the broker - public ConcurrentStack workerThreadMarkers = new(); - - internal void ProcessEvent(ExecutionEvent anEvent) - { - foreach (Envelope e in DispatchEvent(anEvent)) - { - Messages.Enqueue(new ReqnrollCucumberMessage { CucumberMessageSource = Name, Envelope = e }); - } - } - private IEnumerable DispatchEvent(ExecutionEvent anEvent) - { - return anEvent switch - { - FeatureStartedEvent featureStartedEvent => ProcessEvent(featureStartedEvent), - FeatureFinishedEvent featureFinishedEvent => ProcessEvent(featureFinishedEvent), - ScenarioStartedEvent scenarioStartedEvent => ProcessEvent(scenarioStartedEvent), - ScenarioFinishedEvent scenarioFinishedEvent => ProcessEvent(scenarioFinishedEvent), - StepStartedEvent stepStartedEvent => ProcessEvent(stepStartedEvent), - StepFinishedEvent stepFinishedEvent => ProcessEvent(stepFinishedEvent), - HookBindingStartedEvent hookBindingStartedEvent => ProcessEvent(hookBindingStartedEvent), - HookBindingFinishedEvent hookBindingFinishedEvent => ProcessEvent(hookBindingFinishedEvent), - AttachmentAddedEvent attachmentAddedEvent => ProcessEvent(attachmentAddedEvent), - OutputAddedEvent outputAddedEvent => ProcessEvent(outputAddedEvent), - _ => throw new NotImplementedException(), - }; - } - - internal IEnumerable ProcessEvent(FeatureStartedEvent featureStartedEvent) - { - yield return CucumberMessageFactory.ToMeta(featureStartedEvent); - - Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - yield return Envelope.Create(messageSource); - - - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - yield return Envelope.Create(gherkinDocument); - - - var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); - var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); - - string lastID = ExtractLastID(pickles); - IDGenerator = IdGeneratorFactory.Create(lastID); - - foreach (var pickle in pickles) - { - PicklesByScenarioName.TryAdd(pickle.Name, pickle); - yield return Envelope.Create(pickle); - } - - var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - - foreach (var stepTransform in bindingRegistry.GetStepTransformations()) - { - var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); - yield return Envelope.Create(parameterType); - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) - { - var errmsg = binding.ErrorMessage; - if (errmsg.Contains("Undefined parameter type")) - { - var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; - var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); - yield return Envelope.Create(undefinedParameterType); - } - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) - { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id); - - yield return Envelope.Create(stepDefinition); - } - - foreach (var hookBinding in bindingRegistry.GetHooks()) - { - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); - var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - HookDefinitionsByPattern.TryAdd(hookId, hook.Id); - yield return Envelope.Create(hook); - } - - yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(this, featureStartedEvent)); - - } - - internal IEnumerable ProcessEvent(FeatureFinishedEvent featureFinishedEvent) - { - yield return Envelope.Create(CucumberMessageFactory.ToTestRunFinished(this, featureFinishedEvent)); - } - - private string GenerateScenarioKey(ScenarioInfo scenarioInfo) - { - var scenarioArguments = new List(); - foreach (string v in scenarioInfo.Arguments.Values) - { - scenarioArguments.Add(v); - } - return scenarioInfo.Title - + scenarioArguments - + scenarioInfo.CombinedTags; - } - internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) - { - var scenarioName = scenarioStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = new ScenarioEventProcessor(scenarioStartedEvent.ScenarioContext, this); - ScenarioName2ScenarioProcessorMap.Add(scenarioName, scenarioEP); - - foreach (var e in scenarioEP.ProcessEvent(scenarioStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) - { - var scenarioName = scenarioFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(scenarioFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) - { - var scenarioName = stepStartedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(stepStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) - { - var scenarioName = stepFinishedEvent.ScenarioContext.ScenarioInfo.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(stepFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(HookBindingStartedEvent hookStartedEvent) - { - var scenarioName = hookStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookStartedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - var scenarioName = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.Title; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(hookBindingFinishedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) - { - var scenarioName = attachmentAddedEvent.ScenarioName; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - - foreach (var e in scenarioEP.ProcessEvent(attachmentAddedEvent)) - { - yield return e; - } - } - - internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) - { - var scenarioName = outputAddedEvent.ScenarioName; - var scenarioEP = ScenarioName2ScenarioProcessorMap[scenarioName]; - foreach (var e in scenarioEP.ProcessEvent(outputAddedEvent)) - { - yield return e; - } - } - - private string ExtractLastID(List pickles) - { - return pickles.Last().Id; - } - - } -} \ No newline at end of file diff --git a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs b/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs deleted file mode 100644 index d20816a1e..000000000 --- a/Reqnroll/CucumberMesssages/ScenarioEventProcessor.cs +++ /dev/null @@ -1,205 +0,0 @@ -using Cucumber.Messages; -using Gherkin.CucumberMessages; -using Io.Cucumber.Messages.Types; -using Reqnroll.Bindings; -using Reqnroll.Events; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Reqnroll.CucumberMessages -{ - - public class ScenarioEventProcessor - { - internal readonly IIdGenerator IdGenerator; - internal readonly FeatureEventProcessor FeatureState; - - public string TestCaseStartedID; - public string Name { get; set; } - public string TestCaseID { get; set; } - public string PickleID { get; set; } - public Pickle Pickle { get; set; } - private TestCase TestCase; - - private TestCaseStarted TestCaseStarted; - - // we will hold all scenario and step events here until the scenario is finished, then use them to generate TestCase and TestStep messages - private Queue _events = new(); - - public Dictionary StepsByEvent { get; private set; } = new(); - public List Steps - { - get - { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent || kvp.Key is HookBindingFinishedEvent).Select(kvp => kvp.Value).ToList(); - } - } - public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } - - public ScenarioEventProcessor(IScenarioContext context, FeatureEventProcessor featureState) - { - IdGenerator = featureState.IDGenerator; - FeatureState = featureState; - - Name = context.ScenarioInfo.Title; - TestCaseID = IdGenerator.GetNewId(); - Pickle = featureState.PicklesByScenarioName[Name]; - PickleID = featureState.PicklesByScenarioName[Name].Id; - } - - internal IEnumerable ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) - { - TestCaseStartedID = IdGenerator.GetNewId(); - _events.Enqueue(scenarioStartedEvent); - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - _events.Enqueue(hookBindingStartedEvent); - var step = new HookStepProcessor(this); - step.ProcessEvent(hookBindingStartedEvent); - StepsByEvent.Add(hookBindingStartedEvent, step); - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) - return Enumerable.Empty(); - - _events.Enqueue(hookBindingFinishedEvent); - var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); - step.ProcessEvent(hookBindingFinishedEvent); - StepsByEvent.Add(hookBindingFinishedEvent, step); - return Enumerable.Empty(); - } - - private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) - { - return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); - } - - internal IEnumerable ProcessEvent(StepStartedEvent stepStartedEvent) - { - _events.Enqueue(stepStartedEvent); - - var stepState = new ScenarioStepProcessor(this); - StepsByEvent.Add(stepStartedEvent, stepState); - stepState.ProcessEvent(stepStartedEvent); - - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(StepFinishedEvent stepFinishedEvent) - { - _events.Enqueue(stepFinishedEvent); - var stepState = FindMatchingStepStartEvent(stepFinishedEvent); - stepState.ProcessEvent(stepFinishedEvent); - StepsByEvent.Add(stepFinishedEvent, stepState); - - return Enumerable.Empty(); - } - internal IEnumerable ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) - { - //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == attachmentAddedEvent.StepText).ToList().LastOrDefault(); - var pickleStepId = ""; - - var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper(attachmentAddedEvent, pickleStepId); - _events.Enqueue(attachmentExecutionEventWrapper); - - return Enumerable.Empty(); - } - - internal IEnumerable ProcessEvent(OutputAddedEvent outputAddedEvent) - { - //var matchingPickleStep = Pickle.Steps.Where(st => st.Text == outputAddedEvent.StepText).ToList().LastOrDefault(); - //var pickleStepId = matchingPickleStep.Id; - - var pickleStepId = ""; - var outputExecutionEventWrapper = new OutputAddedEventWrapper(outputAddedEvent, pickleStepId); - _events.Enqueue(outputExecutionEventWrapper); - - return Enumerable.Empty(); - } - - private ScenarioStepProcessor FindMatchingStepStartEvent(StepFinishedEvent stepFinishedEvent) - { - return StepsByEvent.Where(kvp => kvp.Key is StepStartedEvent).Where(kvp => ((StepStartedEvent)(kvp.Key)).StepContext == stepFinishedEvent.StepContext).OrderBy(kvp => ((StepStartedEvent)(kvp.Key)).Timestamp).Select(kvp => kvp.Value as ScenarioStepProcessor).LastOrDefault(); - } - - internal IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) - { - _events.Enqueue(scenarioFinishedEvent); - TestStepStarted mostRecentTestStepStarted = null; - - while (_events.Count > 0) - { - var executionEvent = _events.Dequeue(); - - switch (executionEvent) - { - case ScenarioStartedEvent scenarioStartedEvent: - TestCase = CucumberMessageFactory.ToTestCase(this, scenarioStartedEvent); - TestCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, scenarioStartedEvent); - yield return Envelope.Create(TestCase); - yield return Envelope.Create(TestCaseStarted); - break; - case ScenarioFinishedEvent scenarioFinished: - ScenarioExecutionStatus = scenarioFinished.ScenarioContext.ScenarioExecutionStatus; - yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this, scenarioFinished)); - break; - case StepStartedEvent stepStartedEvent: - var stepState = StepsByEvent[stepStartedEvent]; - var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as ScenarioStepProcessor, stepStartedEvent); - mostRecentTestStepStarted = stepStarted; - yield return Envelope.Create(stepStarted); - break; - case StepFinishedEvent stepFinishedEvent: - var stepFinishedState = StepsByEvent[stepFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepFinishedState as ScenarioStepProcessor, stepFinishedEvent)); - break; - case HookBindingStartedEvent hookBindingStartedEvent: - var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; - var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); - mostRecentTestStepStarted = hookStepStarted; - yield return Envelope.Create(hookStepStarted); - break; - case HookBindingFinishedEvent hookBindingFinishedEvent: - var hookStepProcessor = StepsByEvent[hookBindingFinishedEvent]; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); - break; - case AttachmentAddedEventWrapper attachmentAddedEventWrapper: - // find the TestCaseStepId and testCaseStartedId - var testStepID = mostRecentTestStepStarted.TestStepId; - var testCaseStartedId = TestCaseStarted.Id; - attachmentAddedEventWrapper.TestCaseStepID = testStepID; - attachmentAddedEventWrapper.TestCaseStartedID = testCaseStartedId; - yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, attachmentAddedEventWrapper)); - break; - case OutputAddedEventWrapper outputAddedEventWrapper: - // find the TestCaseStepId and testCaseStartedId - testStepID = mostRecentTestStepStarted.TestStepId; - testCaseStartedId = TestCaseStarted.Id; - outputAddedEventWrapper.TestCaseStepID = testStepID; - outputAddedEventWrapper.TestCaseStartedID = testCaseStartedId; - yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); - break; - // add more cases for other event types - default: - throw new ArgumentException($"Invalid event type: {executionEvent.GetType()}"); - } - } - - } - } -} \ No newline at end of file diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index a3f2a0003..e211c8524 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -211,7 +211,7 @@ public interface IExecutionOutputEvent public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string Text { get; } - public string FeatureName { get; } + public FeatureInfo FeatureInfo { get; } public string ScenarioName { get; } public string StepText { get; } @@ -220,31 +220,24 @@ public OutputAddedEvent(string text) Text = text; } - public OutputAddedEvent(string text, string featureName, string scenarioName, string stepText) : this(text) + public OutputAddedEvent(string text, FeatureInfo featureInfo) : this(text) { - FeatureName = featureName; - ScenarioName = scenarioName; - StepText = stepText; + FeatureInfo = featureInfo; } } public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } - public string FeatureName { get; } - public string ScenarioName { get; } - public string StepText { get; } - + public FeatureInfo FeatureInfo { get; } public AttachmentAddedEvent(string filePath) { FilePath = filePath; } - public AttachmentAddedEvent(string filePath, string featureName, string scenarioName, string stepText) : this(filePath) + public AttachmentAddedEvent(string filePath, FeatureInfo featureInfo) : this(filePath) { - FeatureName = featureName; - ScenarioName = scenarioName; - StepText = stepText; + FeatureInfo = featureInfo; } } } diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index 5235ec7c7..42a157b3c 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -17,9 +17,10 @@ public class FeatureInfo public CultureInfo Language { get; private set; } - // TODO: Add this to the constructor and update all the related tests // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } + // This holds the unique identifier for the tracker instance that is being used to generate cucumber messages for this Test Case + public string CucumberMessages_TestCaseTrackerId { get; set; } public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index 604d08d88..477bed81e 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -20,11 +20,9 @@ public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecuti public void WriteLine(string message) { - var featureName = contextManager.FeatureContext.FeatureInfo?.Title; - var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext?.StepInfo?.Text; + var featureInfo = contextManager.FeatureContext.FeatureInfo; - _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureName, scenarioName, stepText)); + _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureInfo)); _traceListener.WriteToolOutput(message); } @@ -35,10 +33,8 @@ public void WriteLine(string format, params object[] args) public void AddAttachment(string filePath) { - var featureName = contextManager.FeatureContext.FeatureInfo?.Title; - var scenarioName = contextManager.ScenarioContext.ScenarioInfo?.Title; - var stepText = contextManager.StepContext?.StepInfo?.Text; - _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureName, scenarioName, stepText)); + var featureInfo = contextManager.FeatureContext.FeatureInfo; + _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureInfo)); _reqnrollAttachmentHandler.AddAttachment(filePath); } } diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index d38ef61af..3071a72fc 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,7 @@ - + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d9cb9cece..f7c6a601b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -42,12 +42,8 @@ public void SmokeTest() AddFeatureFile(""" Feature: Cucumber Messages Smoke Test - @some-tag - Scenario: Log JSON - When the following string is attached as "application/json": - ``` - {"message": "The big question", "foo": "bar"} - ``` + Scenario: Eating Cukes + When I eat 5 cukes """); AddPassingStepBinding("When"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index aea923ac8..4f29059ea 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -132,7 +132,7 @@ - + From 458ac5a29eb03a6a6cde6eb0cd4b63175a28d0d5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:08:56 -0500 Subject: [PATCH 150/218] Most tests passing. Need to clean up Specs. --- .../CucumberMessage_TraversalVisitorBase.cs | 38 +++++++++---------- Reqnroll/CucumberMessages/FeatureTracker.cs | 4 +- Reqnroll/Reqnroll.nuspec | 1 + .../CCK/examples-tables/examples_tables.cs | 2 +- .../CucumberCompatibilityTests.cs | 6 ++- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs index 0bb4ac6d7..a337956af 100644 --- a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs @@ -40,7 +40,7 @@ public virtual void Visit(GherkinDocument gherkinDocument) public virtual void Visit(Feature feature) { OnVisiting(feature); - foreach (var featureChild in feature.Children) + foreach (var featureChild in feature.Children ?? new List()) { Accept(featureChild); } @@ -62,11 +62,11 @@ public virtual void Visit(FeatureChild featureChild) public virtual void Visit(Rule rule) { OnVisiting(rule); - foreach (var ruleChild in rule.Children) + foreach (var ruleChild in rule.Children ?? new List()) { Accept(ruleChild); } - foreach (var tag in rule.Tags) + foreach (var tag in rule.Tags ?? new List()) { Accept(tag); } @@ -87,7 +87,7 @@ public virtual void Visit(Background background) { OnVisiting(background); Accept(background.Location); - foreach (var step in background.Steps) + foreach (var step in background.Steps ?? new List()) { Accept(step); } @@ -98,15 +98,15 @@ public virtual void Visit(Scenario scenario) { OnVisiting(scenario); Accept(scenario.Location); - foreach (var tag in scenario.Tags) + foreach (var tag in scenario.Tags ?? new List()) { Accept(tag); } - foreach (var step in scenario.Steps) + foreach (var step in scenario.Steps ?? new List()) { Accept(step); } - foreach (var example in scenario.Examples) + foreach (var example in scenario.Examples ?? new List()) { Accept(example); } @@ -117,12 +117,12 @@ public virtual void Visit(Examples examples) { OnVisiting(examples); Accept(examples.Location); - foreach (var tag in examples.Tags) + foreach (var tag in examples.Tags ?? new List()) { Accept(tag); } Accept(examples.TableHeader); - foreach (var tableRow in examples.TableBody) + foreach (var tableRow in examples.TableBody ?? new List()) { Accept(tableRow); } @@ -142,7 +142,7 @@ public virtual void Visit(TableRow tableRow) { OnVisiting(tableRow); Accept(tableRow.Location); - foreach (var tableCell in tableRow.Cells) + foreach (var tableCell in tableRow.Cells ?? new List()) { Accept(tableCell); } @@ -166,11 +166,11 @@ public virtual void Visit(Tag tag) public virtual void Visit(Pickle pickle) { OnVisiting(pickle); - foreach (var pickleStep in pickle.Steps) + foreach (var pickleStep in pickle.Steps ?? new List()) { Accept(pickleStep); } - foreach (var tag in pickle.Tags) + foreach (var tag in pickle.Tags ?? new List()) { Accept(tag); } @@ -197,7 +197,7 @@ public virtual void Visit(PickleStepArgument pickleStepArgument) public virtual void Visit(PickleTable pickleTable) { OnVisiting(pickleTable); - foreach (var pickleTableRow in pickleTable.Rows) + foreach (var pickleTableRow in pickleTable.Rows ?? new List()) { Accept(pickleTableRow); } @@ -207,7 +207,7 @@ public virtual void Visit(PickleTable pickleTable) public virtual void Visit(PickleTableRow pickleTableRow) { OnVisiting(pickleTableRow); - foreach (var pickleTableCell in pickleTableRow.Cells) + foreach (var pickleTableCell in pickleTableRow.Cells ?? new List()) { Accept(pickleTableCell); } @@ -229,7 +229,7 @@ public virtual void Visit(PickleTag pickleTag) public virtual void Visit(TestCase testCase) { OnVisiting(testCase); - foreach (var step in testCase.TestSteps) + foreach (var step in testCase.TestSteps ?? new List()) { Accept(step); } @@ -253,7 +253,7 @@ public virtual void Visit(TestCaseFinished testCaseFinished) public virtual void Visit(TestStep testStep) { OnVisiting(testStep); - foreach (var argumentList in testStep.StepMatchArgumentsLists) + foreach (var argumentList in testStep.StepMatchArgumentsLists ?? new List()) { Accept(argumentList); } @@ -385,7 +385,7 @@ public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) { OnVisiting(dataTable); Accept(dataTable.Location); - foreach (var row in dataTable.Rows) + foreach (var row in dataTable.Rows ?? new List()) { Accept(row); } @@ -402,7 +402,7 @@ public virtual void Visit(DocString docString) public virtual void Visit(Group group) { OnVisiting(group); - foreach (var child in group.Children) + foreach (var child in group.Children ?? new List()) { Accept(child); } @@ -456,7 +456,7 @@ public virtual void Visit(StepMatchArgument stepMatchArgument) public virtual void Visit(StepMatchArgumentsList stepMatchArgumentsList) { OnVisiting(stepMatchArgumentsList); - foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments) + foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments ?? new List()) { Accept(stepMatchArgument); } diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs index 0b6ac7f37..18517816d 100644 --- a/Reqnroll/CucumberMessages/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -80,7 +80,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); + if (!StepDefinitionsByPattern.ContainsKey(pattern)) StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); yield return Envelope.Create(stepDefinition); } @@ -89,7 +89,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - StepDefinitionsByPattern.Add(hookId, hook.Id); + if (!StepDefinitionsByPattern.ContainsKey(hookId)) StepDefinitionsByPattern.Add(hookId, hook.Id); yield return Envelope.Create(hook); } diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec index b98dddb40..742ab84f2 100644 --- a/Reqnroll/Reqnroll.nuspec +++ b/Reqnroll/Reqnroll.nuspec @@ -20,6 +20,7 @@ + diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs index a87217d3e..74d5c6cbc 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs @@ -31,7 +31,7 @@ public void WhenIEatCucumbers(int p0) _count -= p0; } - [Then("I should have {int} cuc umbers")] + [Then("I should have {int} cucumbers")] public void ThenIShouldHaveCucumbers(int p0) { if (_count != p0) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index f7c6a601b..4a2bd13aa 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -235,7 +235,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("undefined", "Undefined steps")] [DataRow("unknown-parameter-type", "Unknown Parameter Types")] [DataRow("stack-traces", "Stack traces")] - [DataRow("rules", "Usage of a 'Rule'")] + [DataRow("rules", "Usage of a `Rule`")] public void CCKScenarios(string testName, string featureNameText) { AddCucumberMessagePlugIn(); @@ -255,7 +255,9 @@ public void CCKScenarios(string testName, string featureNameText) validator.ResultShouldPassAllComparisonTests(); validator.ResultShouldPassSanityChecks(); - ConfirmAllTestsRan(null); + + // This is necessary b/c the System Test framework doesn't understand Rules and can't determine the number of expected tests + ConfirmAllTestsRan(testName == "rules" ? 3 : null); } [TestMethod] From 1b8c80f3b50219bae929b3ef2a80465c6f9e2474 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:49:49 -0500 Subject: [PATCH 151/218] Added support for Background steps. Minor refactoring to UnitTestMethodGenerator and ScenarioPartHelper - moved pickle generation to SPH. Add new non-CCK scenario for Backgrounds --- .../Generation/GeneratorConstants.cs | 4 ++ .../Generation/ScenarioPartHelper.cs | 61 +++++++++++++++++-- .../Generation/UnitTestMethodGenerator.cs | 48 +++------------ Reqnroll/CucumberMessages/PickleJar.cs | 1 - .../CCK/background/background.cs | 47 ++++++++++++++ .../CCK/background/background.feature | 17 ++++++ .../CCK/background/background.feature.ndjson | 36 +++++++++++ .../CucumberCompatibilityTests.cs | 1 + ...CucumberMessages.CompatibilityTests.csproj | 6 ++ .../CucumberExpressionIntegrationTests.cs | 2 +- 10 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 3a9a6b081..18f58e348 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -18,5 +18,9 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; + public const string PICKLEID_PARAMETER_NAME = "@pickleId"; + public const string PICKLEID_VARIABLE_NAME = "m_pickleId"; + public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; + public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "pickleStepSequence"; } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 3dd6839a8..4c852e9c0 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -12,8 +12,6 @@ namespace Reqnroll.Generator.Generation { public class ScenarioPartHelper { - private const string BRINE = "PickleJar"; - private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly CodeDomHelper _codeDomHelper; private int _tableCounter; @@ -37,6 +35,7 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) backgroundMethod.Attributes = MemberAttributes.Public; backgroundMethod.Name = GeneratorConstants.BACKGROUND_NAME; + backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); _codeDomHelper.MarkCodeMemberMethodAsAsync(backgroundMethod); @@ -44,13 +43,14 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, background.Location)) { } + AddVariableForPickleStepSequenceFromMethodParameter(backgroundMethod); foreach (var step in background.Steps) { GenerateStep(generationContext, statements, step, null); } backgroundMethod.Statements.AddRange(statements.ToArray()); - + } #region Rule Background Support @@ -96,9 +96,9 @@ public void GenerateStep(TestClassGenerationContext generationContext, List {new CodePrimitiveExpression(formatText)}; + var formatArguments = new List { new CodePrimitiveExpression(formatText) }; formatArguments.AddRange(arguments.Select(id => new CodeVariableReferenceExpression(id))); return new CodeMethodInvokeExpression( @@ -230,5 +234,50 @@ private CodeExpression GetSubstitutedString(string text, ParameterSubstitution p "Format", formatArguments.ToArray()); } + public void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + { + // string m_pickleId = pickleJar.CurrentPickleId; or + // string m_pickleId = @pickleId; + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEID_VARIABLE_NAME, + pickleIdIncludedInParameters ? + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleJar.CurrentPickleId)); + testMethod.Statements.Add(pickleIdVariable); + } + + public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod testMethod) + { + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); + + testMethod.Statements.Add(pickleStepSequence); + } + + public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMethod) + { + // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); + var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + // Right side of the assignment (property access chain) + + new CodeMethodInvokeExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(GeneratorConstants.TESTRUNNER_FIELD), + "FeatureContext" + ), + "FeatureInfo" + ), + "FeatureCucumberMessages" + ), + "PickleJar" + ), + "PickleStepSequenceFor", + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME)) + ); + + testMethod.Statements.Add(pickleStepSequence); + } } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 0d8794649..0dde9c14b 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -18,8 +18,6 @@ namespace Reqnroll.Generator.Generation public class UnitTestMethodGenerator { private const string PICKLEJAR = "PICKLEJAR"; - private const string PICKLEID_PARAMETER_NAME = "@pickleId"; - private const string PICKLEID_VARIABLE_NAME = "m_pickleId"; private const string IGNORE_TAG = "@Ignore"; private const string TESTRUNNER_FIELD = "testRunner"; private readonly CodeDomHelper _codeDomHelper; @@ -212,7 +210,7 @@ private void GenerateTestBody( new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), inheritedTagsExpression, - new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)))); + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -221,40 +219,14 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } - private void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + internal void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) { - var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), PICKLEID_VARIABLE_NAME, - pickleIdIncludedInParameters ? - new CodeVariableReferenceExpression(PICKLEID_PARAMETER_NAME) : - new CodePrimitiveExpression(pickleJar.CurrentPickleId)); - testMethod.Statements.Add(pickleIdVariable); + _scenarioPartHelper.AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, pickleJar); } - private void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) + internal void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) { - // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME, - // Right side of the assignment (property access chain) - - new CodeMethodInvokeExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodeVariableReferenceExpression(TESTRUNNER_FIELD), - "FeatureContext" - ), - "FeatureInfo" - ), - "FeatureCucumberMessages" - ), - "PickleJar" - ), - "PickleStepSequenceFor", - new CodeVariableReferenceExpression(PICKLEID_VARIABLE_NAME)) - ); - - testMethod.Statements.Add(pickleStepSequence); + _scenarioPartHelper.AddVariableForPickleStepSequenceForPickleId(testMethod); } private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { @@ -310,7 +282,8 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex { var backgroundMethodCallExpression = new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), - generationContext.FeatureBackgroundMethod.Name); + generationContext.FeatureBackgroundMethod.Name, + new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)); _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(backgroundMethodCallExpression); statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(backgroundMethodCallExpression)); @@ -322,9 +295,6 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex foreach (var scenarioStep in scenario.Steps) { _scenarioPartHelper.GenerateStep(generationContext, statementsWhenScenarioIsExecuted, scenarioStep, paramToIdentifier); - statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(new CodeMethodInvokeExpression( - new CodeVariableReferenceExpression(PickleStepSequence.PICKLESTEPSEQUENCE_VARIABLE_NAME), - "NextStep"))); } var tagsOfScenarioVariableReferenceExpression = new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME); @@ -433,7 +403,7 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex foreach (var row in examples.TableBody) { var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleJar.CurrentPickleId }); - + _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); _pickleJar.NextPickle(); @@ -497,7 +467,7 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), @"@pickleId")); + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), GeneratorConstants.PICKLEID_PARAMETER_NAME)); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } diff --git a/Reqnroll/CucumberMessages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs index df6b70b02..e6f230cd9 100644 --- a/Reqnroll/CucumberMessages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -45,7 +45,6 @@ public void NextPickle() public class PickleStepSequence { - public static string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; public Pickle CurrentPickle { get; } private int _PickleStepCounter; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs new file mode 100644 index 000000000..d4a5f29d3 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs @@ -0,0 +1,47 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.background +{ + [Binding] + internal class Background + { + private Dictionary accounts = new Dictionary(); + private int total = 0; + + [Given(@"I have ${int} in my {word} account")] + public void AddMoneyToAccount(int amount, string account) + { + accounts[account] = amount; + } + + [When("the accounts are combined")] + public void CombineAccounts() + { + total = accounts.Sum(x => x.Value); + } + + [Then(@"I have ${int}")] + public void CheckTotalBalance(int amount) + { + if(total != amount) throw new ApplicationException("Total balance should be " + amount); + } + + [When(@"I transfer ${int} from {word} to {word}")] + public void TransferMoney(int amount, string from, string to) + { + accounts[from] -= amount; + accounts[to] += amount; + } + + [Then(@"My {word} account has a balance of ${int}")] + public void CheckAccountBalance(string account, int balance) + { + if(accounts[account] != balance) throw new ApplicationException($"Account: {account} balance should be " + balance); + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature new file mode 100644 index 000000000..41424d2e9 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature @@ -0,0 +1,17 @@ +Feature: background + +Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature + +Background: +# set up bank account balance +Given I have $500 in my checking account +And I have $200 in my savings account + +Scenario: Combined Balance + When the accounts are combined + Then I have $700 + +Scenario: Transfer Money + When I transfer $150 from savings to checking + Then My checking account has a balance of $650 + And My savings account has a balance of $50 diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson new file mode 100644 index 000000000..f44c8ac17 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+86e9495e4e6815ab377bdbc6ea2eebd6f89a0537"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} +{"pickle":{"id":"14","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"10","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"11","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"12","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"13","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} +{"pickle":{"id":"20","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"15","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"16","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"17","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"18","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"19","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} +{"stepDefinition":{"id":"21","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} +{"stepDefinition":{"id":"22","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"23","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} +{"stepDefinition":{"id":"24","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} +{"stepDefinition":{"id":"25","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} +{"testRunStarted":{"timestamp":{"seconds":1727192229,"nanos":420947400}}} +{"testCase":{"id":"26","pickleId":"14","testSteps":[{"id":"28","pickleStepId":"10","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"11","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"26","timestamp":{"seconds":1727192229,"nanos":452538900}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"28","timestamp":{"seconds":1727192229,"nanos":455045500}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"28","testStepResult":{"duration":{"seconds":0,"nanos":19933400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":474978900}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"29","timestamp":{"seconds":1727192229,"nanos":476934500}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"29","testStepResult":{"duration":{"seconds":0,"nanos":698600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":477633100}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"30","timestamp":{"seconds":1727192229,"nanos":478014600}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"30","testStepResult":{"duration":{"seconds":0,"nanos":1699400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":479714000}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"31","timestamp":{"seconds":1727192229,"nanos":479992900}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"31","testStepResult":{"duration":{"seconds":0,"nanos":551600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":480544500}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"seconds":1727192229,"nanos":482528500},"willBeRetried":false}} +{"testCase":{"id":"32","pickleId":"20","testSteps":[{"id":"34","pickleStepId":"15","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"17","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"18","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"38","pickleStepId":"19","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"33","testCaseId":"32","timestamp":{"seconds":1727192229,"nanos":495596400}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"34","timestamp":{"seconds":1727192229,"nanos":495781000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"34","testStepResult":{"duration":{"seconds":0,"nanos":278500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496059500}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"35","timestamp":{"seconds":1727192229,"nanos":496095600}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"35","testStepResult":{"duration":{"seconds":0,"nanos":83500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496179100}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"36","timestamp":{"seconds":1727192229,"nanos":496202200}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"36","testStepResult":{"duration":{"seconds":0,"nanos":685000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496887200}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"37","timestamp":{"seconds":1727192229,"nanos":496910500}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"37","testStepResult":{"duration":{"seconds":0,"nanos":512200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497422700}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"38","timestamp":{"seconds":1727192229,"nanos":497441700}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"38","testStepResult":{"duration":{"seconds":0,"nanos":194200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497635900}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":1727192229,"nanos":497750600},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"seconds":1727192229,"nanos":499781200}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 4a2bd13aa..7be0f6340 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -262,6 +262,7 @@ public void CCKScenarios(string testName, string featureNameText) [TestMethod] [DataRow("ambiguous", "ambiguous")] + [DataRow("background", "background")] public void NonCCKScenarios(string testName, string featureNameText) { CCKScenarios(testName, featureNameText); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 4f29059ea..0813739d3 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -12,6 +12,7 @@ + @@ -31,6 +32,8 @@ + + @@ -67,6 +70,9 @@ Always + + + diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 4c140d512..2f4511a66 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -186,7 +186,7 @@ private async Task PerformStepExecution(string methodName, strin await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo("en-US"), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); await engine.OnScenarioStartAsync(); engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null)); - await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); + await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null, "stubPickleStepId"); var contextManager = testThreadContainer.Resolve(); contextManager.ScenarioContext.ScenarioExecutionStatus.Should().Be(ScenarioExecutionStatus.OK, $"should not fail with '{contextManager.ScenarioContext.TestError?.Message}'"); From c6dba89f9772849aa29f69c94a25393c459a5f32 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:32:25 -0500 Subject: [PATCH 152/218] Moved FileSink to be a built-in plug-in --- Reqnroll.sln | 10 - .../CucumberMessages-config-schema.json | 34 +++ .../CucumberMessages/FileSinkConfiguration.cs | 35 ++++ Reqnroll/CucumberMessages/FileSinkPlugin.cs | 195 ++++++++++++++++++ .../DefaultDependencyProvider.cs | 2 + 5 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 Reqnroll/CucumberMessages/CucumberMessages-config-schema.json create mode 100644 Reqnroll/CucumberMessages/FileSinkConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/FileSinkPlugin.cs diff --git a/Reqnroll.sln b/Reqnroll.sln index e0e020419..fe8c30ca0 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,12 +116,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CucumberMessages", "CucumberMessages", "{4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.CompatibilityTests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.CompatibilityTests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "Plugins\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin\Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj", "{58B078D5-37A2-416B-91DA-E5E6BD510062}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -252,10 +248,6 @@ Global {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.Build.0 = Debug|Any CPU {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.ActiveCfg = Release|Any CPU {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.Build.0 = Release|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58B078D5-37A2-416B-91DA-E5E6BD510062}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -294,9 +286,7 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} - {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {5072F73C-8CDD-4B44-B3F8-4212F65C3708} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} - {58B078D5-37A2-416B-91DA-E5E6BD510062} = {4D2A0394-E221-45EE-A5F2-7BECFEBEBB1E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json new file mode 100644 index 000000000..f7fcd8f17 --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "filSinkEnabled": { + "type": "boolean", + "description": "Indicates whether the program is enabled or not" + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether this destination setting is enabled" + }, + "basePath": { + "type": "string", + "description": "The base path for output files" + }, + "outputDirectory": { + "type": "string", + "description": "The directory path where output should go" + } + }, + "required": [ "enabled", "basePath", "outputDirectory" ] + }, + "minItems": 1, + "description": "Array of destination settings" + } + }, + "required": [ "programEnabled", "destinations" ] +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs new file mode 100644 index 000000000..7a56f4233 --- /dev/null +++ b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + internal class FileSinkConfiguration + { + public bool FileSinkEnabled { get; set; } + public List Destinations { get; set; } + + public FileSinkConfiguration() : this(true) { } + public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + { + FileSinkEnabled = fileSinkEnabled; + Destinations = destinations; + } + } + + public class Destination + { + public bool Enabled { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(bool enabled, string basePath, string outputDirectory) + { + Enabled = true; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs new file mode 100644 index 000000000..40baea678 --- /dev/null +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -0,0 +1,195 @@ +#nullable enable + +using Reqnroll.CucumberMessages; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using Io.Cucumber.Messages.Types; +using System.Reflection; +using Reqnroll.Events; +using System.Collections.Concurrent; +using System.Text.Json; +using Reqnroll.Tracing; +using Reqnroll.BoDi; +using System; +using System.Threading.Tasks; +using System.IO; +using System.Linq; + + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin + { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private Task? fileWritingTask; + + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) + private object _lock = new(); + private readonly BlockingCollection postedMessages = new(); + private readonly ConcurrentDictionary fileStreams = new(); + private FileSinkConfiguration? configuration; + private string baseDirectory = ""; + private Lazy? traceListener; + private ITraceListener? trace => traceListener?.Value; + private IObjectContainer? objectContainer; + + public FileSinkPlugin() + { + traceListener = new Lazy(() => objectContainer!.Resolve()); + } + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + + runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); + + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + objectContainer = args.ObjectContainer; + testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); + testThreadExecutionEventPublisher.AddHandler(CloseFileSink); + }; + } + + private string ProcessConfiguration(FileSinkConfiguration configuration) + { + var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeDestination != null) + { + var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); + if (!Directory.Exists(basePath)) + { + lock (_lock) + { + if (!Directory.Exists(basePath)) + Directory.CreateDirectory(basePath); + } + } + trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); + return basePath; + } + else + { + var location = Assembly.GetExecutingAssembly().Location; + trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); + return location; + } + } + + private void CloseFileSink(TestRunFinishedEvent @event) + { + trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); + postedMessages.CompleteAdding(); + fileWritingTask?.Wait(); + fileWritingTask = null; + } + + private void LaunchFileSink(TestRunStartedEvent testRunStarted) + { + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + if (!configuration!.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + return; + } + + baseDirectory = ProcessConfiguration(configuration); + + fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + } + + public void Publish(ReqnrollCucumberMessage message) + { + var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; + trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + postedMessages.Add(message); + } + + private async Task ConsumeAndWriteToFiles() + { + foreach (var message in postedMessages.GetConsumingEnumerable()) + { + var featureName = message.CucumberMessageSource; + + if (message.Envelope != null) + { + var cm = Serialize(message.Envelope); + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + await Write(featureName, cm); + } + else + { + trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + CloseFeatureStream(featureName); + } + } + } + + + private string Serialize(Envelope message) + { + return NdjsonSerializer.Serialize(message); + } + private async Task Write(string featureName, string cucumberMessage) + { + try + { + if (!fileStreams.ContainsKey(featureName)) + { + lock (_lock) + { + if (!fileStreams.ContainsKey(featureName)) + { + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + } + } + } + trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + await fileStreams[featureName].WriteLineAsync(cucumberMessage); + } + catch (System.Exception ex) + { + trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); + } + } + + private void CloseFeatureStream(string featureName) + { + trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); + fileStreams[featureName].Close(); + fileStreams.TryRemove(featureName, out var _); + } + private bool disposedValue; + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CloseFileSink(new TestRunFinishedEvent()); + postedMessages.Dispose(); + foreach (var stream in fileStreams.Values) + { + stream.Close(); + stream.Dispose(); + }; + fileStreams.Clear(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 61d0c283b..93c80f693 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -17,6 +17,7 @@ using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; using Reqnroll.CucumberMessages; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; namespace Reqnroll.Infrastructure { @@ -104,6 +105,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); + container.RegisterTypeAs("FileSinkPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) From 88cd3eedcfb61ba5a8e85098fe022dcb9a9507aa Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:50:32 -0500 Subject: [PATCH 153/218] FileSink Configuration supports environment variable overrides for the storage location of output and a flag to enable or disable execution. --- .../CucumberMessages/FileSinkConfiguration.cs | 51 +++++++++++- Reqnroll/CucumberMessages/FileSinkPlugin.cs | 48 ++++------- .../unknown_parameter_type.feature | 2 +- .../unknown_parameter_type.feature.ndjson | 2 +- .../CucumberCompatibilityTestBase.cs | 8 +- .../CucumberCompatibilityTests.cs | 81 +++++++++---------- 6 files changed, 110 insertions(+), 82 deletions(-) diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs index 7a56f4233..65e5d61c2 100644 --- a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs +++ b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs @@ -1,21 +1,66 @@ -using System; +using Reqnroll.Time; +using Reqnroll.Tracing; +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - internal class FileSinkConfiguration + public class FileSinkConfiguration { + internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; + private object _lock = new(); + public bool FileSinkEnabled { get; set; } public List Destinations { get; set; } public FileSinkConfiguration() : this(true) { } public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) + public FileSinkConfiguration(bool fileSinkEnabled, List destinations) { FileSinkEnabled = fileSinkEnabled; Destinations = destinations; } + public string ConfiguredOutputDirectory(ITraceListener trace) + { + string outputDirectory; + string configuredOutputDirectory = string.Empty; + string defaultOutputDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string environmentVariableOutputDirectory = Environment.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var activeConfiguredDestination = Destinations.Where(d => d.Enabled).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); + } + + outputDirectory = defaultOutputDirectory; + if (!String.IsNullOrEmpty(configuredOutputDirectory)) + outputDirectory = configuredOutputDirectory; + if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) + outputDirectory = environmentVariableOutputDirectory; + + string logEntry; + if (outputDirectory == defaultOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; + else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; + else logEntry = $"FileSinkPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; + + trace?.WriteTestOutput(logEntry); + if (!Directory.Exists(outputDirectory)) + { + lock (_lock) + { + if (!Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + } + } + return outputDirectory; + } + } public class Destination diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs index 40baea678..75b9e016a 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -21,12 +21,13 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; + private object _lock = new(); //Thread safe collections to hold: // 1. Inbound Cucumber Messages - BlockingCollection // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - private object _lock = new(); private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private FileSinkConfiguration? configuration; @@ -53,33 +54,6 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; } - - private string ProcessConfiguration(FileSinkConfiguration configuration) - { - var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeDestination != null) - { - var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); - if (!Directory.Exists(basePath)) - { - lock (_lock) - { - if (!Directory.Exists(basePath)) - Directory.CreateDirectory(basePath); - } - } - trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); - return basePath; - } - else - { - var location = Assembly.GetExecutingAssembly().Location; - trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); - return location; - } - } - private void CloseFileSink(TestRunFinishedEvent @event) { trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); @@ -90,16 +64,24 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) + bool environmentEnabled = "true".Equals(Environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE), StringComparison.InvariantCultureIgnoreCase); + bool environmentLocationSpecified = !String.IsNullOrEmpty(FileSinkConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); + configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + } + else if (environmentEnabled && environmentLocationSpecified) + configuration = new FileSinkConfiguration(true); + else configuration = new FileSinkConfiguration(false); + if (!configuration.FileSinkEnabled) + { + trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } - baseDirectory = ProcessConfiguration(configuration); + baseDirectory = configuration.ConfiguredOutputDirectory(trace); + trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature index 4ce05c974..59e2d6201 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature @@ -1,4 +1,4 @@ -Feature: Parameter Types +Feature: Unknown Parameter Types Cucumber will generate an error message if a step definition registers an unknown parameter type, but the suite will run. diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson index cc9886734..ab4bb0bef 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson @@ -1,5 +1,5 @@ {"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} -{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"source":{"data":"Feature: Unknown Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"1","keyword":"Scenario","location":{"column":3,"line":5},"name":"undefined parameter type","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":6},"text":"CDG is closed because of a strike"}],"tags":[]}}],"description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"pickle":{"astNodeIds":["1"],"id":"3","language":"en","name":"undefined parameter type","steps":[{"astNodeIds":["0"],"id":"2","text":"CDG is closed because of a strike","type":"Context"}],"tags":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"undefinedParameterType":{"expression":"{airport} is closed because of a strike","name":"airport"}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 4f7fa2d1c..a9c7cac2c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -16,11 +16,15 @@ protected override void TestCleanup() // TEMPORARY: this is in place so that SystemTestBase.TestCleanup does not run (which deletes the generated code) } - protected void AddCucumberMessagePlugIn() + protected void EnableCucumberMessages() { - _projectsDriver.AddNuGetPackage("Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin", "2.1.1-local"); + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); } + protected void DisableCucumberMessages() + { + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "false"); + } protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) { var bindingCLassFileContent = _testFileManager.GetTestFileContent(fileName, prefix, assemblyToLoadFrom); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 7be0f6340..a0465bd33 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -11,17 +11,22 @@ using Reqnroll.TestProjectGenerator; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Reqnroll.TestProjectGenerator.Driver; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { + private void ResetCucumberMessages() + { + DisableCucumberMessages(); + } [TestMethod] public void NullTest() { + ResetCucumberMessages(); // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else - AddFeatureFile(""" Feature: Cucumber Messages Null Test Scenario: Eating Cukes @@ -37,7 +42,8 @@ When I eat 5 cukes [TestMethod] public void SmokeTest() { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -55,7 +61,8 @@ When I eat 5 cukes [TestMethod] public void SmokeOutlineTest() { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -78,10 +85,11 @@ public void SmokeOutlineTest() [TestMethod] public void SmokeOutlineTestAsMethods() { + ResetCucumberMessages(); var _configurationFileDriver = GetServiceSafe(); _configurationFileDriver.SetIsRowTestsAllowed(false); - AddCucumberMessagePlugIn(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -106,8 +114,9 @@ public void SmokeOutlineTestAsMethods() [TestMethod] public void CucumberMessagesInteropWithExternalData() { + ResetCucumberMessages(); // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages - AddCucumberMessagePlugIn(); + EnableCucumberMessages(); _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -218,27 +227,30 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) """); ExecuteTests(); - ShouldAllScenariosPass(); + ShouldAllScenariosPass(3); } [TestMethod] - [DataRow("attachments", "Attachments")] [DataRow("minimal", "minimal")] [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] [DataRow("examples-tables", "Examples Tables")] - [DataRow("hooks", "Hooks")] [DataRow("data-tables", "Data Tables")] + [DataRow("hooks", "Hooks")] [DataRow("parameter-types", "Parameter Types")] - [DataRow("skipped", "Skipping scenarios")] [DataRow("undefined", "Undefined steps")] - [DataRow("unknown-parameter-type", "Unknown Parameter Types")] [DataRow("stack-traces", "Stack traces")] [DataRow("rules", "Usage of a `Rule`")] + // These CCK scenario examples produce Cucumber Messages that are materially compliant with the CCK. + // The messages produced match the CCK expected messages, with exceptions for things + // that are not material to the CCK spec (such as IDs don't have to be generated in the same order, timestamps don't have to match, etc.) + // The rules for what must match and what is allowed to not match are built in to a series of custom FluentAssertion validation rules + // (located in the CucumberMessagesValidator class) public void CCKScenarios(string testName, string featureNameText) { - AddCucumberMessagePlugIn(); + ResetCucumberMessages(); + EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); @@ -255,7 +267,6 @@ public void CCKScenarios(string testName, string featureNameText) validator.ResultShouldPassAllComparisonTests(); validator.ResultShouldPassSanityChecks(); - // This is necessary b/c the System Test framework doesn't understand Rules and can't determine the number of expected tests ConfirmAllTestsRan(testName == "rules" ? 3 : null); } @@ -263,11 +274,23 @@ public void CCKScenarios(string testName, string featureNameText) [TestMethod] [DataRow("ambiguous", "ambiguous")] [DataRow("background", "background")] + // These tests are not (yet) within the CCK but are included here to round out the testing. The expected results were generated by the CucumberMessages plugin. + // Once the CCK includes these scenarios, the expected results should come from the CCK repo. public void NonCCKScenarios(string testName, string featureNameText) { CCKScenarios(testName, featureNameText); } + [TestMethod] + [DataRow("attachments", "Attachments")] + [DataRow("skipped", "Skipping scenarios")] + [DataRow("unknown-parameter-type", "Unknown Parameter Types")] + // These scenarios are from the CCK, but Reqnroll cannot provide a compliant implementation. This is usually the result of differences in behavior or support of Gherkin features. + // When these scenarios are run, expect them to fail. + public void NonCompliantCCKScenarios(string testName, string featureNameText) + { + CCKScenarios(testName, featureNameText); + } private void AddUtilClassWithFileSystemPath() { @@ -291,43 +314,17 @@ private IEnumerable GetExpectedResults(string testName, string feature private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var resultLocation = config!.Destinations.Where(d => d.Enabled).First().OutputDirectory; - var basePath = config!.Destinations.Where(d => d.Enabled).First().BasePath; + + var resultLocation = config!.ConfiguredOutputDirectory(null); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario - var actualJsonText = File.ReadAllLines(Path.Combine(basePath, resultLocation, $"{fileName}.ndjson")); + var actualJsonText = File.ReadAllLines(Path.Combine(resultLocation, $"{fileName}.ndjson")); foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } } - internal class FileSinkConfiguration - { - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } } \ No newline at end of file From 1b87adcc9c91ac38fa68c47d68b4005f0a8fa595 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:58:02 -0500 Subject: [PATCH 154/218] FileSink now waits to register as a Sink until it is confirmed that it is Enabled. --- Reqnroll/CucumberMessages/CucumberMessageBroker.cs | 2 +- Reqnroll/CucumberMessages/FileSinkPlugin.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs index 7b4804104..5d25eff32 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/CucumberMessageBroker.cs @@ -23,7 +23,7 @@ public class CucumberMessageBroker : ICucumberMessageBroker { private IObjectContainer _objectContainer; - public bool Enabled => _objectContainer.ResolveAll().ToList().Count > 0; + public bool Enabled => RegisteredSinks.Value.ToList().Count > 0; private Lazy> RegisteredSinks; diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileSinkPlugin.cs index 75b9e016a..d8da19f4f 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileSinkPlugin.cs @@ -34,22 +34,20 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin private string baseDirectory = ""; private Lazy? traceListener; private ITraceListener? trace => traceListener?.Value; - private IObjectContainer? objectContainer; + private IObjectContainer? testThreadObjectContainer; public FileSinkPlugin() { - traceListener = new Lazy(() => objectContainer!.Resolve()); + traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - objectContainer = args.ObjectContainer; + testThreadObjectContainer = args.ObjectContainer; testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); testThreadExecutionEventPublisher.AddHandler(CloseFileSink); }; @@ -83,6 +81,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + testThreadObjectContainer!.RegisterInstanceAs(this); } public void Publish(ReqnrollCucumberMessage message) From b608f8145891f9607a9478188c76bbd865c4878d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:06:07 -0500 Subject: [PATCH 155/218] Removed local nuget location from nuget.config --- Reqnroll/Reqnroll.csproj | 2 +- nuget.config | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 3071a72fc..ab13e29f0 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -16,7 +16,6 @@ - @@ -27,6 +26,7 @@ + diff --git a/nuget.config b/nuget.config index c8c559d96..6076bdca3 100644 --- a/nuget.config +++ b/nuget.config @@ -4,6 +4,5 @@ - \ No newline at end of file From 2246253e60facaac8cbe416dac3e6025a6aee630 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:03:09 -0500 Subject: [PATCH 156/218] Refactored configuration to use Profiles. Fixed regressions in RuntimeTests.AnalyticsProviderTests --- .../CucumberMessages/CucumberConfiguration.cs | 70 ++++++++++++++++ .../CucumberMessages-config-schema.json | 14 ++-- .../CucumberOutputConfiguration.cs | 40 ++++++++++ ...{FileSinkPlugin.cs => FileOutputPlugin.cs} | 49 +++++++----- .../CucumberMessages/FileSinkConfiguration.cs | 80 ------------------- .../EnvironmentInfoProvider.cs | 9 ++- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 1 + .../CucumberCompatibilityTests.cs | 11 ++- .../CucumberMessages.configuration.json | 4 +- .../Analytics/AnalyticsEventProviderTests.cs | 28 +++---- .../ErrorProviderTests.cs | 2 +- .../ReqnrollOutputHelperTests.cs | 4 + 13 files changed, 182 insertions(+), 132 deletions(-) create mode 100644 Reqnroll/CucumberMessages/CucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs rename Reqnroll/CucumberMessages/{FileSinkPlugin.cs => FileOutputPlugin.cs} (68%) delete mode 100644 Reqnroll/CucumberMessages/FileSinkConfiguration.cs diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs new file mode 100644 index 000000000..b7d36932e --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -0,0 +1,70 @@ +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Tracing; +using System; +using System.IO; +using System.Linq; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class CucumberConfiguration + { + internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; + internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + private ITraceListener _trace; + private IEnvironmentWrapper _environmentWrapper; + private object _lock = new(); + + private CucumberOutputConfiguration outputConfiguration; + public bool Enabled => outputConfiguration != null ? outputConfiguration.FileOutputEnabled : false; + + public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) + { + _trace = traceListener; + _environmentWrapper = environmentWrapper; + } + + public string ConfigureOutputDirectory(CucumberOutputConfiguration config) + { + outputConfiguration = config; + string outputDirectory; + string configuredOutputDirectory = string.Empty; + string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); + var outDirValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var profileValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); + string environmentVariableOutputDirectory = outDirValue is Success ? ((Success)outDirValue).Result : null; + string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + + var activeConfiguredDestination = config.Destinations.Where(d => d.ProfileName == profileName).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); + } + + outputDirectory = defaultOutputDirectory; + if (!String.IsNullOrEmpty(configuredOutputDirectory)) + outputDirectory = configuredOutputDirectory; + if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) + outputDirectory = environmentVariableOutputDirectory; + + string logEntry; + if (outputDirectory == defaultOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; + else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; + else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; + + _trace?.WriteTestOutput(logEntry); + if (!Directory.Exists(outputDirectory)) + { + lock (_lock) + { + if (!Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + } + } + return outputDirectory; + } + + } +} + diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json index f7fcd8f17..b6f6b7919 100644 --- a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json +++ b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "filSinkEnabled": { + "fileOutputEnabled": { "type": "boolean", "description": "Indicates whether the program is enabled or not" }, @@ -11,9 +11,9 @@ "items": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "description": "Indicates whether this destination setting is enabled" + "profileName": { + "type": "string", + "description": "Provides a description for this destination (ex: 'dev' or 'prod')" }, "basePath": { "type": "string", @@ -21,14 +21,14 @@ }, "outputDirectory": { "type": "string", - "description": "The directory path where output should go" + "description": "The subdirectory of the base path where output should go" } }, - "required": [ "enabled", "basePath", "outputDirectory" ] + "required": [ "profileName", "basePath", "outputDirectory" ] }, "minItems": 1, "description": "Array of destination settings" } }, - "required": [ "programEnabled", "destinations" ] + "required": [ "fileOutputEnabled", "destinations" ] } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs new file mode 100644 index 000000000..e6c9499e3 --- /dev/null +++ b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs @@ -0,0 +1,40 @@ +using Reqnroll.Time; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; + +namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +{ + public class CucumberOutputConfiguration + { + + public bool FileOutputEnabled { get; set; } + public List Destinations { get; set; } + + public CucumberOutputConfiguration() : this(true) { } + public CucumberOutputConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } + public CucumberOutputConfiguration(bool fileSinkEnabled, List destinations) + { + FileOutputEnabled = fileSinkEnabled; + Destinations = destinations; + } + + } + + public class Destination + { + public string ProfileName { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + + public Destination(string profileName, string basePath, string outputDirectory) + { + ProfileName = String.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; + BasePath = basePath; + OutputDirectory = outputDirectory; + } + } +} + diff --git a/Reqnroll/CucumberMessages/FileSinkPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs similarity index 68% rename from Reqnroll/CucumberMessages/FileSinkPlugin.cs rename to Reqnroll/CucumberMessages/FileOutputPlugin.cs index d8da19f4f..5a025dc8b 100644 --- a/Reqnroll/CucumberMessages/FileSinkPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -14,13 +14,15 @@ using System.Threading.Tasks; using System.IO; using System.Linq; +using Reqnroll.EnvironmentAccess; +using Reqnroll.CommonModels; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin + public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; private object _lock = new(); @@ -30,13 +32,12 @@ public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); - private FileSinkConfiguration? configuration; private string baseDirectory = ""; private Lazy? traceListener; private ITraceListener? trace => traceListener?.Value; private IObjectContainer? testThreadObjectContainer; - public FileSinkPlugin() + public FileOutputPlugin() { traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); } @@ -54,7 +55,7 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar } private void CloseFileSink(TestRunFinishedEvent @event) { - trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); + trace?.WriteTestOutput("FileOutputPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; @@ -62,24 +63,32 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - bool environmentEnabled = "true".Equals(Environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE), StringComparison.InvariantCultureIgnoreCase); - bool environmentLocationSpecified = !String.IsNullOrEmpty(FileSinkConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); + var environment = testThreadObjectContainer!.Resolve(); + var enabledResult = environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "false"; + bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + + bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; + + CucumberOutputConfiguration configurationDTO; if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; } else if (environmentEnabled && environmentLocationSpecified) - configuration = new FileSinkConfiguration(true); - else configuration = new FileSinkConfiguration(false); - if (!configuration.FileSinkEnabled) + configurationDTO = new CucumberOutputConfiguration(true); + else configurationDTO = new CucumberOutputConfiguration(false); + var configuration = new CucumberConfiguration(trace, environment); + + if (!configurationDTO.FileOutputEnabled) { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace?.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } - baseDirectory = configuration.ConfiguredOutputDirectory(trace); + baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); + trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); testThreadObjectContainer!.RegisterInstanceAs(this); } @@ -87,7 +96,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) public void Publish(ReqnrollCucumberMessage message) { var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -100,12 +109,12 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } @@ -130,18 +139,18 @@ private async Task Write(string featureName, string cucumberMessage) } } } - trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); await fileStreams[featureName].WriteLineAsync(cucumberMessage); } catch (System.Exception ex) { - trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Exception: {ex.Message}"); } } private void CloseFeatureStream(string featureName) { - trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); + trace?.WriteTestOutput($"FileOutputPlugin CloseFeatureStream. Closing: {featureName}."); fileStreams[featureName].Close(); fileStreams.TryRemove(featureName, out var _); } diff --git a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs b/Reqnroll/CucumberMessages/FileSinkConfiguration.cs deleted file mode 100644 index 65e5d61c2..000000000 --- a/Reqnroll/CucumberMessages/FileSinkConfiguration.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Reqnroll.Time; -using Reqnroll.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class FileSinkConfiguration - { - internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; - private object _lock = new(); - - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - public string ConfiguredOutputDirectory(ITraceListener trace) - { - string outputDirectory; - string configuredOutputDirectory = string.Empty; - string defaultOutputDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string environmentVariableOutputDirectory = Environment.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); - var activeConfiguredDestination = Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeConfiguredDestination != null) - { - configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); - } - - outputDirectory = defaultOutputDirectory; - if (!String.IsNullOrEmpty(configuredOutputDirectory)) - outputDirectory = configuredOutputDirectory; - if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) - outputDirectory = environmentVariableOutputDirectory; - - string logEntry; - if (outputDirectory == defaultOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; - else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileSinkPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; - else logEntry = $"FileSinkPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - - trace?.WriteTestOutput(logEntry); - if (!Directory.Exists(outputDirectory)) - { - lock (_lock) - { - if (!Directory.Exists(outputDirectory)) - Directory.CreateDirectory(outputDirectory); - } - } - return outputDirectory; - } - - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs index 6a9a1cc50..417f7eb27 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -9,11 +9,11 @@ namespace Reqnroll.EnvironmentAccess { public class EnvironmentInfoProvider : IEnvironmentInfoProvider { - private readonly IEnvironmentWrapper environmentWrapper; + private IEnvironmentWrapper EnvironmentWrapper { get; set; } public EnvironmentInfoProvider(IEnvironmentWrapper environmentWrapper) { - this.environmentWrapper = environmentWrapper; + EnvironmentWrapper = environmentWrapper; } public string GetOSPlatform() @@ -60,11 +60,12 @@ private readonly Dictionary buildServerTypes { "CI_NAME", "CodeShip" } }; + public string GetBuildServerName() { foreach (var buildServerType in buildServerTypes) { - var envVariable = environmentWrapper.GetEnvironmentVariable(buildServerType.Key); + var envVariable = EnvironmentWrapper.GetEnvironmentVariable(buildServerType.Key); if (envVariable is ISuccess) return buildServerType.Value; } @@ -73,7 +74,7 @@ public string GetBuildServerName() public bool IsRunningInDockerContainer() { - return environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + return EnvironmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; } public string GetReqnrollVersion() diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 93c80f693..0ef2feab0 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -105,7 +105,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) //Support for publishing Cucumber Messages container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); - container.RegisterTypeAs("FileSinkPlugin"); + container.RegisterTypeAs("FileOutputPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index a9c7cac2c..956126f96 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -19,6 +19,7 @@ protected override void TestCleanup() protected void EnableCucumberMessages() { Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); + Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE", "LOCAL"); } protected void DisableCucumberMessages() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index a0465bd33..91e055d1d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -12,6 +12,10 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Reqnroll.TestProjectGenerator.Driver; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Moq; +using Reqnroll.Tracing; +using Reqnroll.EnvironmentAccess; +using SpecFlow.Internal.Json; namespace CucumberMessages.CompatibilityTests { @@ -315,9 +319,12 @@ private IEnumerable GetActualResults(string testName, string fileName) { var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var resultLocation = config!.ConfiguredOutputDirectory(null); + var tracerMock = new Mock(); + var env = new EnvironmentWrapper(); + CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); + var resultLocation = configuration.ConfigureOutputDirectory(config); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index cb6aa6ecb..7b600acc6 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -1,8 +1,8 @@ { - "fileSinkEnabled": true, + "fileOutputEnabled": true, "destinations": [ { - "enabled": true, + "profileName": "DEV", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", "outputDirectory": "CucumberMessages" } diff --git a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs index 33e26f385..a16aa82dc 100644 --- a/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Analytics/AnalyticsEventProviderTests.cs @@ -19,15 +19,14 @@ public class AnalyticsEventProviderTests public void Should_return_the_build_server_name_in_Compiling_Event() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); - - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); - - environmentMock + var environmentWrapperMock = new Mock(); + environmentWrapperMock .Setup(m => m.GetEnvironmentVariable("TF_BUILD")) .Returns(new Success("true")); + var environmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoProvider); + var compilingEvent = sut.CreateProjectCompilingEvent(null, null, null, null, null); compilingEvent.BuildServerName.Should().Be("Azure Pipelines"); @@ -37,14 +36,13 @@ public void Should_return_the_build_server_name_in_Compiling_Event() public void Should_return_the_build_server_name_in_Running_Event() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); - - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); - - environmentMock + var environmentWrapperMock = new Mock(); + environmentWrapperMock .Setup(m => m.GetEnvironmentVariable("TEAMCITY_VERSION")) .Returns(new Success("true")); + var environmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); + + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoProvider); var compilingEvent = sut.CreateProjectRunningEvent(null); @@ -55,10 +53,10 @@ public void Should_return_the_build_server_name_in_Running_Event() public void Should_return_null_for_the_build_server_name_when_not_detected() { var userUniqueIdStoreMock = new Mock(); - var environmentMock = new Mock(); - var environmentInfoMock = new Mock(environmentMock.Object); + var environmentWrapperMock = new Mock(); + var evironmentInfoProvider = new EnvironmentInfoProvider(environmentWrapperMock.Object); - var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), environmentInfoMock.Object); + var sut = new AnalyticsEventProvider(userUniqueIdStoreMock.Object, new UnitTestProvider.UnitTestProviderConfiguration(), evironmentInfoProvider); var compilingEvent = sut.CreateProjectRunningEvent(null); diff --git a/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs b/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs index 85cd63537..616ddb037 100644 --- a/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs +++ b/Tests/Reqnroll.RuntimeTests/ErrorProviderTests.cs @@ -131,7 +131,7 @@ private void GetMatchErrorMethod_should_return_BindingException_containing_full_ var result = GetMatchErrorFunc(errorProvider, bindingMatch, null); result.Should().NotBeNull(); - result.Should().BeOfType(); + result.Should().BeAssignableTo(); result.Message.Should().Be($"{expectedPrefixMessage} '{stepInstanceDescription}': {methodBindingAssemblyName}:{method1BindingTypeFullName}.{methodName}({parameter1Type}), {methodBindingAssemblyName}:{method2BindingTypeFullName}.{methodName}({parameter1Type})"); } diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 8bbc7d653..80993d9fd 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -38,6 +38,10 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); var contextManager = new Mock(); + var featureContext = new Mock(); + var featureInfo = new FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "test feature", null); + featureContext.SetupGet(fc => fc.FeatureInfo).Returns(featureInfo); + contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext.Object as FeatureContext); return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } From e85996cf2f54eef026a5397693328b5d33828613 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:01:02 -0500 Subject: [PATCH 157/218] Fixed ReqnrollOutputHelperTest --- Reqnroll/CucumberMessages/CucumberConfiguration.cs | 2 ++ Reqnroll/CucumberMessages/FileOutputPlugin.cs | 5 ++--- .../Infrastructure/ReqnrollOutputHelperTests.cs | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs index b7d36932e..9af3bd1e6 100644 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -11,6 +11,8 @@ public class CucumberConfiguration { internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; + private ITraceListener _trace; private IEnvironmentWrapper _environmentWrapper; private object _lock = new(); diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 5a025dc8b..047e82fbf 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -23,7 +23,6 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; - private const string CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; private Task? fileWritingTask; private object _lock = new(); @@ -64,8 +63,8 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { var environment = testThreadObjectContainer!.Resolve(); - var enabledResult = environment.GetEnvironmentVariable(CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "false"; + var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs index 80993d9fd..6dbd00b1d 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/ReqnrollOutputHelperTests.cs @@ -1,7 +1,9 @@ using Moq; +using Reqnroll.Configuration; using Reqnroll.Events; using Reqnroll.Infrastructure; using Reqnroll.Tracing; +using System; using Xunit; namespace Reqnroll.RuntimeTests.Infrastructure @@ -38,10 +40,10 @@ private ReqnrollOutputHelper CreateReqnrollOutputHelper() var traceListenerMock = new Mock(); var attachmentHandlerMock = new Mock(); var contextManager = new Mock(); - var featureContext = new Mock(); var featureInfo = new FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "test feature", null); - featureContext.SetupGet(fc => fc.FeatureInfo).Returns(featureInfo); - contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext.Object as FeatureContext); + var config = new ReqnrollConfiguration(ConfigSource.Json, null, null, null, null, false, MissingOrPendingStepsOutcome.Error, false, false, TimeSpan.FromSeconds(10), Reqnroll.BindingSkeletons.StepDefinitionSkeletonStyle.CucumberExpressionAttribute, null, false, false, new string[] { }, ObsoleteBehavior.Error, false); + var featureContext = new FeatureContext(null, featureInfo, config); + contextManager.SetupGet(c => c.FeatureContext).Returns(featureContext); return new ReqnrollOutputHelper(_testThreadExecutionEventPublisher.Object, traceListenerMock.Object, attachmentHandlerMock.Object, contextManager.Object); } From c0400e8fd9dc7556696144c1009e9088f4e02da6 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:16:30 -0500 Subject: [PATCH 158/218] Simplified File Plug In handling of configuration. Fixed a defect that threw an exception when Source document is not available; not a critical error, so changed it to allow it to proceed with Pickle creation. Revised tests to delete previous run .ndjson files. --- Reqnroll.Parser/CucumberMessagesConverter.cs | 4 +- .../CucumberMessagePublisher.cs | 21 +++++----- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 28 ++++++++----- .../DefaultDependencyProvider.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 19 +++++++++ .../CucumberCompatibilityTests.cs | 40 ++++++++++++------- .../CucumberMessages.configuration.json | 2 +- 7 files changed, 78 insertions(+), 38 deletions(-) diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 9369631cd..808c34bf4 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -24,7 +24,9 @@ public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) { - var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + string sourceText = $"Source Document: {gherkinDocument.SourceFilePath} not found."; + if (File.Exists(gherkinDocument.SourceFilePath)) + sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); return new Source { Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 4ea98657f..9a3baabc4 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -13,16 +13,16 @@ namespace Reqnroll.CucumberMessages { public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { - private ICucumberMessageBroker broker; + private Lazy _brokerFactory; + private ICucumberMessageBroker _broker; private IObjectContainer objectContainer; private ConcurrentDictionary StartedFeatures = new(); private ConcurrentDictionary testCaseTrackersById = new(); bool Enabled = false; - public CucumberMessagePublisher(ICucumberMessageBroker CucumberMessageBroker, IObjectContainer objectContainer) + public CucumberMessagePublisher() { - //Debugger.Launch(); - broker = CucumberMessageBroker; + _brokerFactory = new Lazy(() => objectContainer.Resolve()); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { @@ -50,13 +50,14 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { + _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; // if not, then this is disabled for this feature - // if true, then it checks with the broker to confirm that a listener/sink has been registered - Enabled = broker.Enabled; + // if true, then it checks with the _broker to confirm that a listener/sink has been registered + Enabled = _broker.Enabled; if (!Enabled) { traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: Broker is disabled for {featureName}."); @@ -75,7 +76,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { foreach (var msg in ft.StaticMessages) { - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } @@ -96,8 +97,8 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve { var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - broker.Complete(featureName); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + _broker.Complete(featureName); } else { @@ -139,7 +140,7 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished foreach (var msg in tccmt.TestCaseCucumberMessages()) { - broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); } } diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 047e82fbf..a8b6c7a2b 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -16,13 +16,14 @@ using System.Linq; using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; +using System.Diagnostics; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configurationDTO.json"; + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; private object _lock = new(); @@ -32,17 +33,24 @@ public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugi private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private string baseDirectory = ""; - private Lazy? traceListener; - private ITraceListener? trace => traceListener?.Value; + private Lazy traceListener; + private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; + private IObjectContainer? globalObjectContainer; + public FileOutputPlugin() { traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { + runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => + { + globalObjectContainer = args.ObjectContainer; + }; runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { @@ -65,19 +73,15 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) var environment = testThreadObjectContainer!.Resolve(); var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; - bool environmentEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + bool isMessagesEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); - bool environmentLocationSpecified = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE) is Success; - CucumberOutputConfiguration configurationDTO; if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; } - else if (environmentEnabled && environmentLocationSpecified) - configurationDTO = new CucumberOutputConfiguration(true); - else configurationDTO = new CucumberOutputConfiguration(false); - var configuration = new CucumberConfiguration(trace, environment); + else + configurationDTO = new CucumberOutputConfiguration(isMessagesEnabled); if (!configurationDTO.FileOutputEnabled) { @@ -85,11 +89,12 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) return; } + var configuration = new CucumberConfiguration(trace, environment); baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); - testThreadObjectContainer!.RegisterInstanceAs(this); + globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } public void Publish(ReqnrollCucumberMessage message) @@ -154,6 +159,7 @@ private void CloseFeatureStream(string featureName) fileStreams.TryRemove(featureName, out var _); } private bool disposedValue; + protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 0ef2feab0..21f481c22 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -103,9 +103,9 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); //Support for publishing Cucumber Messages + container.RegisterTypeAs("FileOutputPlugin"); container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); - container.RegisterTypeAs("FileOutputPlugin"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 956126f96..f97f78f78 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,10 +1,15 @@ using FluentAssertions; +using Moq; +using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.EnvironmentAccess; using Reqnroll.SystemTests; +using Reqnroll.Tracing; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace CucumberMessages.CompatibilityTests @@ -61,5 +66,19 @@ protected void CucumberMessagesAddConfigurationFile(string configFileName) var configFileContent = File.ReadAllText(configFileName); _projectsDriver.AddFile(configFileName, configFileContent); } + + protected static string ActualsResultLocationDirectory() + { + var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + + var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + var tracerMock = new Mock(); + var env = new EnvironmentWrapper(); + CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); + var resultLocation = configuration.ConfigureOutputDirectory(config); + return resultLocation; + } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 91e055d1d..6ef87429d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -16,16 +16,34 @@ using Reqnroll.Tracing; using Reqnroll.EnvironmentAccess; using SpecFlow.Internal.Json; +using Microsoft.VisualBasic.FileIO; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { - private void ResetCucumberMessages() + private void ResetCucumberMessages(string? fileToDelete = null) { DisableCucumberMessages(); + DeletePreviousMessagesOutput(fileToDelete); } + + private void DeletePreviousMessagesOutput(string? fileToDelete = null) + { + var directory = ActualsResultLocationDirectory(); + + if (fileToDelete != null) + { + var fileToDeletePath = Path.Combine(directory, fileToDelete); + + if (File.Exists(fileToDeletePath)) + { + File.Delete(fileToDeletePath); + } + } + } + [TestMethod] public void NullTest() { @@ -46,7 +64,7 @@ When I eat 5 cukes [TestMethod] public void SmokeTest() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -65,7 +83,7 @@ When I eat 5 cukes [TestMethod] public void SmokeOutlineTest() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Outline Test.ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -89,7 +107,7 @@ public void SmokeOutlineTest() [TestMethod] public void SmokeOutlineTestAsMethods() { - ResetCucumberMessages(); + ResetCucumberMessages("Cucumber Messages Smoke Outline Test As Methods.ndjson"); var _configurationFileDriver = GetServiceSafe(); _configurationFileDriver.SetIsRowTestsAllowed(false); @@ -118,7 +136,7 @@ public void SmokeOutlineTestAsMethods() [TestMethod] public void CucumberMessagesInteropWithExternalData() { - ResetCucumberMessages(); + ResetCucumberMessages("External Data from CSV file.ndjson"); // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages EnableCucumberMessages(); _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); @@ -253,7 +271,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) // (located in the CucumberMessagesValidator class) public void CCKScenarios(string testName, string featureNameText) { - ResetCucumberMessages(); + ResetCucumberMessages(featureNameText+".ndjson"); EnableCucumberMessages(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); @@ -317,14 +335,7 @@ private IEnumerable GetExpectedResults(string testName, string feature private IEnumerable GetActualResults(string testName, string fileName) { - var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - var tracerMock = new Mock(); - var env = new EnvironmentWrapper(); - CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); - var resultLocation = configuration.ConfigureOutputDirectory(config); + string resultLocation = ActualsResultLocationDirectory(); // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario @@ -332,6 +343,7 @@ private IEnumerable GetActualResults(string testName, string fileName) foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); } + } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 7b600acc6..22b0b03c8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -2,7 +2,7 @@ "fileOutputEnabled": true, "destinations": [ { - "profileName": "DEV", + "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", "outputDirectory": "CucumberMessages" } From 35181e4aaf760ced7c49aef6f74af2323e634008 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:45:15 -0500 Subject: [PATCH 159/218] Modified Generator to globalize references to Reqnroll types related to Messages --- Reqnroll.Generator/Generation/ScenarioPartHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 4c852e9c0..959d3f4f6 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -35,7 +35,7 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) backgroundMethod.Attributes = MemberAttributes.Public; backgroundMethod.Name = GeneratorConstants.BACKGROUND_NAME; - backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); + backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); _codeDomHelper.MarkCodeMemberMethodAsAsync(backgroundMethod); @@ -247,7 +247,7 @@ public void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdInc public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod testMethod) { - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); testMethod.Statements.Add(pickleStepSequence); @@ -256,7 +256,7 @@ public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMethod) { // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); - var pickleStepSequence = new CodeVariableDeclarationStatement(typeof(PickleStepSequence), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, + var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, // Right side of the assignment (property access chain) new CodeMethodInvokeExpression( From 75e8a8a9bf68009db08ba78778cf8d67ac9c5054 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 09:24:43 -0500 Subject: [PATCH 160/218] Updated GeneratorTests to include now required RnRDocumentLocation elements when testing generation. Fixed MissingLocation patcher to include Feature and Scenario locations. Enhanced Publisher to be more careful of handling of Feature-level enablement. --- .../Generation/UnitTestFeatureGenerator.cs | 3 + Reqnroll.Parser/CucumberMessagesConverter.cs | 25 +++++-- Reqnroll.Parser/GherkinDocumentVisitor.cs | 3 +- ...chMissingLocationElementsTransformation.cs | 25 ++++++- ...on.cs => ScenarioTransformationVisitor.cs} | 2 +- .../CucumberMessagePublisher.cs | 73 ++++++++++++------- Reqnroll/CucumberMessages/FeatureTracker.cs | 4 +- .../TestCaseCucumberMessageTracker.cs | 1 + .../CCK/background/background.feature | 8 ++ .../CustomTestGeneratorProviderTest.cs | 2 +- Tests/Reqnroll.GeneratorTests/ParserHelper.cs | 22 +++--- .../TestGeneratorTest.cs | 2 +- .../MsTestGeneratorProviderTests.cs | 2 +- .../MsTestV2GeneratorProviderTests.cs | 2 +- .../NUnit3GeneratorProviderTests.cs | 2 +- .../XUnit2TestGeneratorProviderTests.cs | 2 +- 16 files changed, 125 insertions(+), 53 deletions(-) rename Reqnroll.Parser/{ScenarioTransformation.cs => ScenarioTransformationVisitor.cs} (98%) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 1142bba12..08316e57a 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -1,6 +1,7 @@ using System; using System.CodeDom; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; @@ -47,6 +48,7 @@ public UnitTestFeatureGenerator( public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace) { + Debugger.Launch(); var codeNamespace = CreateNamespace(targetNamespace); var feature = document.ReqnrollFeature; @@ -227,6 +229,7 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte try { //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + //TODO: make the type of IDGenerator configurable var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessagesConverter.cs index 808c34bf4..38be9c877 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessagesConverter.cs @@ -24,15 +24,26 @@ public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) { - string sourceText = $"Source Document: {gherkinDocument.SourceFilePath} not found."; if (File.Exists(gherkinDocument.SourceFilePath)) - sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); - return new Source { - Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), - Data = sourceText, - MediaType = "text/x.cucumber.gherkin+plain" - }; + var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + return new Source + { + Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), + Data = sourceText, + MediaType = "text/x.cucumber.gherkin+plain" + }; + } + else + { + return new Source + { + Uri = "Unknown", + Data = $"Source Document: {gherkinDocument.SourceFilePath} not found.", + MediaType = "text/x.cucumber.gherkin+plain" + }; + + } } public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument) diff --git a/Reqnroll.Parser/GherkinDocumentVisitor.cs b/Reqnroll.Parser/GherkinDocumentVisitor.cs index 4be6eccaa..ed9e6eba1 100644 --- a/Reqnroll.Parser/GherkinDocumentVisitor.cs +++ b/Reqnroll.Parser/GherkinDocumentVisitor.cs @@ -68,7 +68,8 @@ protected virtual void AcceptRule(Rule rule) OnRuleVisiting(rule); foreach (var ruleChild in rule.Children) { - if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + if (ruleChild is Background background) AcceptBackground(background); + else if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); else if (ruleChild is Scenario scenario) AcceptScenario(scenario); } OnRuleVisited(rule); diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 87daa06ab..462cf37f4 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -6,11 +6,32 @@ namespace Reqnroll.Parser { - internal class PatchMissingLocationElementsTransformation : ScenarioTransformation + internal class PatchMissingLocationElementsTransformation : ScenarioTransformationVisitor { + protected override void OnFeatureVisited(Feature feature) + { + var patchedFeatureLocation = PatchLocation(feature.Location); + var patchedFeature = new Feature( + feature.Tags.ToArray(), + patchedFeatureLocation, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + feature.Children.ToArray()); + base.OnFeatureVisited(patchedFeature); + + } protected override Scenario GetTransformedScenario(Scenario scenario) { - return null; + return new Scenario( + scenario.Tags.ToArray(), + PatchLocation(scenario.Location), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.ToArray(), + scenario.Examples.ToArray()); } protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline) diff --git a/Reqnroll.Parser/ScenarioTransformation.cs b/Reqnroll.Parser/ScenarioTransformationVisitor.cs similarity index 98% rename from Reqnroll.Parser/ScenarioTransformation.cs rename to Reqnroll.Parser/ScenarioTransformationVisitor.cs index 627577d78..2d0d76fb8 100644 --- a/Reqnroll.Parser/ScenarioTransformation.cs +++ b/Reqnroll.Parser/ScenarioTransformationVisitor.cs @@ -5,7 +5,7 @@ namespace Reqnroll.Parser { - abstract class ScenarioTransformation : GherkinDocumentVisitor + abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor { protected ReqnrollDocument _sourceDocument; private ReqnrollDocument _transformedDocument; diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 9a3baabc4..03bcb55da 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -22,13 +22,14 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher() { - _brokerFactory = new Lazy(() => objectContainer.Resolve()); + Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => { objectContainer = args.ObjectContainer; + _brokerFactory = new Lazy(() => objectContainer.Resolve()); var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); }; @@ -72,7 +73,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); var ft = new FeatureTracker(featureStartedEvent); - if (StartedFeatures.TryAdd(featureName, ft)) + if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) { foreach (var msg in ft.StaticMessages) { @@ -90,6 +91,11 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve return; } var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; + if (!StartedFeatures.ContainsKey(featureName) || !StartedFeatures[featureName].Enabled) + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: {featureName} was not started or is Disabled."); + return; + } var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); // IF all TestCaseCucumberMessageTrackers are done, then send the messages to the CucumberMessageBroker @@ -115,13 +121,21 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve return; var traceListener = objectContainer.Resolve(); var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; - var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - var tccmt = new TestCaseCucumberMessageTracker(featureTracker); - traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); - testCaseTrackersById.TryAdd(id, tccmt); - tccmt.ProcessEvent(scenarioStartedEvent); + if (featureTracker.Enabled) + { + var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); + testCaseTrackersById.TryAdd(id, tccmt); + tccmt.ProcessEvent(scenarioStartedEvent); + } + else + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker is disabled"); + return; + } } else { @@ -134,13 +148,15 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished { if (!Enabled) return; - - var tccmt = testCaseTrackersById[scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(scenarioFinishedEvent); - - foreach (var msg in tccmt.TestCaseCucumberMessages()) + var testCaseTrackerId = scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) { - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + tccmt.ProcessEvent(scenarioFinishedEvent); + + foreach (var msg in tccmt.TestCaseCucumberMessages()) + { + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + } } } @@ -148,9 +164,9 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { if (!Enabled) return; - - var tccmt = testCaseTrackersById[stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(stepStartedEvent); + var testCaseTrackerId = stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(stepStartedEvent); } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) @@ -158,8 +174,9 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (!Enabled) return; - var tccmt = testCaseTrackersById[stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(stepFinishedEvent); + var testCaseTrackerId = stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(stepFinishedEvent); } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) @@ -167,8 +184,9 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - var tccmt = testCaseTrackersById[hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(hookBindingStartedEvent); + var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(hookBindingStartedEvent); } private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) @@ -176,8 +194,9 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - var tccmt = testCaseTrackersById[hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(hookBindingFinishedEvent); + var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(hookBindingFinishedEvent); } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) @@ -185,8 +204,9 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (!Enabled) return; - var tccmt = testCaseTrackersById[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(attachmentAddedEvent); + var testCaseTrackerId = attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(attachmentAddedEvent); } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) @@ -194,8 +214,9 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (!Enabled) return; - var tccmt = testCaseTrackersById[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; - tccmt.ProcessEvent(outputAddedEvent); + var testCaseTrackerId = outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; + if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + tccmt.ProcessEvent(outputAddedEvent); } } } diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/FeatureTracker.cs index 18517816d..689e5715f 100644 --- a/Reqnroll/CucumberMessages/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/FeatureTracker.cs @@ -24,11 +24,13 @@ public class FeatureTracker public FeatureTracker(FeatureStartedEvent featureStartedEvent) { FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - Enabled = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles == null ? false : true; + var featureHasCucumberMessages = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages != null; + Enabled = (featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null) ? true : false; PreProcessEvent(featureStartedEvent); } internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) { + if (!Enabled) return; // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow StaticMessages = GenerateStaticMessages(featureStartedEvent).ToList(); } diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs index faeb018d5..cb4ae4ae9 100644 --- a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -120,6 +120,7 @@ private void InvokePreProcessEvent(ExecutionEvent anEvent) public IEnumerable TestCaseCucumberMessages() { + if (!Enabled) return Enumerable.Empty(); // Stage 2 return _events.Select(e => InvokePostProcessEvent(e)).SelectMany(x => x); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature index 41424d2e9..3c9ca9e94 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature @@ -15,3 +15,11 @@ Scenario: Transfer Money When I transfer $150 from savings to checking Then My checking account has a balance of $650 And My savings account has a balance of $50 + +Rule: A rule with a background + Background: First Transfer Money + When I transfer $50 from savings to checking + Then My savings account has a balance of $150 + Scenario: total balance unchanged + When the accounts are combined + Then I have $700 diff --git a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs index 07c2fb8fb..dc61a6544 100644 --- a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs +++ b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs @@ -66,7 +66,7 @@ public void GenerateScenarioExampleTests() var parser = new ReqnrollGherkinParser(new CultureInfo("en-US")); using (var reader = new StringReader(SampleFeatureFile)) { - var feature = parser.Parse(reader, null); + var feature = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_location_for_{nameof(GenerateScenarioExampleTests)}")); feature.Should().NotBeNull(); var sampleTestGeneratorProvider = new SimpleTestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp)); diff --git a/Tests/Reqnroll.GeneratorTests/ParserHelper.cs b/Tests/Reqnroll.GeneratorTests/ParserHelper.cs index f3b6a5594..5ead50214 100644 --- a/Tests/Reqnroll.GeneratorTests/ParserHelper.cs +++ b/Tests/Reqnroll.GeneratorTests/ParserHelper.cs @@ -12,35 +12,39 @@ class ParserHelper { public static ReqnrollDocument CreateAnyDocument(string[] tags = null) { - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, null, null, null, null, null); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), null, null, null, null, null); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); } public static Tag[] GetTags(params string[] tags) { - return tags == null ? new Tag[0] : tags.Select(t => new Tag(null, t)).ToArray(); + return tags == null ? new Tag[0] : tags.Select(t => new Tag(new Location(0), t)).ToArray(); } public static ReqnrollDocument CreateDocument(string[] tags = null, string[] scenarioTags = null) { tags = tags ?? new string[0]; - var scenario1 = new Scenario(GetTags(scenarioTags), null, "Scenario", "scenario1 title", "", new Step[0], new Examples[0]); + var scenario1 = new Scenario(GetTags(scenarioTags), new Location(0), "Scenario", "scenario1 title", "", new Step[0], new Examples[0]); - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); } public static ReqnrollDocument CreateDocumentWithScenarioOutline(string[] tags = null, string[] scenarioOutlineTags = null, string[] examplesTags = null) { tags = tags ?? new string[0]; - var scenario1 = new ScenarioOutline(GetTags(scenarioOutlineTags), null, "Scenario Outline", "scenario outline1 title", "", new Step[0], new [] + var scenario1 = new ScenarioOutline(GetTags(scenarioOutlineTags), new Location(0), "Scenario Outline", "scenario outline1 title", "", new Step[0], new [] { new Examples(GetTags(examplesTags), null, "Examples", "examples name", "", new Gherkin.Ast.TableRow(null, new []{ new TableCell(null, "col1"), }), new Gherkin.Ast.TableRow[0]) }); - var reqnrollFeature = new ReqnrollFeature(GetTags(tags), null, "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); - return new ReqnrollDocument(reqnrollFeature, new Comment[0], null); + var reqnrollFeature = new ReqnrollFeature(GetTags(tags), new Location(0), "en", "feature", "title", "desc", new StepsContainer[] {scenario1}); + return new ReqnrollDocument(reqnrollFeature, new Comment[0], CreateDummyReqnrollLocation()); + } + private static ReqnrollDocumentLocation CreateDummyReqnrollLocation() + { + return new ReqnrollDocumentLocation("dummy_location"); } } } diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs index f95c3183f..5b87d2327 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs @@ -63,7 +63,7 @@ public void GenerateScenarioExampleTests() var parser = new ReqnrollGherkinParser(new CultureInfo("en-US")); using (var reader = new StringReader(SampleFeatureFile)) { - var feature = parser.Parse(reader, null); + var feature = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_location_for_{nameof(GenerateScenarioExampleTests)}")); feature.Should().NotBeNull(); var sampleTestGeneratorProvider = new SimpleTestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp)); diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs index 55ee1b379..83acb6449 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestGeneratorProviderTests.cs @@ -276,7 +276,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_Reqnroll_Location_for{nameof(MsTestGeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs index 192190bdf..9446ef88c 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/MsTestV2GeneratorProviderTests.cs @@ -238,7 +238,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_Location_for_{nameof(MsTestV2GeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs index dce0efc83..f44957d38 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/NUnit3GeneratorProviderTests.cs @@ -349,7 +349,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_ReqnrollLocation_for_{nameof(NUnit3GeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } diff --git a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs index d716902bc..9b707ad97 100644 --- a/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/UnitTestProvider/XUnit2TestGeneratorProviderTests.cs @@ -453,7 +453,7 @@ public ReqnrollDocument ParseDocumentFromString(string documentSource, CultureIn var parser = new ReqnrollGherkinParser(parserCultureInfo ?? new CultureInfo("en-US")); using (var reader = new StringReader(documentSource)) { - var document = parser.Parse(reader, null); + var document = parser.Parse(reader, new ReqnrollDocumentLocation($"dummy_ReqnrollLocation_for_{nameof(XUnit2TestGeneratorProviderTests)}")); document.Should().NotBeNull(); return document; } From 93fd11137843e1579bb6fa90b24ef7df4a635e65 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:58:12 -0500 Subject: [PATCH 161/218] Fixed bug in LocationPatcher that was obliterating Rule Backgrounds. Added Location fixup to Tags, Features and Scenario elements to better support testability when tests set them as null as a convenience when setting up test data. Added smoke test to include more than one feature per test. --- .../Generation/UnitTestFeatureGenerator.cs | 1 - ...chMissingLocationElementsTransformation.cs | 16 ++-- .../ScenarioTransformationVisitor.cs | 2 +- .../CucumberMessages/CucumberConfiguration.cs | 2 +- .../CucumberMessagePublisher.cs | 4 +- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 40 +++++++-- .../TestCaseCucumberMessageTracker.cs | 4 +- .../CCK/background/background.feature.ndjson | 88 +++++++++++-------- .../CucumberCompatibilityTests.cs | 25 ++++++ ...CucumberMessages.CompatibilityTests.csproj | 4 +- 10 files changed, 127 insertions(+), 59 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 08316e57a..4b13a7f2c 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -48,7 +48,6 @@ public UnitTestFeatureGenerator( public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace) { - Debugger.Launch(); var codeNamespace = CreateNamespace(targetNamespace); var feature = document.ReqnrollFeature; diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs index 462cf37f4..2569928a0 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs @@ -12,7 +12,7 @@ protected override void OnFeatureVisited(Feature feature) { var patchedFeatureLocation = PatchLocation(feature.Location); var patchedFeature = new Feature( - feature.Tags.ToArray(), + feature.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), patchedFeatureLocation, feature.Language, feature.Keyword, @@ -25,12 +25,12 @@ protected override void OnFeatureVisited(Feature feature) protected override Scenario GetTransformedScenario(Scenario scenario) { return new Scenario( - scenario.Tags.ToArray(), + scenario.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(scenario.Location), scenario.Keyword, scenario.Name, scenario.Description, - scenario.Steps.ToArray(), + scenario.Steps.Select( s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), scenario.Examples.ToArray()); } @@ -42,23 +42,23 @@ protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenar var exampleTables = scenarioOutline.Examples; List transformedExamples = new List(); - transformedExamples.AddRange(exampleTables.Select(e => PatchLocations(e))); + transformedExamples.AddRange(exampleTables.Select(e => PatchExamplesLocations(e))); return new ScenarioOutline( - scenarioOutline.Tags.ToArray(), + scenarioOutline.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(scenarioOutline.Location), scenarioOutline.Keyword, scenarioOutline.Name, scenarioOutline.Description, - scenarioOutline.Steps.ToArray(), + scenarioOutline.Steps.Select(s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), transformedExamples.ToArray()); } - private Examples PatchLocations(Examples e) + private Examples PatchExamplesLocations(Examples e) { var headerCells = e.TableHeader.Cells; var tableHeader = new Gherkin.Ast.TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new Gherkin.Ast.TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); var rows = e.TableBody.Select(r => new Gherkin.Ast.TableRow(PatchLocation(r.Location), r.Cells.Select(c => new Gherkin.Ast.TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); - return new Examples(e.Tags.ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); + return new Examples(e.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); } private static Location PatchLocation(Location l) diff --git a/Reqnroll.Parser/ScenarioTransformationVisitor.cs b/Reqnroll.Parser/ScenarioTransformationVisitor.cs index 2d0d76fb8..621732af3 100644 --- a/Reqnroll.Parser/ScenarioTransformationVisitor.cs +++ b/Reqnroll.Parser/ScenarioTransformationVisitor.cs @@ -65,7 +65,7 @@ private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedSc protected override void OnBackgroundVisited(Background background) { - _featureChildren.Add(background); + _currentChildren.Add(background); } protected override void OnRuleVisiting(Rule rule) diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs index 9af3bd1e6..2c7722ffe 100644 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/CucumberConfiguration.cs @@ -55,7 +55,7 @@ public string ConfigureOutputDirectory(CucumberOutputConfiguration config) else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - _trace?.WriteTestOutput(logEntry); + _trace!.WriteTestOutput(logEntry); if (!Directory.Exists(outputDirectory)) { lock (_lock) diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index 03bcb55da..d927d8961 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -22,7 +22,7 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi public CucumberMessagePublisher() { - Debugger.Launch(); + //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { @@ -127,9 +127,9 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve { var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; var tccmt = new TestCaseCucumberMessageTracker(featureTracker); + tccmt.ProcessEvent(scenarioStartedEvent); traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); testCaseTrackersById.TryAdd(id, tccmt); - tccmt.ProcessEvent(scenarioStartedEvent); } else { diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index a8b6c7a2b..9c066f390 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -85,13 +85,13 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) if (!configurationDTO.FileOutputEnabled) { - trace?.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace!.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); return; } var configuration = new CucumberConfiguration(trace, environment); baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - + trace!.WriteTestOutput($"FileOutputPlugin LaunchFileSink. Cucumber Messages is ENABLED. Base Directory: {baseDirectory}"); trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); @@ -100,7 +100,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) public void Publish(ReqnrollCucumberMessage message) { var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); + //trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } @@ -113,12 +113,12 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); + //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); await Write(featureName, cm); } else { - trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); + //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); CloseFeatureStream(featureName); } } @@ -139,11 +139,11 @@ private async Task Write(string featureName, string cucumberMessage) { if (!fileStreams.ContainsKey(featureName)) { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); + fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, SanitizeFileName($"{featureName}.ndjson"))); } } } - trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); + trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {SanitizeFileName($"{featureName}.ndjson")}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); await fileStreams[featureName].WriteLineAsync(cucumberMessage); } catch (System.Exception ex) @@ -186,5 +186,31 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + public static string SanitizeFileName(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Get the invalid characters for file names + char[] invalidChars = Path.GetInvalidFileNameChars(); + + // Replace invalid characters with underscores + string sanitized = new string(input.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray()); + + // Remove leading and trailing spaces and dots + sanitized = sanitized.Trim().Trim('.'); + + // Ensure the filename is not empty after sanitization + if (string.IsNullOrEmpty(sanitized)) + return "_"; + + // Truncate the filename if it's too long (255 characters is a common limit) + const int maxLength = 255; + if (sanitized.Length > maxLength) + sanitized = sanitized.Substring(0, maxLength); + + return sanitized; + } + } } diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs index cb4ae4ae9..ec9da8b6c 100644 --- a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs @@ -46,8 +46,8 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } - public Dictionary StepsById { get; private set; } = new(); - public Dictionary StepsByEvent { get; private set; } = new(); + private Dictionary StepsById { get; set; } = new(); + private Dictionary StepsByEvent { get; set; } = new(); public List Steps { get diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson index f44c8ac17..cfaced93a 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson @@ -1,36 +1,52 @@ -{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+86e9495e4e6815ab377bdbc6ea2eebd6f89a0537"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} -{"source":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} -{"gherkinDocument":{"uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} -{"pickle":{"id":"14","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"10","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"11","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"12","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"13","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} -{"pickle":{"id":"20","uri":"FeatureFilec8b27db484c344498a4318b5fab8f32b.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"15","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"16","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"17","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"18","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"19","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} -{"stepDefinition":{"id":"21","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} -{"stepDefinition":{"id":"22","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} -{"stepDefinition":{"id":"23","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} -{"stepDefinition":{"id":"24","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} -{"stepDefinition":{"id":"25","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} -{"testRunStarted":{"timestamp":{"seconds":1727192229,"nanos":420947400}}} -{"testCase":{"id":"26","pickleId":"14","testSteps":[{"id":"28","pickleStepId":"10","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"11","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"26","timestamp":{"seconds":1727192229,"nanos":452538900}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"28","timestamp":{"seconds":1727192229,"nanos":455045500}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"28","testStepResult":{"duration":{"seconds":0,"nanos":19933400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":474978900}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"29","timestamp":{"seconds":1727192229,"nanos":476934500}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"29","testStepResult":{"duration":{"seconds":0,"nanos":698600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":477633100}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"30","timestamp":{"seconds":1727192229,"nanos":478014600}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"30","testStepResult":{"duration":{"seconds":0,"nanos":1699400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":479714000}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"31","timestamp":{"seconds":1727192229,"nanos":479992900}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"31","testStepResult":{"duration":{"seconds":0,"nanos":551600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":480544500}}} -{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"seconds":1727192229,"nanos":482528500},"willBeRetried":false}} -{"testCase":{"id":"32","pickleId":"20","testSteps":[{"id":"34","pickleStepId":"15","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"17","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"18","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"38","pickleStepId":"19","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"33","testCaseId":"32","timestamp":{"seconds":1727192229,"nanos":495596400}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"34","timestamp":{"seconds":1727192229,"nanos":495781000}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"34","testStepResult":{"duration":{"seconds":0,"nanos":278500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496059500}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"35","timestamp":{"seconds":1727192229,"nanos":496095600}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"35","testStepResult":{"duration":{"seconds":0,"nanos":83500},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496179100}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"36","timestamp":{"seconds":1727192229,"nanos":496202200}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"36","testStepResult":{"duration":{"seconds":0,"nanos":685000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":496887200}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"37","timestamp":{"seconds":1727192229,"nanos":496910500}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"37","testStepResult":{"duration":{"seconds":0,"nanos":512200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497422700}}} -{"testStepStarted":{"testCaseStartedId":"33","testStepId":"38","timestamp":{"seconds":1727192229,"nanos":497441700}}} -{"testStepFinished":{"testCaseStartedId":"33","testStepId":"38","testStepResult":{"duration":{"seconds":0,"nanos":194200},"message":"","status":"PASSED"},"timestamp":{"seconds":1727192229,"nanos":497635900}}} -{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":1727192229,"nanos":497750600},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"seconds":1727192229,"nanos":499781200}}} +{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+098e7ef7a96ecab08f239f15463aec6b9098c778"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"source":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n\r\nRule: A rule with a background\r\n\tBackground: First Transfer Money\r\n\t\tWhen I transfer $50 from savings to checking\r\n\t\tThen My savings account has a balance of $150\r\n\tScenario: total balance unchanged\r\n\t\tWhen the accounts are combined\r\n\t\tThen I have $700\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}},{"rule":{"location":{"line":19,"column":1},"tags":[],"keyword":"Rule","name":"A rule with a background","description":"","children":[{"background":{"location":{"line":20,"column":2},"keyword":"Background","name":"First Transfer Money","description":"","steps":[{"location":{"line":21,"column":3},"keyword":"When ","keywordType":"Action","text":"I transfer $50 from savings to checking","id":"10"},{"location":{"line":22,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"My savings account has a balance of $150","id":"11"}],"id":"12"}},{"scenario":{"location":{"line":23,"column":2},"tags":[],"keyword":"Scenario","name":"total balance unchanged","description":"","steps":[{"location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"13"},{"location":{"line":25,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"14"}],"examples":[],"id":"15"}}],"id":"16"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} +{"pickle":{"id":"21","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"17","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"18","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"19","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"20","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} +{"pickle":{"id":"27","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"22","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"23","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"24","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"25","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"26","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} +{"pickle":{"id":"34","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"total balance unchanged","language":"en-US","steps":[{"astNodeIds":["0"],"id":"28","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"29","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["10"],"id":"30","type":"Action","text":"I transfer $50 from savings to checking"},{"astNodeIds":["11"],"id":"31","type":"Outcome","text":"My savings account has a balance of $150"},{"astNodeIds":["13"],"id":"32","type":"Action","text":"the accounts are combined"},{"astNodeIds":["14"],"id":"33","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["15"]}} +{"stepDefinition":{"id":"35","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} +{"stepDefinition":{"id":"36","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"37","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} +{"stepDefinition":{"id":"38","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} +{"stepDefinition":{"id":"39","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} +{"testRunStarted":{"timestamp":{"seconds":1727518980,"nanos":932282100}}} +{"testCase":{"id":"40","pickleId":"21","testSteps":[{"id":"42","pickleStepId":"17","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"43","pickleStepId":"18","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"44","pickleStepId":"19","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"45","pickleStepId":"20","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"41","testCaseId":"40","timestamp":{"seconds":1727518980,"nanos":988751100}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"42","timestamp":{"seconds":1727518980,"nanos":991631700}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"42","testStepResult":{"duration":{"seconds":0,"nanos":31313600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":22945300}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"43","timestamp":{"seconds":1727518981,"nanos":25132700}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"43","testStepResult":{"duration":{"seconds":0,"nanos":856400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":25989100}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"44","timestamp":{"seconds":1727518981,"nanos":26528500}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"44","testStepResult":{"duration":{"seconds":0,"nanos":2514700},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":29043200}}} +{"testStepStarted":{"testCaseStartedId":"41","testStepId":"45","timestamp":{"seconds":1727518981,"nanos":29468400}}} +{"testStepFinished":{"testCaseStartedId":"41","testStepId":"45","testStepResult":{"duration":{"seconds":0,"nanos":728600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":30197000}}} +{"testCaseFinished":{"testCaseStartedId":"41","timestamp":{"seconds":1727518981,"nanos":33150200},"willBeRetried":false}} +{"testCase":{"id":"46","pickleId":"27","testSteps":[{"id":"48","pickleStepId":"22","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"49","pickleStepId":"23","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"50","pickleStepId":"24","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"51","pickleStepId":"25","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"26","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"47","testCaseId":"46","timestamp":{"seconds":1727518981,"nanos":75179500}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"48","timestamp":{"seconds":1727518981,"nanos":75361800}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"48","testStepResult":{"duration":{"seconds":0,"nanos":378000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75739800}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"49","timestamp":{"seconds":1727518981,"nanos":75791400}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"49","testStepResult":{"duration":{"seconds":0,"nanos":133800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75925200}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"50","timestamp":{"seconds":1727518981,"nanos":75964700}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"50","testStepResult":{"duration":{"seconds":0,"nanos":986400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":76951100}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"51","timestamp":{"seconds":1727518981,"nanos":76991800}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"51","testStepResult":{"duration":{"seconds":0,"nanos":739400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":77731200}}} +{"testStepStarted":{"testCaseStartedId":"47","testStepId":"52","timestamp":{"seconds":1727518981,"nanos":77758700}}} +{"testStepFinished":{"testCaseStartedId":"47","testStepId":"52","testStepResult":{"duration":{"seconds":0,"nanos":283000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":78041700}}} +{"testCaseFinished":{"testCaseStartedId":"47","timestamp":{"seconds":1727518981,"nanos":78211800},"willBeRetried":false}} +{"testCase":{"id":"53","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"28","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"56","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"57","pickleStepId":"30","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"50"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"58","pickleStepId":"31","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"150"},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"60","pickleStepId":"33","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"53","timestamp":{"seconds":1727518981,"nanos":85654600}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"55","timestamp":{"seconds":1727518981,"nanos":85763100}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"55","testStepResult":{"duration":{"seconds":0,"nanos":490900},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86254000}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"56","timestamp":{"seconds":1727518981,"nanos":86283700}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"56","testStepResult":{"duration":{"seconds":0,"nanos":101800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86385500}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"57","timestamp":{"seconds":1727518981,"nanos":86404200}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"57","testStepResult":{"duration":{"seconds":0,"nanos":270000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86674200}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"58","timestamp":{"seconds":1727518981,"nanos":86695700}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"58","testStepResult":{"duration":{"seconds":0,"nanos":91100},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86786800}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"59","timestamp":{"seconds":1727518981,"nanos":86800600}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"59","testStepResult":{"duration":{"seconds":0,"nanos":173800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86974400}}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"60","timestamp":{"seconds":1727518981,"nanos":86988900}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"60","testStepResult":{"duration":{"seconds":0,"nanos":225400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":87214300}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":1727518981,"nanos":87242200},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"seconds":1727518981,"nanos":96745400}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 6ef87429d..3eb7719de 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -79,6 +79,31 @@ When I eat 5 cukes ShouldAllScenariosPass(); } + [TestMethod] + public void SmokeTestMultipleFeatures() + { + ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); + ResetCucumberMessages("Cucumber Messages Smoke Test Second Smoke Test.ndjson"); + EnableCucumberMessages(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test Second Smoke Test + Scenario: Eating Other Cukes + When I eat 6 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] public void SmokeOutlineTest() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 0813739d3..d9a14c9e4 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -70,9 +70,11 @@ Always + + Always + - From 96bd3a882cf59038575c79b4e99f0fa57bec8ed5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:14:16 -0500 Subject: [PATCH 162/218] Fix Publisher to subscribe to TestThreadPublisher on CustomizeTestThreadDependencies. Check for null Feature Context on hooks that execute before a feature is started. --- Reqnroll/CucumberMessages/CucumberMessagePublisher.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs index d927d8961..730b4083d 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs @@ -26,7 +26,7 @@ public CucumberMessagePublisher() } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - runtimePluginEvents.CustomizeFeatureDependencies += (sender, args) => + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { objectContainer = args.ObjectContainer; _brokerFactory = new Lazy(() => objectContainer.Resolve()); @@ -184,7 +184,9 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. + // Bypass them by checking for null + var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingStartedEvent); } @@ -194,7 +196,9 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; + // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. + // Bypass them by checking for null + var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingFinishedEvent); } From 2d20806e818f032ad388d20b9526fdea36bd9fa2 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:03:07 -0500 Subject: [PATCH 163/218] Added the Empty scenario from the CCK; adjusted test validation to account for step-less Scenarios. --- .../CCK/empty/empty.cs | 12 ++++++++++++ .../CCK/empty/empty.feature | 7 +++++++ .../CCK/empty/empty.feature.ndjson | 9 +++++++++ .../CucumberCompatibilityTests.cs | 1 + .../CucumberMessages.CompatibilityTests.csproj | 8 ++++++++ .../CucumberMessagesValidator.cs | 13 ++++++++++--- 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs new file mode 100644 index 000000000..d606b7f0a --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.CCK.empty +{ + internal class Empty + { + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature new file mode 100644 index 000000000..eb6eced01 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature @@ -0,0 +1,7 @@ +Feature: Empty Scenarios + Sometimes we want to quickly jot down a new scenario without specifying any actual steps + for what should be executed. + + In this instance we want to stipulate what should / shouldn't run and what the output is + + Scenario: Blank Scenario diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson new file mode 100644 index 000000000..4d171d6dd --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson @@ -0,0 +1,9 @@ +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"17.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"22.7.0"}}} +{"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is\n\n Scenario: Blank Scenario\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/empty/empty.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"0","keyword":"Scenario","location":{"column":3,"line":7},"name":"Blank Scenario","steps":[],"tags":[]}}],"description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Empty Scenarios","tags":[]},"uri":"samples/empty/empty.feature"}} +{"pickle":{"astNodeIds":["0"],"id":"1","language":"en","name":"Blank Scenario","steps":[],"tags":[],"uri":"samples/empty/empty.feature"}} +{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"2","pickleId":"1","testSteps":[]}} +{"testCaseStarted":{"attempt":0,"id":"3","testCaseId":"2","timestamp":{"nanos":1000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"3","timestamp":{"nanos":2000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"nanos":3000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 3eb7719de..43c196c8b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -282,6 +282,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) [DataRow("minimal", "minimal")] [DataRow("cdata", "cdata")] [DataRow("pending", "Pending steps")] + [DataRow("empty", "Empty Scenarios")] [DataRow("examples-tables", "Examples Tables")] [DataRow("data-tables", "Data Tables")] [DataRow("hooks", "Hooks")] diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index d9a14c9e4..d4620485d 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -15,6 +15,7 @@ + @@ -38,6 +39,8 @@ + + @@ -73,6 +76,9 @@ Always + + Always + @@ -85,6 +91,8 @@ Always + + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 7bcb01a15..e8710f5c0 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -148,8 +148,11 @@ public void ResultShouldPassSanityChecks() private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpected() { - // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message + // IF the expected results contains no TestStepStarted messages, then there is nothing to check + if (!expecteds_elementsByType.Keys.Contains(typeof(TestStepStarted))) + return; + // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message var actualTestStepStarted_TestStepIds = actuals_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); var expectedTestStepStarteds_TestStepIds = expecteds_elementsByType[typeof(TestStepStarted)].OfType().Select(tss => tss.TestStepId).ToList(); @@ -190,12 +193,16 @@ private void TestExecutionStepsShouldProperlyReferenceTestCases() var testCaseStartedIds = testCaseStarteds.Select(tcs => tcs.Id).ToList(); var testCaseFinisheds = actuals_elementsByType[typeof(TestCaseFinished)].OfType().ToList(); + testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); + + // IF the Scenario has no steps, return early. + if (!actuals_elementsByType.Keys.Contains(typeof(TestStepStarted))) + return; var testStepStarteds = actuals_elementsByType[typeof(TestStepStarted)].OfType().ToList(); var testStepFinisheds = actuals_elementsByType[typeof(TestStepFinished)].OfType().ToList(); - + testCaseStartedIds.Should().Contain(id => testStepStarteds.Any(tss => tss.TestCaseStartedId == id), "a test case started should be referenced by at least one test step started message"); testCaseStartedIds.Should().Contain(id => testStepFinisheds.Any(tsf => tsf.TestCaseStartedId == id), "a test case started should be referenced by at least one test step finished message"); - testCaseStartedIds.Should().Contain(id => testCaseFinisheds.Any(tcf => tcf.TestCaseStartedId == id), "a test case started should be referenced by a test case finished message"); } private void TestExecutionMessagesShouldProperlyNest() From 042cc2cb1581492afbfae6869ad6fd3a7c51f511 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:26:53 -0500 Subject: [PATCH 164/218] Removing the Messages FileSink Plugin directory as it is no longer needed. --- .../CucumberMessages-config-schema.json | 34 --- .../FileSinkConfiguration.cs | 35 ---- .../FileSinkPlugin.cs | 194 ------------------ ...mberMessage.FileSink.ReqnrollPlugin.csproj | 21 -- 4 files changed, 284 deletions(-) delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs delete mode 100644 Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json deleted file mode 100644 index f7fcd8f17..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/CucumberMessages-config-schema.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "filSinkEnabled": { - "type": "boolean", - "description": "Indicates whether the program is enabled or not" - }, - "destinations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Indicates whether this destination setting is enabled" - }, - "basePath": { - "type": "string", - "description": "The base path for output files" - }, - "outputDirectory": { - "type": "string", - "description": "The directory path where output should go" - } - }, - "required": [ "enabled", "basePath", "outputDirectory" ] - }, - "minItems": 1, - "description": "Array of destination settings" - } - }, - "required": [ "programEnabled", "destinations" ] -} \ No newline at end of file diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs deleted file mode 100644 index 7a56f4233..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkConfiguration.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - internal class FileSinkConfiguration - { - public bool FileSinkEnabled { get; set; } - public List Destinations { get; set; } - - public FileSinkConfiguration() : this(true) { } - public FileSinkConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public FileSinkConfiguration(bool fileSinkEnabled, List destinations) - { - FileSinkEnabled = fileSinkEnabled; - Destinations = destinations; - } - } - - public class Destination - { - public bool Enabled { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(bool enabled, string basePath, string outputDirectory) - { - Enabled = true; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs deleted file mode 100644 index d95de1804..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/FileSinkPlugin.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Reqnroll.CucumberMessages; -using Reqnroll.Plugins; -using Reqnroll.UnitTestProvider; -using Io.Cucumber.Messages; -using Cucumber.Messages; -using Io.Cucumber.Messages.Types; -using System.Reflection; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; -using System.Diagnostics; -using Reqnroll.Events; -using System.Collections.Concurrent; -using System.Text.Json; -using Reqnroll.Tracing; -using Reqnroll.BoDi; - -[assembly: RuntimePlugin(typeof(FileSinkPlugin))] - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class FileSinkPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin - { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; - private Task? fileWritingTask; - - //Thread safe collections to hold: - // 1. Inbound Cucumber Messages - BlockingCollection - // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) - private object _lock = new(); - private readonly BlockingCollection postedMessages = new(); - private readonly ConcurrentDictionary fileStreams = new(); - private FileSinkConfiguration? configuration; - private string baseDirectory = ""; - private Lazy? traceListener; - private ITraceListener? trace => traceListener?.Value; - private IObjectContainer? objectContainer; - - public FileSinkPlugin() - { - traceListener = new Lazy(() => objectContainer!.Resolve()); - } - - public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) - { - - runtimePluginEvents.RegisterGlobalDependencies += (sender, args) => args.ObjectContainer.RegisterInstanceAs(this); - - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => - { - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - objectContainer = args.ObjectContainer; - testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); - testThreadExecutionEventPublisher.AddHandler(CloseFileSink); - }; - } - - private string ProcessConfiguration(FileSinkConfiguration configuration) - { - var activeDestination = configuration.Destinations.Where(d => d.Enabled).FirstOrDefault(); - - if (activeDestination != null) - { - var basePath = Path.Combine(activeDestination.BasePath, activeDestination.OutputDirectory); - if (!Directory.Exists(basePath)) - { - lock (_lock) - { - if (!Directory.Exists(basePath)) - Directory.CreateDirectory(basePath); - } - } - trace?.WriteTestOutput($"FileSinkPlugin Initialized. BasePath: {basePath}"); - return basePath; - } - else - { - var location = Assembly.GetExecutingAssembly().Location; - trace?.WriteTestOutput($"FileSinkPlugin Initialized from Assembly Location. BasePath: {location}"); - return location; - } - } - - private void CloseFileSink(TestRunFinishedEvent @event) - { - trace?.WriteTestOutput("FileSinkPlugin Closing File Sink long running thread."); - postedMessages.CompleteAdding(); - fileWritingTask?.Wait(); - fileWritingTask = null; - } - - private void LaunchFileSink(TestRunStartedEvent testRunStarted) - { - trace?.WriteTestOutput("FileSinkPlugin Starting File Sink long running thread."); - configuration = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - if (!configuration!.FileSinkEnabled) - { - trace?.WriteTestOutput("FileSinkPlugin LaunchFileSink. FileSinkEnabled = false. Returning"); - return; - } - - baseDirectory = ProcessConfiguration(configuration); - - fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); - } - - public void Publish(ReqnrollCucumberMessage message) - { - var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - trace?.WriteTestOutput($"FileSinkPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); - postedMessages.Add(message); - } - - private async Task ConsumeAndWriteToFiles() - { - foreach (var message in postedMessages.GetConsumingEnumerable()) - { - var featureName = message.CucumberMessageSource; - - if (message.Envelope != null) - { - var cm = Serialize(message.Envelope); - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); - await Write(featureName, cm); - } - else - { - trace?.WriteTestOutput($"FileSinkPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); - CloseFeatureStream(featureName); - } - } - } - - - private string Serialize(Envelope message) - { - return NdjsonSerializer.Serialize(message); - } - private async Task Write(string featureName, string cucumberMessage) - { - try - { - if (!fileStreams.ContainsKey(featureName)) - { - lock (_lock) - { - if (!fileStreams.ContainsKey(featureName)) - { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, $"{featureName}.ndjson")); - } - } - } - trace?.WriteTestOutput($"FileSinkPlugin Write. Writing to: {featureName}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); - await fileStreams[featureName].WriteLineAsync(cucumberMessage); - } - catch (System.Exception ex) - { - trace?.WriteTestOutput($"FileSinkPlugin Write. Exception: {ex.Message}"); - } - } - - private void CloseFeatureStream(string featureName) - { - trace?.WriteTestOutput($"FileSinkPlugin CloseFeatureStream. Closing: {featureName}."); - fileStreams[featureName].Close(); - fileStreams.TryRemove(featureName, out var _); - } - private bool disposedValue; - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - CloseFileSink(new TestRunFinishedEvent()); - postedMessages.Dispose(); - foreach (var stream in fileStreams.Values) - { - stream.Close(); - stream.Dispose(); - }; - fileStreams.Clear(); - } - - disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} diff --git a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj b/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj deleted file mode 100644 index 3b49d3ad1..000000000 --- a/Plugins/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin/Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netstandard2.0 - enable - enable - True - Reqnroll Plugin which publishes test results as Cucumber Messages - - - - - - - - - - - - - From 6b8775bc50a1d2796a6c71371da3f6a8aaaf7dae Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:43:58 -0500 Subject: [PATCH 165/218] Moved the assignment of a pickle step ID from inline generated code to the TestExecutionEngine ExecuteStepAsync. it also calls NextStep on the pickleStepSequence. --- .../Generation/ScenarioPartHelper.cs | 6 +----- .../Generation/UnitTestMethodGenerator.cs | 3 ++- Reqnroll/ITestRunner.cs | 10 +++++----- .../Infrastructure/ITestExecutionEngine.cs | 3 ++- .../Infrastructure/TestExecutionEngine.cs | 6 +++++- Reqnroll/ScenarioInfo.cs | 6 +++++- Reqnroll/TestRunner.cs | 20 +++++++++---------- .../CucumberExpressionIntegrationTests.cs | 2 +- 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 959d3f4f6..63d7dd7ab 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -95,8 +95,7 @@ public void GenerateStep(TestClassGenerationContext generationContext, List ConvertArg(object value, IBindingType typeToConvertTo #region Given-When-Then - public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg, string pickleStepId = null) + public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, string keyword, string text, string multilineTextArg, Table tableArg) { StepDefinitionType stepDefinitionType = stepDefinitionKeyword == StepDefinitionKeyword.And || stepDefinitionKeyword == StepDefinitionKeyword.But ? GetCurrentBindingType() : (StepDefinitionType) stepDefinitionKeyword; + var stepSequenceIdentifiers = ScenarioContext.ScenarioInfo.PickleStepSequence; + var pickleStepId = stepSequenceIdentifiers?.CurrentPickleStepId ?? ""; + _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); @@ -638,6 +641,7 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, finally { _testThreadExecutionEventPublisher.PublishEvent(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + stepSequenceIdentifiers?.NextStep(); _contextManager.CleanupStepContext(); } } diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 0247f7e54..7271f6f5b 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -1,3 +1,4 @@ +using Reqnroll.CucumberMessages; using System; using System.Collections.Specialized; using System.Linq; @@ -40,8 +41,10 @@ public class ScenarioInfo /// The PickleId of the Scenario when exported as a Cucumber Message "pickle". /// public string PickleId { get; } + // The list of step PickleIds in the step sequence for this test case. + public PickleStepSequence PickleStepSequence { get; private set; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null) + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null, PickleStepSequence stepSequence = null) { Title = title; Description = description; @@ -49,6 +52,7 @@ public ScenarioInfo(string title, string description, string[] tags, IOrderedDic Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); PickleId = pickleId; + PickleStepSequence = stepSequence; } } } \ No newline at end of file diff --git a/Reqnroll/TestRunner.cs b/Reqnroll/TestRunner.cs index dbbe282dc..82c5bff7a 100644 --- a/Reqnroll/TestRunner.cs +++ b/Reqnroll/TestRunner.cs @@ -73,29 +73,29 @@ public async Task OnTestRunEndAsync() await _executionEngine.OnTestRunEndAsync(); } - public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task GivenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.Given, keyword, text, multilineTextArg, tableArg); } - public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task WhenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.When, keyword, text, multilineTextArg, tableArg); } - public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task ThenAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.Then, keyword, text, multilineTextArg, tableArg); } - public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task AndAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.And, keyword, text, multilineTextArg, tableArg); } - public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null, string pickleStepId = null) + public async Task ButAsync(string text, string multilineTextArg, Table tableArg, string keyword = null) { - await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg, pickleStepId); + await _executionEngine.StepAsync(StepDefinitionKeyword.But, keyword, text, multilineTextArg, tableArg); } public void Pending() diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 2f4511a66..4c140d512 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -186,7 +186,7 @@ private async Task PerformStepExecution(string methodName, strin await engine.OnFeatureStartAsync(new FeatureInfo(CultureInfo.GetCultureInfo("en-US"), ".", "Sample feature", null, ProgrammingLanguage.CSharp)); await engine.OnScenarioStartAsync(); engine.OnScenarioInitialize(new ScenarioInfo("Sample scenario", null, null, null)); - await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null, "stubPickleStepId"); + await engine.StepAsync(StepDefinitionKeyword.Given, "Given ", stepText, null, null); var contextManager = testThreadContainer.Resolve(); contextManager.ScenarioContext.ScenarioExecutionStatus.Should().Be(ScenarioExecutionStatus.OK, $"should not fail with '{contextManager.ScenarioContext.TestError?.Message}'"); From 98ac31577737c5d07140ad984092cfa707bfe96b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:44:35 -0500 Subject: [PATCH 166/218] Refactored Configuration into distinct subsystem. --- .../Configuration/ConfigurationDTO.cs | 51 +++++++ .../Configuration/CucumberConfiguration.cs | 130 ++++++++++++++++++ .../CucumberConfigurationConstants.cs | 11 ++ .../CucumberMessages-config-schema.json | 32 +++++ .../DefaultConfigurationSource.cs | 28 ++++ .../Configuration/IConfigurationSource.cs | 11 ++ .../Configuration/ICucumberConfiguration.cs | 7 + .../RCM_ConfigFile_ConfigurationSource.cs | 20 +++ .../Configuration/ResolvedConfiguration.cs | 11 ++ .../CucumberMessages/CucumberConfiguration.cs | 72 ---------- .../CucumberMessages-config-schema.json | 34 ----- .../CucumberOutputConfiguration.cs | 40 ------ Reqnroll/CucumberMessages/FileOutputPlugin.cs | 33 ++--- .../DefaultDependencyProvider.cs | 2 + .../CucumberCompatibilityTestBase.cs | 13 +- .../CucumberMessages.configuration.json | 3 +- 16 files changed, 324 insertions(+), 174 deletions(-) create mode 100644 Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json create mode 100644 Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs create mode 100644 Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs delete mode 100644 Reqnroll/CucumberMessages/CucumberConfiguration.cs delete mode 100644 Reqnroll/CucumberMessages/CucumberMessages-config-schema.json delete mode 100644 Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs new file mode 100644 index 000000000..c3c5d0a82 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + /// + /// This class holds configuration information from a configuration source. + /// Each configuration source may provide one or more Profiles (such as Dev or Prod). + /// The default profile is always named 'DEFAULT'. + /// + public class ConfigurationDTO + { + + public bool FileOutputEnabled { get; set; } + public string ActiveProfileName { get; set; } + public List Profiles { get; set; } + + public Profile ActiveProfile => Profiles.Where(p => p.ProfileName == ActiveProfileName).FirstOrDefault(); + + public ConfigurationDTO() : this(true) { } + public ConfigurationDTO(bool enabled) : this(enabled, "DEFAULT", new List()) { } + public ConfigurationDTO(bool enabled, string activeProfile, List profiles) + { + FileOutputEnabled = enabled; + ActiveProfileName = activeProfile; + Profiles = profiles; + } + + } + + public class Profile + { + public string ProfileName { get; set; } + public string BasePath { get; set; } + public string OutputDirectory { get; set; } + public string OutputFileName { get; set; } + + public Profile(string profileName, string basePath, string outputDirectory, string outputFileName) + { + ProfileName = string.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; + BasePath = basePath ?? ""; + OutputDirectory = outputDirectory ?? ""; + OutputFileName = outputFileName ?? ""; + } + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs new file mode 100644 index 000000000..18af77f5d --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -0,0 +1,130 @@ +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Tracing; +using System; +using System.IO; +using System.Linq; + +namespace Reqnroll.CucumberMessages.Configuration +{ + public class CucumberConfiguration : ICucumberConfiguration + { + private ITraceListener _trace; + private IEnvironmentWrapper _environmentWrapper; + + private ResolvedConfiguration outputConfiguration = new(); + private bool _enablementOverrideFlag = true; + + public bool Enabled => _enablementOverrideFlag && outputConfiguration.Enabled; + public string BaseDirectory => outputConfiguration.BaseDirectory; + public string OutputDirectory => outputConfiguration.OutputDirectory; + public string OutputFileName => outputConfiguration.OutputFileName; + + public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) + { + _trace = traceListener; + _environmentWrapper = environmentWrapper; + } + #region Override API + public void SetEnabled(bool value) + { + _enablementOverrideFlag = value; + } + #endregion + public ResolvedConfiguration ResolveConfiguration() + { + var config = ApplyHierarchicalConfiguration(); + var resolved = ApplyEnvironmentOverrides(config); + EnsureOutputDirectory(resolved); + + string logEntry; + logEntry = $"Cucumber Messages: FileOutput Initialized. Output Path: {Path.Combine(resolved.BaseDirectory, resolved.OutputDirectory, resolved.OutputFileName)}"; + + _trace!.WriteTestOutput(logEntry); + outputConfiguration = resolved; + return resolved; + } + private ConfigurationDTO ApplyHierarchicalConfiguration() + { + var defaultConfigurationProvider = new DefaultConfigurationSource(_environmentWrapper); + var fileBasedConfigurationProvider = new RCM_ConfigFile_ConfigurationSource(); + + ConfigurationDTO config = defaultConfigurationProvider.GetConfiguration(); + config = AddConfig(config, fileBasedConfigurationProvider.GetConfiguration()); + return config; + } + + private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) + { + var baseOutDirValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE); + var relativePathValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE); + var fileNameValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE); + var profileValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); + string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + + var activeConfiguredDestination = config.Profiles.Where(d => d.ProfileName == profileName).FirstOrDefault(); + + if (activeConfiguredDestination != null) + { + config.ActiveProfileName = profileName; + }; + var result = new ResolvedConfiguration() + { + Enabled = config.FileOutputEnabled, + BaseDirectory = config.ActiveProfile.BasePath, + OutputDirectory = config.ActiveProfile.OutputDirectory, + OutputFileName = config.ActiveProfile.OutputFileName + }; + + if (baseOutDirValue is Success) + result.BaseDirectory = ((Success)baseOutDirValue).Result; + + if (relativePathValue is Success) + result.OutputDirectory = ((Success)relativePathValue).Result; + + if (fileNameValue is Success) + result.OutputFileName = ((Success)fileNameValue).Result; + var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; + + result.Enabled = Convert.ToBoolean(enabled); + + return result; + } + + private ConfigurationDTO AddConfig(ConfigurationDTO config, ConfigurationDTO overridingConfig) + { + if (overridingConfig != null) + { + config.Profiles.AddRange(overridingConfig.Profiles); + if (overridingConfig.ActiveProfileName != null && !config.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) + { + // The incoming configuration DTO points to a profile that doesn't exist. + _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {config.ActiveProfileName} instead."); + } + else if (overridingConfig.ActiveProfileName != null) + config.ActiveProfileName = overridingConfig.ActiveProfileName; + + config.FileOutputEnabled = overridingConfig.FileOutputEnabled; + } + + return config; + } + + private void EnsureOutputDirectory(ResolvedConfiguration config) + { + + if (!Directory.Exists(config.BaseDirectory)) + { + Directory.CreateDirectory(config.BaseDirectory); + Directory.CreateDirectory(config.OutputDirectory); + } + else if (!Directory.Exists(Path.Combine(config.BaseDirectory, config.OutputDirectory))) + { + Directory.CreateDirectory(Path.Combine(config.BaseDirectory, config.OutputDirectory)); + } + } + + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs new file mode 100644 index 000000000..335ce9e6a --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public static class CucumberConfigurationConstants + { + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME"; + public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; + public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json new file mode 100644 index 000000000..8f7c6e917 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json @@ -0,0 +1,32 @@ +{ + "description": "This class holds configuration information from a configuration source.\nEach configuration source may provide one or more Profiles (such as Dev or Prod). \nThe default profile is always named \u0027DEFAULT\u0027.", + "type": "object", + "properties": { + "FileOutputEnabled": { + "type": "boolean" + }, + "ActiveProfileName": { + "type": "string" + }, + "Profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ProfileName": { + "type": "string" + }, + "BasePath": { + "type": "string" + }, + "OutputDirectory": { + "type": "string" + }, + "OutputFileName": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs new file mode 100644 index 000000000..1138b160e --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -0,0 +1,28 @@ +using Reqnroll.EnvironmentAccess; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal class DefaultConfigurationSource : IConfigurationSource + { + private IEnvironmentWrapper _environmentWrapper; + + public DefaultConfigurationSource(IEnvironmentWrapper environmentWrapper) + { + _environmentWrapper = environmentWrapper; + } + public ConfigurationDTO GetConfiguration() + { + var res = new ConfigurationDTO(); + string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); + string defaultOutputFileName = "reqnroll_report.ndjson"; + + var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName); + res.FileOutputEnabled = false; + res.Profiles.Add(defaultProfile); + return res; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs new file mode 100644 index 000000000..2993802cb --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal interface IConfigurationSource + { + ConfigurationDTO GetConfiguration(); + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs new file mode 100644 index 000000000..5f77e3d58 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs @@ -0,0 +1,7 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public interface ICucumberConfiguration + { + ResolvedConfiguration ResolveConfiguration(); + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs new file mode 100644 index 000000000..d3521255d --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Text.Json; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal class RCM_ConfigFile_ConfigurationSource : IConfigurationSource + { + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + + public ConfigurationDTO GetConfiguration() + { + ConfigurationDTO configurationDTO = null; + if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) + { + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + } + return configurationDTO; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs new file mode 100644 index 000000000..a11c69f05 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public class ResolvedConfiguration + { + public bool Enabled { get; set; } + public string BaseDirectory { get; set; } + public string OutputDirectory { get; set; } + public string OutputFileName { get; set; } + } +} + diff --git a/Reqnroll/CucumberMessages/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/CucumberConfiguration.cs deleted file mode 100644 index 2c7722ffe..000000000 --- a/Reqnroll/CucumberMessages/CucumberConfiguration.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Reqnroll.CommonModels; -using Reqnroll.EnvironmentAccess; -using Reqnroll.Tracing; -using System; -using System.IO; -using System.Linq; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class CucumberConfiguration - { - internal const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY"; - internal const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; - public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; - - private ITraceListener _trace; - private IEnvironmentWrapper _environmentWrapper; - private object _lock = new(); - - private CucumberOutputConfiguration outputConfiguration; - public bool Enabled => outputConfiguration != null ? outputConfiguration.FileOutputEnabled : false; - - public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) - { - _trace = traceListener; - _environmentWrapper = environmentWrapper; - } - - public string ConfigureOutputDirectory(CucumberOutputConfiguration config) - { - outputConfiguration = config; - string outputDirectory; - string configuredOutputDirectory = string.Empty; - string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); - var outDirValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_OUTPUT_DIRECTORY_ENVIRONMENT_VARIABLE); - var profileValue = _environmentWrapper.GetEnvironmentVariable(REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); - string environmentVariableOutputDirectory = outDirValue is Success ? ((Success)outDirValue).Result : null; - string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; - - var activeConfiguredDestination = config.Destinations.Where(d => d.ProfileName == profileName).FirstOrDefault(); - - if (activeConfiguredDestination != null) - { - configuredOutputDirectory = Path.Combine(activeConfiguredDestination.BasePath, activeConfiguredDestination.OutputDirectory); - } - - outputDirectory = defaultOutputDirectory; - if (!String.IsNullOrEmpty(configuredOutputDirectory)) - outputDirectory = configuredOutputDirectory; - if (!String.IsNullOrEmpty(environmentVariableOutputDirectory)) - outputDirectory = environmentVariableOutputDirectory; - - string logEntry; - if (outputDirectory == defaultOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Assembly Location. BasePath: {outputDirectory}"; - else if (outputDirectory == configuredOutputDirectory) logEntry = $"FileOutputPlugin Initialized from Configuration File. BasePath: {configuredOutputDirectory}"; - else logEntry = $"FileOutputPlugin Initialized from Environment Variable. BasePath: {environmentVariableOutputDirectory}"; - - _trace!.WriteTestOutput(logEntry); - if (!Directory.Exists(outputDirectory)) - { - lock (_lock) - { - if (!Directory.Exists(outputDirectory)) - Directory.CreateDirectory(outputDirectory); - } - } - return outputDirectory; - } - - } -} - diff --git a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json deleted file mode 100644 index b6f6b7919..000000000 --- a/Reqnroll/CucumberMessages/CucumberMessages-config-schema.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "fileOutputEnabled": { - "type": "boolean", - "description": "Indicates whether the program is enabled or not" - }, - "destinations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "profileName": { - "type": "string", - "description": "Provides a description for this destination (ex: 'dev' or 'prod')" - }, - "basePath": { - "type": "string", - "description": "The base path for output files" - }, - "outputDirectory": { - "type": "string", - "description": "The subdirectory of the base path where output should go" - } - }, - "required": [ "profileName", "basePath", "outputDirectory" ] - }, - "minItems": 1, - "description": "Array of destination settings" - } - }, - "required": [ "fileOutputEnabled", "destinations" ] -} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs b/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs deleted file mode 100644 index e6c9499e3..000000000 --- a/Reqnroll/CucumberMessages/CucumberOutputConfiguration.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Reqnroll.Time; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Text; - -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin -{ - public class CucumberOutputConfiguration - { - - public bool FileOutputEnabled { get; set; } - public List Destinations { get; set; } - - public CucumberOutputConfiguration() : this(true) { } - public CucumberOutputConfiguration(bool fileSinkEnabled) : this(fileSinkEnabled, new List()) { } - public CucumberOutputConfiguration(bool fileSinkEnabled, List destinations) - { - FileOutputEnabled = fileSinkEnabled; - Destinations = destinations; - } - - } - - public class Destination - { - public string ProfileName { get; set; } - public string BasePath { get; set; } - public string OutputDirectory { get; set; } - - public Destination(string profileName, string basePath, string outputDirectory) - { - ProfileName = String.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; - BasePath = basePath; - OutputDirectory = outputDirectory; - } - } -} - diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index 9c066f390..b2711813f 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -17,13 +17,13 @@ using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; using System.Diagnostics; +using Reqnroll.CucumberMessages.Configuration; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; private Task? fileWritingTask; private object _lock = new(); @@ -33,16 +33,18 @@ public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugi private readonly BlockingCollection postedMessages = new(); private readonly ConcurrentDictionary fileStreams = new(); private string baseDirectory = ""; + private ICucumberConfiguration _configuration; private Lazy traceListener; private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; private IObjectContainer? globalObjectContainer; - public FileOutputPlugin() + public FileOutputPlugin(ICucumberConfiguration configuration) { + _configuration = configuration; traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); - //Debugger.Launch(); + Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) @@ -70,29 +72,18 @@ private void CloseFileSink(TestRunFinishedEvent @event) private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - var environment = testThreadObjectContainer!.Resolve(); - var enabledResult = environment.GetEnvironmentVariable(CucumberConfiguration.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; - bool isMessagesEnabled = "true".Equals(enabled, StringComparison.InvariantCultureIgnoreCase); + ResolvedConfiguration config = _configuration.ResolveConfiguration(); - CucumberOutputConfiguration configurationDTO; - if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) + if (!config.Enabled) { - configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; - } - else - configurationDTO = new CucumberOutputConfiguration(isMessagesEnabled); - - if (!configurationDTO.FileOutputEnabled) - { - trace!.WriteTestOutput("FileOutputPlugin LaunchFileSink. Cucumber Messages is DISABLED."); + trace!.WriteTestOutput("Cucumber Messages is DISABLED."); + // By returning here, we don't launch the File writing thread, + // and this class is not registered as a CucumberMessageSink, which indicates to the Broker that Messages are disabled. return; } + baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); - var configuration = new CucumberConfiguration(trace, environment); - baseDirectory = configuration.ConfigureOutputDirectory(configurationDTO); - trace!.WriteTestOutput($"FileOutputPlugin LaunchFileSink. Cucumber Messages is ENABLED. Base Directory: {baseDirectory}"); - trace?.WriteTestOutput("FileOutputPlugin Starting File Sink long running thread."); + trace?.WriteToolOutput("Cuccumber Messages: Starting File Sink long running thread."); fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index 21f481c22..b9c8fbae6 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -18,6 +18,7 @@ using Reqnroll.PlatformCompatibility; using Reqnroll.CucumberMessages; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.CucumberMessages.Configuration; namespace Reqnroll.Infrastructure { @@ -103,6 +104,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); //Support for publishing Cucumber Messages + container.RegisterTypeAs(); container.RegisterTypeAs("FileOutputPlugin"); container.RegisterTypeAs(); container.RegisterTypeAs("CucumberMessagePublisher"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index f97f78f78..19a268395 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,6 +1,6 @@ using FluentAssertions; using Moq; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; +using Reqnroll.CucumberMessages.Configuration; using Reqnroll.EnvironmentAccess; using Reqnroll.SystemTests; using Reqnroll.Tracing; @@ -23,8 +23,8 @@ protected override void TestCleanup() protected void EnableCucumberMessages() { - Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "true"); - Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE", "LOCAL"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE, "true"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE, "LOCAL"); } protected void DisableCucumberMessages() @@ -69,14 +69,15 @@ protected void CucumberMessagesAddConfigurationFile(string configFileName) protected static string ActualsResultLocationDirectory() { - var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); + //var configFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CucumberMessages.configuration.json"); - var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + //var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var tracerMock = new Mock(); var env = new EnvironmentWrapper(); CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); - var resultLocation = configuration.ConfigureOutputDirectory(config); + var resolvedconfiguration = configuration.ResolveConfiguration(); + var resultLocation = Path.Combine(resolvedconfiguration.BaseDirectory, resolvedconfiguration.OutputDirectory); return resultLocation; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 22b0b03c8..a9d0152ee 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -1,6 +1,7 @@ { "fileOutputEnabled": true, - "destinations": [ + "activeProfileName": "LOCAL", + "profiles": [ { "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", From a28a08d65b9da0af7c0a0586ab5d82feeca60be2 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:45:32 -0500 Subject: [PATCH 167/218] Add Explicit Text.Json assembly dependencies to MSBuild.Generation In order to avoid assembly version conflicts, adding explicit assembly dependencies to the MSBuild.Generation nuspec. --- .../Reqnroll.Tools.MsBuild.Generation.nuspec | 8 +++++++- Reqnroll/CucumberMessages/FileOutputPlugin.cs | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec index 263b360ab..00f3987df 100644 --- a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec +++ b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec @@ -29,7 +29,13 @@ exclude="bin\$config$\netstandard2.0\System.*;bin\$config$\netstandard2.0\Microsoft.*" target="tasks\$Reqnroll_Core_Tools_TFM$" /> - + + + + + + + diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/FileOutputPlugin.cs index b2711813f..083f55d33 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/FileOutputPlugin.cs @@ -44,7 +44,6 @@ public FileOutputPlugin(ICucumberConfiguration configuration) { _configuration = configuration; traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); - Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) From 8c0e8e7df279b98b5eca094fc5c20f864348754c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:36:47 -0500 Subject: [PATCH 168/218] Improved PickleJar's handling of edge-case in which we have no Pickles. Will allow code gen and execution to continue without support for Messages. --- .../Generation/UnitTestFeatureGenerator.cs | 12 +++---- Reqnroll/CucumberMessages/PickleJar.cs | 31 ++++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 4b13a7f2c..b4c6c7edb 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -18,11 +18,11 @@ namespace Reqnroll.Generator.Generation { public class UnitTestFeatureGenerator : IFeatureGenerator { - private const string PICKLES = "Pickles"; private const string PICKLEJAR = "PICKLEJAR"; private readonly CodeDomHelper _codeDomHelper; private readonly IDecoratorRegistry _decoratorRegistry; + private readonly ITraceListener _traceListener; private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _testGeneratorProvider; @@ -33,12 +33,14 @@ public UnitTestFeatureGenerator( IUnitTestGeneratorProvider testGeneratorProvider, CodeDomHelper codeDomHelper, ReqnrollConfiguration reqnrollConfiguration, - IDecoratorRegistry decoratorRegistry) + IDecoratorRegistry decoratorRegistry, + ITraceListener traceListener) { _testGeneratorProvider = testGeneratorProvider; _codeDomHelper = codeDomHelper; _reqnrollConfiguration = reqnrollConfiguration; _decoratorRegistry = decoratorRegistry; + _traceListener = traceListener; _linePragmaHandler = new LinePragmaHandler(_reqnrollConfiguration, _codeDomHelper); _scenarioPartHelper = new ScenarioPartHelper(_reqnrollConfiguration, _codeDomHelper); _unitTestMethodGenerator = new UnitTestMethodGenerator(testGeneratorProvider, decoratorRegistry, _codeDomHelper, _scenarioPartHelper, _reqnrollConfiguration); @@ -240,14 +242,12 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); // Save the Pickles to the GenerationContext so that the Pickle and Step Ids can be injected as arguments into the Scenario and Step method signatures - //TODO: Confirm whether the Pickles are nessessary as the PickleJar already includes them - generationContext.CustomData.Add(PICKLES, featurePickleMessages); generationContext.CustomData.Add(PICKLEJAR, new PickleJar(featurePickleMessages)); } - catch + catch(Exception e) { + _traceListener.WriteToolOutput($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. - generationContext.CustomData.Add(PICKLES, null); generationContext.CustomData.Add(PICKLEJAR, new PickleJar(new List())); return; } diff --git a/Reqnroll/CucumberMessages/PickleJar.cs b/Reqnroll/CucumberMessages/PickleJar.cs index e6f230cd9..af5308d5b 100644 --- a/Reqnroll/CucumberMessages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/PickleJar.cs @@ -11,6 +11,8 @@ public class PickleJar public const string PICKLEJAR_VARIABLE_NAME = "m_pickleJar"; public int _PickleCounter = 0; + + public bool HasPickles { get; } public IEnumerable Pickles { get; set; } //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) @@ -22,18 +24,22 @@ public PickleJar(IEnumerable pickles, int { Pickles = pickles; _PickleCounter = pickleCounter; + HasPickles = pickles != null && pickles.Count() > 0; } - public string CurrentPickleId { get { return Pickles.ElementAt(_PickleCounter).Id; } } - public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } - - public IEnumerable PickleStepIdsFor(string pickleId) + public string CurrentPickleId { - return Pickles.Where(p => p.Id == pickleId).SelectMany(p => p.Steps.Select(s => s.Id)).ToArray(); + get + { + if (!HasPickles) return null; + return Pickles.ElementAt(_PickleCounter).Id; + } } + public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } + public PickleStepSequence PickleStepSequenceFor(string pickleId) { - return new PickleStepSequence(Pickles.Where(p => p.Id == pickleId).First()); + return new PickleStepSequence(HasPickles, Pickles.Where(p => p.Id == pickleId).First()); } public void NextPickle() @@ -45,12 +51,14 @@ public void NextPickle() public class PickleStepSequence { + public bool HasPickles { get; } public Pickle CurrentPickle { get; } private int _PickleStepCounter; - public PickleStepSequence(Gherkin.CucumberMessages.Types.Pickle pickle) + public PickleStepSequence(bool hasPickles, Gherkin.CucumberMessages.Types.Pickle pickle) { + HasPickles = hasPickles; CurrentPickle = pickle; _PickleStepCounter = 0; } @@ -58,7 +66,14 @@ public void NextStep() { _PickleStepCounter++; } - public string CurrentPickleStepId { get { return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; } } + public string CurrentPickleStepId + { + get + { + if (!HasPickles) return null; + return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; + } + } } } From 25bcc59486256cea4eddf525d0efb6a6c16c57eb Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:27:02 -0500 Subject: [PATCH 169/218] Fix GeneratorTests to match the new constructor signature of the UnitFeatureGenerator. --- .../CustomTestGeneratorProviderTest.cs | 4 +++- .../Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs | 3 ++- .../IUnitTestGeneratorProviderExtensions.cs | 3 ++- Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs | 3 ++- Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs | 3 ++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs index dc61a6544..a984dc039 100644 --- a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs +++ b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs @@ -14,6 +14,8 @@ using Reqnroll.Generator.UnitTestProvider; using Reqnroll.Parser; using Reqnroll.Utils; +using System.Diagnostics; +using Reqnroll.Tracing; namespace Reqnroll.GeneratorTests { @@ -54,7 +56,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(IUnitTestGenerato runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); } /// diff --git a/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs index 4d1c199e7..3d37a9e3b 100644 --- a/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs @@ -6,6 +6,7 @@ using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.Generation; using Reqnroll.Generator.UnitTestProvider; +using Reqnroll.Tracing; namespace Reqnroll.GeneratorTests { @@ -17,7 +18,7 @@ private static UnitTestFeatureGeneratorProvider CreateUnitTestFeatureGeneratorPr Configuration.ReqnrollConfiguration generatorReqnrollConfiguration = ConfigurationLoader.GetDefault(); CodeDomHelper codeDomHelper = new CodeDomHelper(CodeDomProviderLanguage.CSharp); UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator( - new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub()); + new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener()); return new UnitTestFeatureGeneratorProvider(unitTestFeatureGenerator); } diff --git a/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs b/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs index 1ed9843cb..75c3e5cc1 100644 --- a/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs +++ b/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs @@ -7,6 +7,7 @@ using Reqnroll.Generator.Interfaces; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; +using Reqnroll.Tracing; using Reqnroll.Utils; namespace Reqnroll.GeneratorTests @@ -21,7 +22,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(this IUnitTestGen runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); } public static IFeatureGenerator CreateFeatureGenerator(this IUnitTestGeneratorProvider testGeneratorProvider, string[] addNonParallelizableMarkerForTags = null) diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs index 5b87d2327..2a1b542fe 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs @@ -11,6 +11,7 @@ using Reqnroll.Generator.Generation; using Reqnroll.Generator.UnitTestProvider; using Reqnroll.Parser; +using Reqnroll.Tracing; namespace Reqnroll.GeneratorTests { @@ -51,7 +52,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(IUnitTestGenerato runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); } /// diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs index 2d4babbe1..4ea012ef8 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs @@ -9,6 +9,7 @@ using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; using Reqnroll.Parser; +using Reqnroll.Tracing; namespace Reqnroll.GeneratorTests { @@ -84,7 +85,7 @@ protected TestGenerator CreateTestGenerator(ProjectSettings projectSettings) { Configuration.ReqnrollConfiguration generatorReqnrollConfiguration = ConfigurationLoader.GetDefault(); CodeDomHelper codeDomHelper = new CodeDomHelper(CodeDomProviderLanguage.CSharp); - UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator(new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub()); + UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator(new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener()); var gherkinParserFactory = new ReqnrollGherkinParserFactory(); From c7eda77922572cc2a05af868d538a966a750e55d Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:35:18 -0500 Subject: [PATCH 170/218] Refactored namespaces and put classes into Solution Folders. Reworked FileSink so that it is single-threaded, synchronous, but background task in operation. (Overall, simpler). Modified output to always go to a single file (instead of one per Feature). Configurable. --- .../Generation/ScenarioPartHelper.cs | 2 +- .../Generation/UnitTestFeatureGenerator.cs | 1 + .../Generation/UnitTestMethodGenerator.cs | 2 +- .../Configuration/CucumberConfiguration.cs | 41 ++++++++-- .../CucumberMessageFactory.cs | 34 ++++---- .../{ => ExecutionTracking}/FeatureTracker.cs | 8 +- .../HookStepTracker.cs} | 10 +-- .../StepExecutionTrackerBase.cs} | 6 +- .../TestCaseCucumberMessageTracker.cs | 35 ++++---- .../TestStepTracker.cs} | 14 ++-- .../{IStepProcessor.cs => IStepTracker.cs} | 2 +- .../CucumberMessagEnumConverter.cs | 2 +- .../CucumberMessageExtensions.cs | 0 .../CucumberMessageTransformer.cs | 30 +++---- .../CucumberMessageVisitor.cs | 2 +- .../CucumberMessage_TraversalVisitorBase.cs | 4 +- .../FileExtensionToMIMETypeMap.cs | 2 +- .../ICucumberMessageVisitor.cs | 4 +- .../NdjsonSerializer.cs | 14 ++-- .../{ => PubSub}/CucumberMessageBroker.cs | 10 +-- .../{ => PubSub}/CucumberMessagePublisher.cs | 3 +- .../{ => PubSub}/FileOutputPlugin.cs | 82 +++++++++---------- .../{ => PubSub}/ICucumberMessageSink.cs | 2 +- .../{ => PubSub}/ReqnrollCucumberMessage.cs | 2 +- .../AttachmentAddedEventWrapper.cs | 2 +- .../FeatureLevelCucumberMessages.cs | 9 +- .../IdGeneratorFactory.cs | 2 +- .../OutputAddedEventWrapper.cs | 2 +- .../{ => RuntimeSupport}/PickleJar.cs | 14 ++-- Reqnroll/FeatureInfo.cs | 2 +- .../DefaultDependencyProvider.cs | 2 +- Reqnroll/ScenarioInfo.cs | 2 +- .../CCK/examples-tables/examples_tables.cs | 2 +- .../CrossReferenceBuilder.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 33 ++++++++ .../CucumberCompatibilityTests.cs | 34 ++------ .../CucumberMessagesValidator.cs | 1 + 37 files changed, 233 insertions(+), 186 deletions(-) rename Reqnroll/CucumberMessages/{ => ExecutionTracking}/CucumberMessageFactory.cs (92%) rename Reqnroll/CucumberMessages/{ => ExecutionTracking}/FeatureTracker.cs (93%) rename Reqnroll/CucumberMessages/{HookStepProcessor.cs => ExecutionTracking/HookStepTracker.cs} (76%) rename Reqnroll/CucumberMessages/{StepProcessorBase.cs => ExecutionTracking/StepExecutionTrackerBase.cs} (69%) rename Reqnroll/CucumberMessages/{ => ExecutionTracking}/TestCaseCucumberMessageTracker.cs (92%) rename Reqnroll/CucumberMessages/{TestStepProcessor.cs => ExecutionTracking/TestStepTracker.cs} (86%) rename Reqnroll/CucumberMessages/{IStepProcessor.cs => IStepTracker.cs} (74%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/CucumberMessagEnumConverter.cs (96%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/CucumberMessageExtensions.cs (100%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/CucumberMessageTransformer.cs (88%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/CucumberMessageVisitor.cs (99%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/CucumberMessage_TraversalVisitorBase.cs (99%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/FileExtensionToMIMETypeMap.cs (98%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/ICucumberMessageVisitor.cs (96%) rename Reqnroll/CucumberMessages/{ => PayloadProcessing}/NdjsonSerializer.cs (88%) rename Reqnroll/CucumberMessages/{ => PubSub}/CucumberMessageBroker.cs (93%) rename Reqnroll/CucumberMessages/{ => PubSub}/CucumberMessagePublisher.cs (99%) rename Reqnroll/CucumberMessages/{ => PubSub}/FileOutputPlugin.cs (69%) rename Reqnroll/CucumberMessages/{ => PubSub}/ICucumberMessageSink.cs (77%) rename Reqnroll/CucumberMessages/{ => PubSub}/ReqnrollCucumberMessage.cs (82%) rename Reqnroll/CucumberMessages/{ => RuntimeSupport}/AttachmentAddedEventWrapper.cs (91%) rename Reqnroll/CucumberMessages/{ => RuntimeSupport}/FeatureLevelCucumberMessages.cs (73%) rename Reqnroll/CucumberMessages/{ => RuntimeSupport}/IdGeneratorFactory.cs (95%) rename Reqnroll/CucumberMessages/{ => RuntimeSupport}/OutputAddedEventWrapper.cs (90%) rename Reqnroll/CucumberMessages/{ => RuntimeSupport}/PickleJar.cs (73%) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 63d7dd7ab..ccf7608dd 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -4,7 +4,7 @@ using System.Text.RegularExpressions; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Generator.CodeDom; using Reqnroll.Parser; diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index b4c6c7edb..c69ef7cc6 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -8,6 +8,7 @@ using Gherkin.CucumberMessages; using Reqnroll.Configuration; using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 0c7b2170d..c459d3ba6 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using Gherkin.Ast; using Reqnroll.Configuration; -using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 18af77f5d..c0265a30a 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -2,6 +2,7 @@ using Reqnroll.EnvironmentAccess; using Reqnroll.Tracing; using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -25,16 +26,26 @@ public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper e _trace = traceListener; _environmentWrapper = environmentWrapper; } + #region Override API public void SetEnabled(bool value) { _enablementOverrideFlag = value; } #endregion + + public ResolvedConfiguration ResolveConfiguration() { var config = ApplyHierarchicalConfiguration(); var resolved = ApplyEnvironmentOverrides(config); + + // a final sanity check, the filename cannot be empty + if (string.IsNullOrEmpty(resolved.OutputFileName)) + { + resolved.OutputFileName = "reqnroll_report.ndjson"; + _trace!.WriteToolOutput($"WARNING: Cucumber Messages: Output filename was empty. Setting filename to {resolved.OutputFileName}"); + } EnsureOutputDirectory(resolved); string logEntry; @@ -92,23 +103,39 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) return result; } - private ConfigurationDTO AddConfig(ConfigurationDTO config, ConfigurationDTO overridingConfig) + private ConfigurationDTO AddConfig(ConfigurationDTO rootConfig, ConfigurationDTO overridingConfig) { if (overridingConfig != null) { - config.Profiles.AddRange(overridingConfig.Profiles); - if (overridingConfig.ActiveProfileName != null && !config.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) + foreach (var overridingProfile in overridingConfig.Profiles) + { + AddOrOverrideProfile(rootConfig.Profiles, overridingProfile); + } + if (overridingConfig.ActiveProfileName != null && !rootConfig.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) { // The incoming configuration DTO points to a profile that doesn't exist. - _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {config.ActiveProfileName} instead."); + _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {rootConfig.ActiveProfileName} instead."); } else if (overridingConfig.ActiveProfileName != null) - config.ActiveProfileName = overridingConfig.ActiveProfileName; + rootConfig.ActiveProfileName = overridingConfig.ActiveProfileName; - config.FileOutputEnabled = overridingConfig.FileOutputEnabled; + rootConfig.FileOutputEnabled = overridingConfig.FileOutputEnabled; } - return config; + return rootConfig; + } + + private void AddOrOverrideProfile(List masterList, Profile overridingProfile) + { + if (masterList.Any(p => p.ProfileName == overridingProfile.ProfileName)) + { + var existingProfile = masterList.Where(p => p.ProfileName == overridingProfile.ProfileName).FirstOrDefault(); + + existingProfile.BasePath = overridingProfile.BasePath; + existingProfile.OutputDirectory = overridingProfile.OutputDirectory; + existingProfile.OutputFileName = overridingProfile.OutputFileName; + } + else masterList.Add(overridingProfile); } private void EnsureOutputDirectory(ResolvedConfiguration config) diff --git a/Reqnroll/CucumberMessages/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs similarity index 92% rename from Reqnroll/CucumberMessages/CucumberMessageFactory.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs index b0a1a33ec..de628b6a1 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs @@ -4,6 +4,8 @@ using Reqnroll.Analytics; using Reqnroll.Bindings; using Reqnroll.CommonModels; +using Reqnroll.CucumberMessages.PayloadProcessing; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.EnvironmentAccess; using Reqnroll.Events; using System; @@ -15,7 +17,7 @@ using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { internal class CucumberMessageFactory { @@ -36,12 +38,12 @@ internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTrack { switch (stepState) { - case TestStepProcessor _: - var testStep = CucumberMessageFactory.ToPickleTestStep(testCaseTracker, stepState as TestStepProcessor); + case TestStepTracker _: + var testStep = ToPickleTestStep(testCaseTracker, stepState as TestStepTracker); testSteps.Add(testStep); break; - case HookStepProcessor _: - var hookTestStep = CucumberMessageFactory.ToHookTestStep(stepState as HookStepProcessor); + case HookStepTracker _: + var hookTestStep = ToHookTestStep(stepState as HookStepTracker); testSteps.Add(hookTestStep); break; default: @@ -120,13 +122,13 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToPickleTestStep(TestCaseCucumberMessageTracker tracker, TestStepProcessor stepState) + internal static TestStep ToPickleTestStep(TestCaseCucumberMessageTracker tracker, TestStepTracker stepState) { bool bound = stepState.Bound; bool ambiguous = stepState.Ambiguous; var args = stepState.StepArguments - .Select(arg => CucumberMessageFactory.ToStepMatchArgument(arg)) + .Select(arg => ToStepMatchArgument(arg)) .ToList(); var result = new TestStep( @@ -150,7 +152,7 @@ internal static StepMatchArgument ToStepMatchArgument(StepArgument argument) ), NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); } - internal static TestStepStarted ToTestStepStarted(TestStepProcessor stepState, StepStartedEvent stepStartedEvent) + internal static TestStepStarted ToTestStepStarted(TestStepTracker stepState, StepStartedEvent stepStartedEvent) { return new TestStepStarted( stepState.TestCaseStartedID, @@ -158,7 +160,7 @@ internal static TestStepStarted ToTestStepStarted(TestStepProcessor stepState, S Converters.ToTimestamp(stepStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(TestStepProcessor stepState, StepFinishedEvent stepFinishedEvent) + internal static TestStepFinished ToTestStepFinished(TestStepTracker stepState, StepFinishedEvent stepFinishedEvent) { return new TestStepFinished( stepState.TestCaseStartedID, @@ -181,7 +183,7 @@ internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) return result; } - internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) + internal static TestStep ToHookTestStep(HookStepTracker hookStepState) { // find the Hook message at the Feature level var hookCacheKey = CanonicalizeHookBinding(hookStepState.HookBindingFinishedEvent.HookBinding); @@ -194,12 +196,12 @@ internal static TestStep ToHookTestStep(HookStepProcessor hookStepState) null, null); } - internal static TestStepStarted ToTestStepStarted(HookStepProcessor hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) + internal static TestStepStarted ToTestStepStarted(HookStepTracker hookStepProcessor, HookBindingStartedEvent hookBindingStartedEvent) { return new TestStepStarted(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, Converters.ToTimestamp(hookBindingStartedEvent.Timestamp)); } - internal static TestStepFinished ToTestStepFinished(HookStepProcessor hookStepProcessor, HookBindingFinishedEvent hookFinishedEvent) + internal static TestStepFinished ToTestStepFinished(HookStepTracker hookStepProcessor, HookBindingFinishedEvent hookFinishedEvent) { return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } @@ -229,7 +231,7 @@ internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, null); } - private static TestStepResult ToTestStepResult(StepProcessorBase stepState) + private static TestStepResult ToTestStepResult(StepExecutionTrackerBase stepState) { return new TestStepResult( Converters.ToDuration(stepState.Duration), @@ -291,7 +293,7 @@ public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) var ci = ToCi(ci_name, environmentInfoProvider, environmentWrapper); return Envelope.Create(new Meta( - (Cucumber.Messages.ProtocolVersion.Version).Split('+')[0], + ProtocolVersion.Version.Split('+')[0], implementation, runTime, os, @@ -303,7 +305,7 @@ private static Ci ToCi(string ci_name, IEnvironmentInfoProvider environmentInfoP { //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. // Until then, these will be hard coded as null - if (String.IsNullOrEmpty(ci_name)) return null; + if (string.IsNullOrEmpty(ci_name)) return null; var git = ToGit(environmentWrapper); @@ -345,7 +347,7 @@ public static string CanonicalizeHookBinding(IHookBinding hookBinding) private static string GenerateSignature(IBinding stepDefinition) { - return stepDefinition.Method != null ? String.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + return stepDefinition.Method != null ? string.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; } public static string Base64EncodeFile(string filePath) { diff --git a/Reqnroll/CucumberMessages/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs similarity index 93% rename from Reqnroll/CucumberMessages/FeatureTracker.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 689e5715f..3d5cfbc04 100644 --- a/Reqnroll/CucumberMessages/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -1,12 +1,14 @@ using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { public class FeatureTracker { @@ -25,7 +27,7 @@ public FeatureTracker(FeatureStartedEvent featureStartedEvent) { FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; var featureHasCucumberMessages = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages != null; - Enabled = (featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null) ? true : false; + Enabled = featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null ? true : false; PreProcessEvent(featureStartedEvent); } internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) @@ -39,7 +41,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature yield return CucumberMessageFactory.ToMeta(featureStartedEvent); Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Io.Cucumber.Messages.Types.Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); yield return Envelope.Create(messageSource); diff --git a/Reqnroll/CucumberMessages/HookStepProcessor.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs similarity index 76% rename from Reqnroll/CucumberMessages/HookStepProcessor.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs index 80a7fc77d..7f70317e7 100644 --- a/Reqnroll/CucumberMessages/HookStepProcessor.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Linq; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { - public class HookStepProcessor : StepProcessorBase + public class HookStepTracker : StepExecutionTrackerBase { public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } - public HookStepProcessor(TestCaseCucumberMessageTracker tracker) : base(tracker) + public HookStepTracker(TestCaseCucumberMessageTracker tracker) : base(tracker) { } @@ -21,11 +21,11 @@ public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) public void ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) { - HookBindingFinishedEvent = hookFinishedEvent; + HookBindingFinishedEvent = hookFinishedEvent; Exception = hookFinishedEvent.HookException; Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; } } - + } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/StepProcessorBase.cs b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs similarity index 69% rename from Reqnroll/CucumberMessages/StepProcessorBase.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs index c1d3ec601..986c4568c 100644 --- a/Reqnroll/CucumberMessages/StepProcessorBase.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs @@ -1,8 +1,8 @@ using System; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { - public class StepProcessorBase : IStepProcessor + public class StepExecutionTrackerBase : IStepTracker { public string TestStepID { get; set; } public string TestCaseStartedID => ParentTestCase.TestCaseStartedId; @@ -12,7 +12,7 @@ public class StepProcessorBase : IStepProcessor public TestCaseCucumberMessageTracker ParentTestCase; - public StepProcessorBase(TestCaseCucumberMessageTracker parentScenario) + public StepExecutionTrackerBase(TestCaseCucumberMessageTracker parentScenario) { ParentTestCase = parentScenario; } diff --git a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs similarity index 92% rename from Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs index ec9da8b6c..755fc8895 100644 --- a/Reqnroll/CucumberMessages/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs @@ -1,6 +1,7 @@ using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System; using System.Collections.Concurrent; @@ -8,7 +9,7 @@ using System.Linq; using System.Net.NetworkInformation; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { /// /// This class is used to track the execution of Test Cases @@ -26,7 +27,7 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // Feature FeatureInfo and Pickle ID make up a unique identifier for tracking execution of Test Cases public string FeatureName { get; set; } - public string PickleId { get; set; } = String.Empty; + public string PickleId { get; set; } = string.Empty; public string TestCaseTrackerId { get { return FeatureName + PickleId; } } public string TestCaseId { get; set; } public string TestCaseStartedId { get; private set; } @@ -34,7 +35,7 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // When this class is first created (on FeatureStarted), it will not yet be assigned a Scenario/Pickle; // When a Scenario is started, the Publisher will assign the Scenario to the first UnAssigned TestCaseCucumberMessageTracker it finds // This property will indicate that state - public bool IsUnassigned { get { return PickleId == String.Empty; } } + public bool IsUnassigned { get { return PickleId == string.Empty; } } public bool Enabled { get; set; } //This will be false if the feature could not be pickled @@ -46,9 +47,9 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } - private Dictionary StepsById { get; set; } = new(); - private Dictionary StepsByEvent { get; set; } = new(); - public List Steps + private Dictionary StepsById { get; set; } = new(); + private Dictionary StepsByEvent { get; set; } = new(); + public List Steps { get { @@ -153,7 +154,7 @@ internal IEnumerable PostProcessEvent(FeatureStartedEvent featureStart return Enumerable.Empty(); } - internal void PreProcessEvent(FeatureFinishedEvent featureFinishedEvent) + internal void PreProcessEvent(FeatureFinishedEvent featureFinishedEvent) { } internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFinishedEvent) @@ -188,7 +189,7 @@ internal IEnumerable PostProcessEvent(ScenarioFinishedEvent scenarioFi internal void PreProcessEvent(StepStartedEvent stepStartedEvent) { - var stepState = new TestStepProcessor(this); + var stepState = new TestStepTracker(this); stepState.ProcessEvent(stepStartedEvent); StepsById.Add(stepState.PickleStepID, stepState); @@ -197,20 +198,20 @@ internal void PreProcessEvent(StepStartedEvent stepStartedEvent) internal IEnumerable PostProcessEvent(StepStartedEvent stepStartedEvent) { var stepState = StepsById[stepStartedEvent.StepContext.StepInfo.PickleStepId]; - var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as TestStepProcessor, stepStartedEvent); + var stepStarted = CucumberMessageFactory.ToTestStepStarted(stepState as TestStepTracker, stepStartedEvent); mostRecentTestStepStarted = stepStarted; yield return Envelope.Create(stepStarted); } internal void PreProcessEvent(StepFinishedEvent stepFinishedEvent) { - var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepTracker; stepState.ProcessEvent(stepFinishedEvent); StepsByEvent.Add(stepFinishedEvent, stepState); } internal IEnumerable PostProcessEvent(StepFinishedEvent stepFinishedEvent) { - var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepProcessor; - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepState as TestStepProcessor, stepFinishedEvent)); + var stepState = StepsById[stepFinishedEvent.StepContext.StepInfo.PickleStepId] as TestStepTracker; + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(stepState as TestStepTracker, stepFinishedEvent)); } internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) @@ -219,7 +220,7 @@ internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) return; - var step = new HookStepProcessor(this); + var step = new HookStepTracker(this); step.ProcessEvent(hookBindingStartedEvent); StepsByEvent.Add(hookBindingStartedEvent, step); @@ -227,7 +228,7 @@ internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) internal IEnumerable PostProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { var hookStepStartState = StepsByEvent[hookBindingStartedEvent]; - var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepProcessor, hookBindingStartedEvent); + var hookStepStarted = CucumberMessageFactory.ToTestStepStarted(hookStepStartState as HookStepTracker, hookBindingStartedEvent); mostRecentTestStepStarted = hookStepStarted; yield return Envelope.Create(hookStepStarted); } @@ -247,7 +248,7 @@ internal IEnumerable PostProcessEvent(HookBindingFinishedEvent hookBin { var hookStepProcessor = FindMatchingHookStartedEvent(hookBindingFinishedEvent); - yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepProcessor, hookBindingFinishedEvent)); + yield return Envelope.Create(CucumberMessageFactory.ToTestStepFinished(hookStepProcessor as HookStepTracker, hookBindingFinishedEvent)); } internal void PreProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { @@ -283,9 +284,9 @@ internal IEnumerable PostProcessEvent(OutputAddedEventWrapper outputAd outputAddedEventWrapper.TestCaseStartedID = mostRecentTestStepStarted.TestCaseStartedId; yield return Envelope.Create(CucumberMessageFactory.ToAttachment(this, outputAddedEventWrapper)); } - private HookStepProcessor FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + private HookStepTracker FindMatchingHookStartedEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepProcessor).LastOrDefault(); + return StepsByEvent.Where(kvp => kvp.Key is HookBindingStartedEvent && ((HookBindingStartedEvent)kvp.Key).HookBinding == hookBindingFinishedEvent.HookBinding).Select(kvp => kvp.Value as HookStepTracker).LastOrDefault(); } } diff --git a/Reqnroll/CucumberMessages/TestStepProcessor.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs similarity index 86% rename from Reqnroll/CucumberMessages/TestStepProcessor.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs index dce2ad41c..ff1669ee6 100644 --- a/Reqnroll/CucumberMessages/TestStepProcessor.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Threading; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { public class StepArgument { @@ -15,11 +15,11 @@ public class StepArgument public string Type; } - public class TestStepProcessor : StepProcessorBase + public class TestStepTracker : StepExecutionTrackerBase { private StepStartedEvent stepStartedEvent; - public TestStepProcessor(TestCaseCucumberMessageTracker parentTracker) : base(parentTracker) + public TestStepTracker(TestCaseCucumberMessageTracker parentTracker) : base(parentTracker) { } @@ -28,7 +28,7 @@ public TestStepProcessor(TestCaseCucumberMessageTracker parentTracker) : base(pa public string CanonicalizedStepPattern { get; set; } public string StepDefinitionId { get; private set; } public IEnumerable AmbiguousStepDefinitions { get; set; } - public bool Ambiguous { get { return AmbiguousStepDefinitions != null && AmbiguousStepDefinitions.Count() > 0;} } + public bool Ambiguous { get { return AmbiguousStepDefinitions != null && AmbiguousStepDefinitions.Count() > 0; } } public IStepDefinitionBinding StepDefinition { get; set; } public List StepArguments { get; set; } @@ -62,7 +62,7 @@ internal void ProcessEvent(StepFinishedEvent stepFinishedEvent) Exception = stepFinishedEvent.ScenarioContext.TestError; if (Exception is AmbiguousBindingException) { - AmbiguousStepDefinitions = new List(((AmbiguousBindingException)Exception).Matches.Select(m => + AmbiguousStepDefinitions = new List(((AmbiguousBindingException)Exception).Matches.Select(m => FindStepDefIDByStepPattern(CucumberMessageFactory.CanonicalizeStepDefinitionPattern(m.StepBinding)))); } } @@ -71,8 +71,8 @@ internal void ProcessEvent(StepFinishedEvent stepFinishedEvent) var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.ToString()).ToList() : new List(); var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); StepArguments = Bound && !IsInputDataTableOrDocString ? - argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() - : Enumerable.Empty().ToList(); + argumentValues.Zip(argumentTypes, (x, y) => new StepArgument { Value = x, Type = y }).ToList() + : Enumerable.Empty().ToList(); } } diff --git a/Reqnroll/CucumberMessages/IStepProcessor.cs b/Reqnroll/CucumberMessages/IStepTracker.cs similarity index 74% rename from Reqnroll/CucumberMessages/IStepProcessor.cs rename to Reqnroll/CucumberMessages/IStepTracker.cs index 02ba5ed1b..caf8eb4a0 100644 --- a/Reqnroll/CucumberMessages/IStepProcessor.cs +++ b/Reqnroll/CucumberMessages/IStepTracker.cs @@ -1,6 +1,6 @@ namespace Reqnroll.CucumberMessages { - public interface IStepProcessor + public interface IStepTracker { public string TestStepID { get; set; } diff --git a/Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs similarity index 96% rename from Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs index 3722e8c4e..6f5beaf06 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagEnumConverter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadProcessing { internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum { diff --git a/Reqnroll/CucumberMessages/CucumberMessageExtensions.cs b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageExtensions.cs similarity index 100% rename from Reqnroll/CucumberMessages/CucumberMessageExtensions.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageExtensions.cs diff --git a/Reqnroll/CucumberMessages/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs similarity index 88% rename from Reqnroll/CucumberMessages/CucumberMessageTransformer.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs index c4e019639..f4f630479 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadProcessing { /// /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace @@ -22,7 +22,7 @@ internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessa ( gherkinSource.Uri, gherkinSource.Data, - gherkinSource.MediaType == "text/x.cucumber.gherkin+plain" ? Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN : Io.Cucumber.Messages.Types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN + gherkinSource.MediaType == "text/x.cucumber.gherkin+plain" ? SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN : SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN ); return result; } @@ -32,8 +32,8 @@ internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(Ghe var result = new Io.Cucumber.Messages.Types.GherkinDocument ( gherkinDoc.Uri, - CucumberMessageTransformer.ToFeature(gherkinDoc.Feature), - CucumberMessageTransformer.ToComments(gherkinDoc.Comments) + ToFeature(gherkinDoc.Feature), + ToComments(gherkinDoc.Comments) ); return result; } @@ -217,17 +217,17 @@ private static Io.Cucumber.Messages.Types.Comment ToComment(Gherkin.CucumberMess comment.Text ); } - private static Io.Cucumber.Messages.Types.StepKeywordType ToKeyWordType(Gherkin.StepKeywordType keywordType) + private static StepKeywordType ToKeyWordType(Gherkin.StepKeywordType keywordType) { return keywordType switch { //case Gherkin.StepKeywordType.Unspecified: // return Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN; - Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.StepKeywordType.CONTEXT, - Gherkin.StepKeywordType.Conjunction => Io.Cucumber.Messages.Types.StepKeywordType.CONJUNCTION, - Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.StepKeywordType.ACTION, - Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.StepKeywordType.OUTCOME, - Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN, + Gherkin.StepKeywordType.Context => StepKeywordType.CONTEXT, + Gherkin.StepKeywordType.Conjunction => StepKeywordType.CONJUNCTION, + Gherkin.StepKeywordType.Action => StepKeywordType.ACTION, + Gherkin.StepKeywordType.Outcome => StepKeywordType.OUTCOME, + Gherkin.StepKeywordType.Unknown => StepKeywordType.UNKNOWN, _ => throw new ArgumentException($"Invalid keyword type: {keywordType}"), }; } @@ -296,14 +296,14 @@ private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgumen ); } - private static Io.Cucumber.Messages.Types.PickleStepType ToPickleStepType(Gherkin.StepKeywordType pickleStepType) + private static PickleStepType ToPickleStepType(Gherkin.StepKeywordType pickleStepType) { return pickleStepType switch { - Gherkin.StepKeywordType.Unknown => Io.Cucumber.Messages.Types.PickleStepType.UNKNOWN, - Gherkin.StepKeywordType.Action => Io.Cucumber.Messages.Types.PickleStepType.ACTION, - Gherkin.StepKeywordType.Outcome => Io.Cucumber.Messages.Types.PickleStepType.OUTCOME, - Gherkin.StepKeywordType.Context => Io.Cucumber.Messages.Types.PickleStepType.CONTEXT, + Gherkin.StepKeywordType.Unknown => PickleStepType.UNKNOWN, + Gherkin.StepKeywordType.Action => PickleStepType.ACTION, + Gherkin.StepKeywordType.Outcome => PickleStepType.OUTCOME, + Gherkin.StepKeywordType.Context => PickleStepType.CONTEXT, _ => throw new ArgumentException($"Invalid pickle step type: {pickleStepType}") }; } diff --git a/Reqnroll/CucumberMessages/CucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs similarity index 99% rename from Reqnroll/CucumberMessages/CucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs index c8d1c9274..7a0eff6be 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs @@ -4,7 +4,7 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages; +namespace Reqnroll.CucumberMessages.PayloadPatching; public class CucumberMessageVisitor { diff --git a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs similarity index 99% rename from Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs index a337956af..24a8c4186 100644 --- a/Reqnroll/CucumberMessages/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadPatching { public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor { @@ -385,7 +385,7 @@ public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) { OnVisiting(dataTable); Accept(dataTable.Location); - foreach (var row in dataTable.Rows ?? new List()) + foreach (var row in dataTable.Rows ?? new List()) { Accept(row); } diff --git a/Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs similarity index 98% rename from Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs index 6a6cc6ae0..9a70e7689 100644 --- a/Reqnroll/CucumberMessages/FileExtensionToMIMETypeMap.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadProcessing { public static class FileExtensionToMIMETypeMap { diff --git a/Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs similarity index 96% rename from Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs index c0e1464c6..1fe636728 100644 --- a/Reqnroll/CucumberMessages/ICucumberMessageVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs @@ -1,6 +1,6 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages; +namespace Reqnroll.CucumberMessages.PayloadPatching; // This interface is used to support the implementation of an External Vistor pattern against the Cucumber Messages. // Visitors impmlement this interface and then invoke it using the helper class below. @@ -43,7 +43,7 @@ public interface ICucumberMessageVisitor void Visit(SourceReference sourceReference); void Visit(Duration duration); void Visit(Timestamp timestamp); - void Visit(Io.Cucumber.Messages.Types.Exception exception); + void Visit(Exception exception); void Visit(Meta meta); void Visit(Product product); void Visit(Ci ci); diff --git a/Reqnroll/CucumberMessages/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs similarity index 88% rename from Reqnroll/CucumberMessages/NdjsonSerializer.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index 17954295a..44e99073a 100644 --- a/Reqnroll/CucumberMessages/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -2,7 +2,7 @@ using System; using System.Text.Json; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadProcessing { /// /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. @@ -27,15 +27,17 @@ public class NdjsonSerializer return options; }); - private static JsonSerializerOptions JsonOptions { get - { + private static JsonSerializerOptions JsonOptions + { + get + { return _jsonOptions.Value; } - } + } public static string Serialize(Envelope message) { - return NdjsonSerializer.Serialize(message); + return Serialize(message); } internal static string Serialize(T message) @@ -45,7 +47,7 @@ internal static string Serialize(T message) public static Envelope Deserialize(string json) { - return NdjsonSerializer.Deserialize(json); + return Deserialize(json); } internal static T Deserialize(string json) diff --git a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs similarity index 93% rename from Reqnroll/CucumberMessages/CucumberMessageBroker.cs rename to Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index 5d25eff32..f07d4d308 100644 --- a/Reqnroll/CucumberMessages/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PubSub { public interface ICucumberMessageBroker @@ -25,10 +25,10 @@ public class CucumberMessageBroker : ICucumberMessageBroker public bool Enabled => RegisteredSinks.Value.ToList().Count > 0; - private Lazy> RegisteredSinks; + private Lazy> RegisteredSinks; public CucumberMessageBroker(IObjectContainer objectContainer) - { + { _objectContainer = objectContainer; RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); } @@ -37,7 +37,7 @@ public void Publish(ReqnrollCucumberMessage message) var _traceListener = _objectContainer.Resolve(); foreach (var sink in RegisteredSinks.Value) - { + { _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}: {message.Envelope.Content()}"); sink.Publish(message); @@ -45,7 +45,7 @@ public void Publish(ReqnrollCucumberMessage message) } // using an empty CucumberMessage to indicate completion - public void Complete(string cucumberMessageSource) + public void Complete(string cucumberMessageSource) { var _traceListener = _objectContainer.Resolve(); diff --git a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs similarity index 99% rename from Reqnroll/CucumberMessages/CucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 730b4083d..9e2356c26 100644 --- a/Reqnroll/CucumberMessages/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -8,8 +8,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Reqnroll.CucumberMessages.ExecutionTracking; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PubSub { public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { diff --git a/Reqnroll/CucumberMessages/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs similarity index 69% rename from Reqnroll/CucumberMessages/FileOutputPlugin.cs rename to Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 083f55d33..3833d5ac9 100644 --- a/Reqnroll/CucumberMessages/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -18,6 +18,9 @@ using Reqnroll.CommonModels; using System.Diagnostics; using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PubSub; +using Reqnroll.CucumberMessages.PayloadProcessing; +using System.Runtime.InteropServices.ComTypes; namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin @@ -25,14 +28,11 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private Task? fileWritingTask; - private object _lock = new(); //Thread safe collections to hold: // 1. Inbound Cucumber Messages - BlockingCollection - // 2. Dictionary of Feature Streams (Key: Feature Name, Value: StreamWriter) private readonly BlockingCollection postedMessages = new(); - private readonly ConcurrentDictionary fileStreams = new(); - private string baseDirectory = ""; + private ICucumberConfiguration _configuration; private Lazy traceListener; private ITraceListener? trace => traceListener.Value; @@ -58,11 +58,20 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); testThreadObjectContainer = args.ObjectContainer; testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); - testThreadExecutionEventPublisher.AddHandler(CloseFileSink); + testThreadExecutionEventPublisher.AddHandler(Close); }; } - private void CloseFileSink(TestRunFinishedEvent @event) + + private void Close(TestRunFinishedEvent @event) + { + // Dispose will call CloseFileSink and CloseStream. + // The former will shut down the message pipe and wait for the writer to complete. + // The latter will close down the file stream. + Dispose(true); + } + private void CloseFileSink() { + if (disposedValue) return; trace?.WriteTestOutput("FileOutputPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); @@ -80,22 +89,26 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) // and this class is not registered as a CucumberMessageSink, which indicates to the Broker that Messages are disabled. return; } - baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); + string baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); + string fileName = SanitizeFileName(config.OutputFileName); - trace?.WriteToolOutput("Cuccumber Messages: Starting File Sink long running thread."); - fileWritingTask = Task.Factory.StartNew(async () => await ConsumeAndWriteToFiles(), TaskCreationOptions.LongRunning); + trace?.WriteToolOutput($"Cuccumber Messages: Starting File Sink long running thread. Writing to: {baseDirectory}"); + fileWritingTask = Task.Factory.StartNew( () => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } public void Publish(ReqnrollCucumberMessage message) { - var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; + //var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; //trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } - private async Task ConsumeAndWriteToFiles() + private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) { + var fileStream = File.CreateText(Path.Combine(baseDirectory, fileName)); + + foreach (var message in postedMessages.GetConsumingEnumerable()) { var featureName = message.CucumberMessageSource; @@ -103,15 +116,12 @@ private async Task ConsumeAndWriteToFiles() if (message.Envelope != null) { var cm = Serialize(message.Envelope); - //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {message.CucumberMessageSource}: {cm.Substring(0, 20)}"); - await Write(featureName, cm); - } - else - { - //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. End of Messages Marker Received."); - CloseFeatureStream(featureName); + //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {featureName}: {cm.Substring(0, 20)}"); + Write(fileStream, cm); } } + + CloseStream(fileStream); } @@ -119,22 +129,12 @@ private string Serialize(Envelope message) { return NdjsonSerializer.Serialize(message); } - private async Task Write(string featureName, string cucumberMessage) + private void Write(StreamWriter fileStream, string cucumberMessage) { try { - if (!fileStreams.ContainsKey(featureName)) - { - lock (_lock) - { - if (!fileStreams.ContainsKey(featureName)) - { - fileStreams[featureName] = File.CreateText(Path.Combine(baseDirectory, SanitizeFileName($"{featureName}.ndjson"))); - } - } - } - trace?.WriteTestOutput($"FileOutputPlugin Write. Writing to: {SanitizeFileName($"{featureName}.ndjson")}. Cucumber Message: {featureName}: {cucumberMessage.Substring(0, 20)}"); - await fileStreams[featureName].WriteLineAsync(cucumberMessage); + trace?.WriteTestOutput($"FileOutputPlugin Write. Cucumber Message: {cucumberMessage.Substring(0, 20)}"); + fileStream!.WriteLine(cucumberMessage); } catch (System.Exception ex) { @@ -142,13 +142,14 @@ private async Task Write(string featureName, string cucumberMessage) } } - private void CloseFeatureStream(string featureName) + private void CloseStream(StreamWriter fileStream) { - trace?.WriteTestOutput($"FileOutputPlugin CloseFeatureStream. Closing: {featureName}."); - fileStreams[featureName].Close(); - fileStreams.TryRemove(featureName, out var _); + trace?.WriteTestOutput($"FileOutputPlugin Closing File Stream."); + fileStream?.Flush(); + fileStream?.Close(); + fileStream?.Dispose(); } - private bool disposedValue; + private bool disposedValue = false; protected virtual void Dispose(bool disposing) { @@ -156,16 +157,9 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - CloseFileSink(new TestRunFinishedEvent()); + CloseFileSink(); postedMessages.Dispose(); - foreach (var stream in fileStreams.Values) - { - stream.Close(); - stream.Dispose(); - }; - fileStreams.Clear(); } - disposedValue = true; } } diff --git a/Reqnroll/CucumberMessages/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs similarity index 77% rename from Reqnroll/CucumberMessages/ICucumberMessageSink.cs rename to Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs index 2c1219506..9a6987968 100644 --- a/Reqnroll/CucumberMessages/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PubSub { public interface ICucumberMessageSink { diff --git a/Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs similarity index 82% rename from Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs rename to Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs index 12c6a3784..6fc5412e8 100644 --- a/Reqnroll/CucumberMessages/ReqnrollCucumberMessage.cs +++ b/Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs @@ -1,7 +1,7 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PubSub { public class ReqnrollCucumberMessage { diff --git a/Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs similarity index 91% rename from Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs index 44f371407..827a56290 100644 --- a/Reqnroll/CucumberMessages/AttachmentAddedEventWrapper.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.RuntimeSupport { internal class AttachmentAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs similarity index 73% rename from Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs rename to Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 12fb81ac1..e275af1cb 100644 --- a/Reqnroll/CucumberMessages/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -1,9 +1,8 @@ -using Reqnroll.CucumberMessages; -using System; +using System; using System.Collections.Generic; using System.Text; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.RuntimeSupport { public class FeatureLevelCucumberMessages { @@ -15,8 +14,8 @@ public FeatureLevelCucumberMessages(string source, string gkerkinDocument, strin PickleJar = new PickleJar(pickles); } - public string Source { get; } - public string GherkinDocument { get; } + public string Source { get; } + public string GherkinDocument { get; } public string Pickles { get; } public PickleJar PickleJar { get; } } diff --git a/Reqnroll/CucumberMessages/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs similarity index 95% rename from Reqnroll/CucumberMessages/IdGeneratorFactory.cs rename to Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs index 80bfbf10d..ab98d0af9 100644 --- a/Reqnroll/CucumberMessages/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs @@ -1,7 +1,7 @@ using Gherkin.CucumberMessages; using System; using System.Threading; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.RuntimeSupport { public class IdGeneratorFactory { diff --git a/Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs similarity index 90% rename from Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs rename to Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs index 62e5491a5..ca23002d2 100644 --- a/Reqnroll/CucumberMessages/OutputAddedEventWrapper.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.RuntimeSupport { internal class OutputAddedEventWrapper : ExecutionEvent { diff --git a/Reqnroll/CucumberMessages/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs similarity index 73% rename from Reqnroll/CucumberMessages/PickleJar.cs rename to Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index af5308d5b..3cc736658 100644 --- a/Reqnroll/CucumberMessages/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.RuntimeSupport { public class PickleJar { @@ -13,14 +13,14 @@ public class PickleJar public int _PickleCounter = 0; public bool HasPickles { get; } - public IEnumerable Pickles { get; set; } + public IEnumerable Pickles { get; set; } //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) //{ } - public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } - public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } + public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } + public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } - public PickleJar(IEnumerable pickles, int pickleCounter, int pickleStepCounter) + public PickleJar(IEnumerable pickles, int pickleCounter, int pickleStepCounter) { Pickles = pickles; _PickleCounter = pickleCounter; @@ -35,7 +35,7 @@ public string CurrentPickleId return Pickles.ElementAt(_PickleCounter).Id; } } - public Gherkin.CucumberMessages.Types.Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } + public Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } public PickleStepSequence PickleStepSequenceFor(string pickleId) { @@ -56,7 +56,7 @@ public class PickleStepSequence private int _PickleStepCounter; - public PickleStepSequence(bool hasPickles, Gherkin.CucumberMessages.Types.Pickle pickle) + public PickleStepSequence(bool hasPickles, Pickle pickle) { HasPickles = hasPickles; CurrentPickle = pickle; diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index 42a157b3c..4641b6698 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,6 +1,6 @@ using System; using System.Globalization; -using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Tracing; namespace Reqnroll diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index b9c8fbae6..e2dca6b56 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,9 +16,9 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; -using Reqnroll.CucumberMessages; using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PubSub; namespace Reqnroll.Infrastructure { diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 7271f6f5b..584160ccb 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -1,4 +1,4 @@ -using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.RuntimeSupport; using System; using System.Collections.Specialized; using System.Linq; diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs index 74d5c6cbc..c8255cb3f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs @@ -8,7 +8,7 @@ namespace CucumberMessages.CompatibilityTests.CCK.examples_tables { [Binding] - internal class example_tables + internal class Example_tables { private int _count; private int _friends; diff --git a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs index b28ea5835..a2218e32f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs @@ -1,6 +1,6 @@ using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; -using Reqnroll.CucumberMessages; +using Reqnroll.CucumberMessages.PayloadPatching; using System; using System.Collections.Generic; using System.Linq; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 19a268395..d49432119 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -27,10 +27,43 @@ protected void EnableCucumberMessages() Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE, "LOCAL"); } + protected void SetCucumberMessagesOutputFileName(string fileName) + { + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE, fileName); + } + protected void DisableCucumberMessages() { Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "false"); } + + protected void ResetCucumberMessages(string? fileToDelete = null) + { + DisableCucumberMessages(); + DeletePreviousMessagesOutput(fileToDelete); + ResetCucumberMessagesOutputFileName(); + } + + protected void ResetCucumberMessagesOutputFileName() + { + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE, null); + } + + protected void DeletePreviousMessagesOutput(string? fileToDelete = null) + { + var directory = ActualsResultLocationDirectory(); + + if (fileToDelete != null) + { + var fileToDeletePath = Path.Combine(directory, fileToDelete); + + if (File.Exists(fileToDeletePath)) + { + File.Delete(fileToDeletePath); + } + } + } + protected void AddBindingClassFromResource(string fileName, string? prefix = null, Assembly? assemblyToLoadFrom = null) { var bindingCLassFileContent = _testFileManager.GetTestFileContent(fileName, prefix, assemblyToLoadFrom); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 43c196c8b..42fea24a9 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -1,5 +1,3 @@ - -using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using Newtonsoft.Json.Bson; @@ -17,32 +15,13 @@ using Reqnroll.EnvironmentAccess; using SpecFlow.Internal.Json; using Microsoft.VisualBasic.FileIO; +using Reqnroll.CucumberMessages.PayloadProcessing; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { - private void ResetCucumberMessages(string? fileToDelete = null) - { - DisableCucumberMessages(); - DeletePreviousMessagesOutput(fileToDelete); - } - - private void DeletePreviousMessagesOutput(string? fileToDelete = null) - { - var directory = ActualsResultLocationDirectory(); - - if (fileToDelete != null) - { - var fileToDeletePath = Path.Combine(directory, fileToDelete); - - if (File.Exists(fileToDeletePath)) - { - File.Delete(fileToDeletePath); - } - } - } [TestMethod] public void NullTest() @@ -64,8 +43,9 @@ When I eat 5 cukes [TestMethod] public void SmokeTest() { - ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); + ResetCucumberMessages("reqnoll_report.ndjson"); EnableCucumberMessages(); + //SetCucumberMessagesOutputFileName(); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -82,9 +62,9 @@ When I eat 5 cukes [TestMethod] public void SmokeTestMultipleFeatures() { - ResetCucumberMessages("Cucumber Messages Smoke Test.ndjson"); - ResetCucumberMessages("Cucumber Messages Smoke Test Second Smoke Test.ndjson"); + ResetCucumberMessages("SmokeTestMultipleFeatures.ndjson"); EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("SmokeTestMultipleFeatures.ndjson"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -110,6 +90,7 @@ public void SmokeOutlineTest() { ResetCucumberMessages("Cucumber Messages Smoke Outline Test.ndjson"); EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test.ndjson"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -137,6 +118,7 @@ public void SmokeOutlineTestAsMethods() _configurationFileDriver.SetIsRowTestsAllowed(false); EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test As Methods.ndjson"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddFeatureFile(""" @@ -164,6 +146,7 @@ public void CucumberMessagesInteropWithExternalData() ResetCucumberMessages("External Data from CSV file.ndjson"); // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("External Data from CSV file.ndjson"); _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); @@ -299,6 +282,7 @@ public void CCKScenarios(string testName, string featureNameText) { ResetCucumberMessages(featureNameText+".ndjson"); EnableCucumberMessages(); + SetCucumberMessagesOutputFileName(featureNameText + ".ndjson"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); AddUtilClassWithFileSystemPath(); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index e8710f5c0..75e9b74a3 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -4,6 +4,7 @@ using System.ComponentModel.Design; using FluentAssertions.Execution; using System.Reflection; +using Reqnroll.CucumberMessages.PayloadPatching; namespace CucumberMessages.CompatibilityTests { From ff753ef1a9e6d5cbb06a8601cd2a04ccf17de032 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:27:32 -0500 Subject: [PATCH 171/218] Fix bug with creation of ParameterType messages when StepArgumentTransformation's regex is null. --- .../ExecutionTracking/CucumberMessageFactory.cs | 5 +++-- .../CucumberMessages/ExecutionTracking/FeatureTracker.cs | 6 ++++-- .../CucumberMessages/PubSub/CucumberMessagePublisher.cs | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs index de628b6a1..4e608c9c7 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs @@ -95,14 +95,15 @@ internal static UndefinedParameterType ToUndefinedParameterType(string expressio internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) { - var regex = stepTransform.Regex.ToString(); + var regex = stepTransform.Regex; + var regexPattern = regex == null ? null : regex.ToString(); var name = stepTransform.Name ?? stepTransform.Method.ReturnType.Name; var result = new ParameterType ( name, new List { - regex + regexPattern }, false, false, diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 3d5cfbc04..ff723529a 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -4,6 +4,7 @@ using Reqnroll.CucumberMessages.PayloadProcessing; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -12,7 +13,8 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking { public class FeatureTracker { - internal IEnumerable StaticMessages; + internal IEnumerable StaticMessages => _staticMessages.Value; + private Lazy> _staticMessages; // ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. @@ -34,7 +36,7 @@ internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) { if (!Enabled) return; // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow - StaticMessages = GenerateStaticMessages(featureStartedEvent).ToList(); + _staticMessages = new Lazy>(() => GenerateStaticMessages(featureStartedEvent)); } private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) { diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 9e2356c26..3d486c23e 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -52,6 +52,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { + //Debugger.Launch(); _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; From d232bbecbe4db9159905ecb9a8780a730911437f Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:29:24 -0500 Subject: [PATCH 172/218] Fix VB Generation error caused by illegal character in method parameter name Changed the @pickleId parameter name (used in row test method signatures) to a value that doesn't conflict with VB parameter/variable naming rules. --- Reqnroll.Generator/Generation/GeneratorConstants.cs | 2 +- .../Configuration/CucumberConfiguration.cs | 2 +- .../Configuration/CucumberConfigurationConstants.cs | 10 +++++----- .../CucumberCompatibilityTestBase.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 18f58e348..57c1ba091 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -18,7 +18,7 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; - public const string PICKLEID_PARAMETER_NAME = "@pickleId"; + public const string PICKLEID_PARAMETER_NAME = "generatedParameter_pickleId"; public const string PICKLEID_VARIABLE_NAME = "m_pickleId"; public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "pickleStepSequence"; diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index c0265a30a..a842f03f7 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -95,7 +95,7 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) if (fileNameValue is Success) result.OutputFileName = ((Success)fileNameValue).Result; - var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; result.Enabled = Convert.ToBoolean(enabled); diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs index 335ce9e6a..6a9f9b535 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -2,10 +2,10 @@ { public static class CucumberConfigurationConstants { - public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY"; - public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH"; - public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME"; - public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE"; - public const string REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL_CUCUMBER_MESSAGES_ENABLED"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_BASE_DIRECTORY_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__OUTPUT_BASE_DIRECTORY"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__OUTPUT_RELATIVE_PATH"; + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__OUTPUT_FILENAME"; + public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ACTIVE_OUTPUT_PROFILE"; + public const string REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ENABLED"; } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index d49432119..1ff2bc860 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -23,7 +23,7 @@ protected override void TestCleanup() protected void EnableCucumberMessages() { - Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBERMESSAGES_ENABLE_ENVIRONMENT_VARIABLE, "true"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE, "true"); Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE, "LOCAL"); } From 40a97d57a3d5eccfdefaafeb6a93422b72845273 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:10:07 -0500 Subject: [PATCH 173/218] Modified configuration system to contain setting for ID Generation Style. --- .../Configuration/ConfigurationDTO.cs | 6 ++- .../Configuration/CucumberConfiguration.cs | 37 ++++++++++++++----- .../CucumberConfigurationConstants.cs | 4 ++ .../DefaultConfigurationSource.cs | 2 +- .../Configuration/ICucumberConfiguration.cs | 6 ++- .../Configuration/IDGenerationStyle.cs | 13 +++++++ .../Configuration/ResolvedConfiguration.cs | 1 + .../PubSub/FileOutputPlugin.cs | 2 +- .../RuntimeSupport/IdGeneratorFactory.cs | 24 ------------ .../SeedableIncrementingIdGenerator.cs | 27 ++++++++++++++ .../CucumberCompatibilityTestBase.cs | 3 +- 11 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs create mode 100644 Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs index c3c5d0a82..6aff88e48 100644 --- a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -38,14 +38,18 @@ public class Profile public string BasePath { get; set; } public string OutputDirectory { get; set; } public string OutputFileName { get; set; } + public IDGenerationStyle IDGenerationStyle { get; set; } - public Profile(string profileName, string basePath, string outputDirectory, string outputFileName) + public Profile(string profileName, string basePath, string outputDirectory, string outputFileName, string idGenerationStyle = "UUID") { ProfileName = string.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; BasePath = basePath ?? ""; OutputDirectory = outputDirectory ?? ""; OutputFileName = outputFileName ?? ""; + + IDGenerationStyle = CucumberConfiguration.ParseIdGenerationStyle(idGenerationStyle); } + } } diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index a842f03f7..414549380 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -10,21 +10,24 @@ namespace Reqnroll.CucumberMessages.Configuration { public class CucumberConfiguration : ICucumberConfiguration { + public bool Enabled => _enablementOverrideFlag && _resolvedConfiguration.Value.Enabled; + public string BaseDirectory => _resolvedConfiguration.Value.BaseDirectory; + public string OutputDirectory => _resolvedConfiguration.Value.OutputDirectory; + public string OutputFileName => _resolvedConfiguration.Value.OutputFileName; + public IDGenerationStyle IDGenerationStyle => _resolvedConfiguration.Value.IDGenerationStyle; + + private ITraceListener _trace; private IEnvironmentWrapper _environmentWrapper; - private ResolvedConfiguration outputConfiguration = new(); + private Lazy _resolvedConfiguration; private bool _enablementOverrideFlag = true; - public bool Enabled => _enablementOverrideFlag && outputConfiguration.Enabled; - public string BaseDirectory => outputConfiguration.BaseDirectory; - public string OutputDirectory => outputConfiguration.OutputDirectory; - public string OutputFileName => outputConfiguration.OutputFileName; - public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) { _trace = traceListener; _environmentWrapper = environmentWrapper; + _resolvedConfiguration = new Lazy(ResolveConfiguration); } #region Override API @@ -35,7 +38,7 @@ public void SetEnabled(bool value) #endregion - public ResolvedConfiguration ResolveConfiguration() + private ResolvedConfiguration ResolveConfiguration() { var config = ApplyHierarchicalConfiguration(); var resolved = ApplyEnvironmentOverrides(config); @@ -52,7 +55,6 @@ public ResolvedConfiguration ResolveConfiguration() logEntry = $"Cucumber Messages: FileOutput Initialized. Output Path: {Path.Combine(resolved.BaseDirectory, resolved.OutputDirectory, resolved.OutputFileName)}"; _trace!.WriteTestOutput(logEntry); - outputConfiguration = resolved; return resolved; } private ConfigurationDTO ApplyHierarchicalConfiguration() @@ -72,6 +74,7 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) var fileNameValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE); var profileValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + var idGenStyleValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE); var activeConfiguredDestination = config.Profiles.Where(d => d.ProfileName == profileName).FirstOrDefault(); @@ -84,7 +87,8 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) Enabled = config.FileOutputEnabled, BaseDirectory = config.ActiveProfile.BasePath, OutputDirectory = config.ActiveProfile.OutputDirectory, - OutputFileName = config.ActiveProfile.OutputFileName + OutputFileName = config.ActiveProfile.OutputFileName, + IDGenerationStyle = config.ActiveProfile.IDGenerationStyle }; if (baseOutDirValue is Success) @@ -95,6 +99,10 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) if (fileNameValue is Success) result.OutputFileName = ((Success)fileNameValue).Result; + + if (idGenStyleValue is Success) + result.IDGenerationStyle = CucumberConfiguration.ParseIdGenerationStyle(((Success)idGenStyleValue).Result); + var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; @@ -134,6 +142,7 @@ private void AddOrOverrideProfile(List masterList, Profile overridingPr existingProfile.BasePath = overridingProfile.BasePath; existingProfile.OutputDirectory = overridingProfile.OutputDirectory; existingProfile.OutputFileName = overridingProfile.OutputFileName; + existingProfile.IDGenerationStyle = overridingProfile.IDGenerationStyle; } else masterList.Add(overridingProfile); } @@ -151,6 +160,16 @@ private void EnsureOutputDirectory(ResolvedConfiguration config) Directory.CreateDirectory(Path.Combine(config.BaseDirectory, config.OutputDirectory)); } } + public static IDGenerationStyle ParseIdGenerationStyle(string idGenerationStyle) + { + if (string.IsNullOrEmpty(idGenerationStyle)) + idGenerationStyle = "UUID"; + + if ("INCREMENTING".Equals(idGenerationStyle, StringComparison.OrdinalIgnoreCase)) + return IDGenerationStyle.Incrementing; + else + return IDGenerationStyle.UUID; + } } } diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs index 6a9f9b535..3a821e427 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -7,5 +7,9 @@ public static class CucumberConfigurationConstants public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__OUTPUT_FILENAME"; public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ACTIVE_OUTPUT_PROFILE"; public const string REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ENABLED"; + public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ID_GENERATION_STYLE"; + + public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_INCREMENTING = "INCREMENTING"; + public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_UUID = "UUID"; } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs index 1138b160e..1ff1bed5e 100644 --- a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -19,7 +19,7 @@ public ConfigurationDTO GetConfiguration() string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); string defaultOutputFileName = "reqnroll_report.ndjson"; - var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName); + var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName, "UUID"); res.FileOutputEnabled = false; res.Profiles.Add(defaultProfile); return res; diff --git a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs index 5f77e3d58..67c499aab 100644 --- a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs @@ -2,6 +2,10 @@ { public interface ICucumberConfiguration { - ResolvedConfiguration ResolveConfiguration(); + bool Enabled { get; } + string BaseDirectory { get; } + public string OutputDirectory { get; } + public string OutputFileName { get; } + public IDGenerationStyle IDGenerationStyle { get; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs b/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs new file mode 100644 index 000000000..f21f83ea2 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + public enum IDGenerationStyle + { + Incrementing, + UUID + + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs index a11c69f05..646bf822b 100644 --- a/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs @@ -6,6 +6,7 @@ public class ResolvedConfiguration public string BaseDirectory { get; set; } public string OutputDirectory { get; set; } public string OutputFileName { get; set; } + public IDGenerationStyle IDGenerationStyle { get; set; } } } diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 3833d5ac9..b2fc9560c 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -80,7 +80,7 @@ private void CloseFileSink() private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - ResolvedConfiguration config = _configuration.ResolveConfiguration(); + ICucumberConfiguration config = _configuration; if (!config.Enabled) { diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs index ab98d0af9..716c3d039 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs @@ -1,6 +1,5 @@ using Gherkin.CucumberMessages; using System; -using System.Threading; namespace Reqnroll.CucumberMessages.RuntimeSupport { public class IdGeneratorFactory @@ -17,27 +16,4 @@ public static IIdGenerator Create(string previousId) } } } - - public class SeedableIncrementingIdGenerator : IIdGenerator - { - public SeedableIncrementingIdGenerator(int seed) - { - _counter = seed; - } - - private int _counter = 0; - - public string GetNewId() - { - // Using thread-safe incrementing in case scenarios are running in parallel - var nextId = Interlocked.Increment(ref _counter); - return nextId.ToString(); - } - - - public void Reset() - { - _counter = 0; - } - } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs b/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs new file mode 100644 index 000000000..b4e0cd825 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs @@ -0,0 +1,27 @@ +using Gherkin.CucumberMessages; +using System.Threading; +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + public class SeedableIncrementingIdGenerator : IIdGenerator + { + public SeedableIncrementingIdGenerator(int seed) + { + _counter = seed; + } + + private int _counter = 0; + + public string GetNewId() + { + // Using thread-safe incrementing in case scenarios are running in parallel + var nextId = Interlocked.Increment(ref _counter); + return nextId.ToString(); + } + + + public void Reset() + { + _counter = 0; + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 1ff2bc860..1f068e851 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -109,8 +109,7 @@ protected static string ActualsResultLocationDirectory() var tracerMock = new Mock(); var env = new EnvironmentWrapper(); CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); - var resolvedconfiguration = configuration.ResolveConfiguration(); - var resultLocation = Path.Combine(resolvedconfiguration.BaseDirectory, resolvedconfiguration.OutputDirectory); + var resultLocation = Path.Combine(configuration.BaseDirectory, configuration.OutputDirectory); return resultLocation; } From 3052fe79c29e8e617f39cab530bdfa294ad91f23 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 4 Oct 2024 07:50:34 -0500 Subject: [PATCH 174/218] Refactored project structure and namespaces to better group Cucumber Message related items. Fixed a serialization problem with the IDGeneration Style enum. --- .../Generation/GeneratorConstants.cs | 4 +- .../Generation/ScenarioPartHelper.cs | 10 +- .../Generation/UnitTestFeatureGenerator.cs | 15 +- .../Generation/UnitTestMethodGenerator.cs | 31 ++-- .../CucumberMessagesConverter.cs | 2 +- .../GherkinDocumentVisitor.cs | 2 +- .../ICucumberMessageConverter.cs | 2 +- ...chMissingLocationElementsTransformation.cs | 8 +- .../ScenarioTransformationVisitor.cs | 2 +- .../Configuration/ConfigurationDTO.cs | 5 +- .../Configuration/CucumberConfiguration.cs | 14 +- .../CucumberMessages-config-schema.json | 3 + .../DefaultConfigurationSource.cs | 2 +- .../IdGenerationStyleEnumConverter.cs | 36 +++++ .../RCM_ConfigFile_ConfigurationSource.cs | 6 +- .../ExecutionTracking/FeatureTracker.cs | 8 +- .../{ => ExecutionTracking}/IStepTracker.cs | 4 +- .../CucumberMessagEnumConverter.cs | 2 +- .../CucumberMessageExtensions.cs | 6 +- .../CucumberMessageTransformer.cs | 78 +++++----- .../{ => Cucumber}/CucumberMessageVisitor.cs | 2 +- .../CucumberMessage_TraversalVisitorBase.cs | 2 +- .../{ => Cucumber}/ICucumberMessageVisitor.cs | 2 +- .../Gherkin/GherkinDocumentVisitor.cs | 144 ++++++++++++++++++ .../Gherkin/ScenarioTransformationVisitor.cs | 122 +++++++++++++++ .../PayloadProcessing/NdjsonSerializer.cs | 3 +- .../PubSub/CucumberMessageBroker.cs | 19 +-- .../PubSub/CucumberMessagePublisher.cs | 2 +- .../PubSub/FileOutputPlugin.cs | 1 + .../{ => PubSub}/ICucumberMessagePublisher.cs | 2 +- .../FeatureLevelCucumberMessages.cs | 25 +-- .../IncrementingToUUIDConverter.cs | 16 ++ .../RuntimeSupport/PickleJar.cs | 6 +- .../CrossReferenceBuilder.cs | 2 +- .../CucumberMessages.configuration.json | 3 +- .../CucumberMessagesValidator.cs | 3 +- 36 files changed, 447 insertions(+), 147 deletions(-) rename Reqnroll.Parser/{ => CucumberMessageSupport}/CucumberMessagesConverter.cs (97%) rename Reqnroll.Parser/{ => CucumberMessageSupport}/GherkinDocumentVisitor.cs (98%) rename Reqnroll.Parser/{ => CucumberMessageSupport}/ICucumberMessageConverter.cs (90%) rename Reqnroll.Parser/{ => CucumberMessageSupport}/PatchMissingLocationElementsTransformation.cs (82%) rename Reqnroll.Parser/{ => CucumberMessageSupport}/ScenarioTransformationVisitor.cs (98%) create mode 100644 Reqnroll/CucumberMessages/Configuration/IdGenerationStyleEnumConverter.cs rename Reqnroll/CucumberMessages/{ => ExecutionTracking}/IStepTracker.cs (62%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/CucumberMessagEnumConverter.cs (95%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/CucumberMessageExtensions.cs (97%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/CucumberMessageTransformer.cs (77%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/CucumberMessageVisitor.cs (99%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/CucumberMessage_TraversalVisitorBase.cs (99%) rename Reqnroll/CucumberMessages/PayloadProcessing/{ => Cucumber}/ICucumberMessageVisitor.cs (97%) create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs rename Reqnroll/CucumberMessages/{ => PubSub}/ICucumberMessagePublisher.cs (83%) create mode 100644 Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 57c1ba091..7b14ca75e 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -18,8 +18,8 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; - public const string PICKLEID_PARAMETER_NAME = "generatedParameter_pickleId"; - public const string PICKLEID_VARIABLE_NAME = "m_pickleId"; + public const string PICKLEINDEX_PARAMETER_NAME = "generatedParameter_pickleIndex"; + public const string PICKLEINDEX_VARIABLE_NAME = "m_pickleIndex"; public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "pickleStepSequence"; } diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index ccf7608dd..dba549b04 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -230,14 +230,14 @@ private CodeExpression GetSubstitutedString(string text, ParameterSubstitution p "Format", formatArguments.ToArray()); } - public void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + public void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int pickleIndex) { // string m_pickleId = pickleJar.CurrentPickleId; or // string m_pickleId = @pickleId; - var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEID_VARIABLE_NAME, + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEINDEX_VARIABLE_NAME, pickleIdIncludedInParameters ? - new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_PARAMETER_NAME) : - new CodePrimitiveExpression(pickleJar.CurrentPickleId)); + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleIndex.ToString())); testMethod.Statements.Add(pickleIdVariable); } @@ -270,7 +270,7 @@ public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMet "PickleJar" ), "PickleStepSequenceFor", - new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME)) + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME)) ); testMethod.Statements.Add(pickleStepSequence); diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index c69ef7cc6..d9d8e620e 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -3,24 +3,23 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using Gherkin.CucumberMessages; using Reqnroll.Configuration; -using Reqnroll.CucumberMessages; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; using Reqnroll.Parser; +using Reqnroll.Parser.CucmberMessageSupport; using Reqnroll.Tracing; namespace Reqnroll.Generator.Generation { public class UnitTestFeatureGenerator : IFeatureGenerator { - private const string PICKLEJAR = "PICKLEJAR"; - private readonly CodeDomHelper _codeDomHelper; private readonly IDecoratorRegistry _decoratorRegistry; private readonly ITraceListener _traceListener; @@ -228,8 +227,10 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte string featureSourceMessageString = null; string featureGherkinDocumentMessageString = null; string featurePickleMessagesString = null; + string sourceFileLocation = null; try { + sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo //TODO: make the type of IDGenerator configurable var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); @@ -241,15 +242,14 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); - // Save the Pickles to the GenerationContext so that the Pickle and Step Ids can be injected as arguments into the Scenario and Step method signatures - generationContext.CustomData.Add(PICKLEJAR, new PickleJar(featurePickleMessages)); + //generationContext.CustomData.Add(PICKLEJAR, new PickleJar()); } catch(Exception e) { _traceListener.WriteToolOutput($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. - generationContext.CustomData.Add(PICKLEJAR, new PickleJar(new List())); + //generationContext.CustomData.Add(PICKLEJAR, new PickleJar(new List())); return; } // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object @@ -263,7 +263,8 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), new CodePrimitiveExpression(featureSourceMessageString), new CodePrimitiveExpression(featureGherkinDocumentMessageString), - new CodePrimitiveExpression(featurePickleMessagesString)); + new CodePrimitiveExpression(featurePickleMessagesString), + new CodePrimitiveExpression(sourceFileLocation)); CucumberMessagesInitializeMethod.Statements.Add( new CodeAssignStatement( diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index c459d3ba6..389bf0e7f 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -17,7 +17,6 @@ namespace Reqnroll.Generator.Generation { public class UnitTestMethodGenerator { - private const string PICKLEJAR = "PICKLEJAR"; private const string IGNORE_TAG = "@Ignore"; private const string TESTRUNNER_FIELD = "testRunner"; private readonly CodeDomHelper _codeDomHelper; @@ -25,8 +24,7 @@ public class UnitTestMethodGenerator private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; - private PickleJar _pickleJar; - + private int _pickleIndex = 0; public UnitTestMethodGenerator(IUnitTestGeneratorProvider unitTestGeneratorProvider, IDecoratorRegistry decoratorRegistry, CodeDomHelper codeDomHelper, ScenarioPartHelper scenarioPartHelper, ReqnrollConfiguration reqnrollConfiguration) { @@ -51,7 +49,7 @@ IEnumerable GetScenarioDefinitionsOfRule(IEnume public void CreateUnitTests(ReqnrollFeature feature, TestClassGenerationContext generationContext) { - _pickleJar = generationContext.CustomData[PICKLEJAR] as PickleJar; + _pickleIndex = 0; foreach (var scenarioDefinition in GetScenarioDefinitions(feature)) { CreateUnitTest(generationContext, scenarioDefinition); @@ -72,7 +70,7 @@ private void CreateUnitTest(TestClassGenerationContext generationContext, Scenar else { GenerateTest(generationContext, scenarioDefinitionInFeatureFile); - _pickleJar.NextPickle(); + _pickleIndex++; } } @@ -199,7 +197,7 @@ private void GenerateTestBody( AddVariableForArguments(testMethod, paramToIdentifier); - AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, generationContext.CustomData[PICKLEJAR] as PickleJar); + AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, _pickleIndex); AddVariableForPickleStepSequence(testMethod); testMethod.Statements.Add( @@ -210,7 +208,7 @@ private void GenerateTestBody( new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), inheritedTagsExpression, - new CodeVariableReferenceExpression(GeneratorConstants.PICKLEID_VARIABLE_NAME), + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -220,9 +218,9 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } - internal void AddVariableForPickleId(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, PickleJar pickleJar) + internal void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int pickleIndex) { - _scenarioPartHelper.AddVariableForPickleId(testMethod, pickleIdIncludedInParameters, pickleJar); + _scenarioPartHelper.AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, pickleIndex); } internal void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) @@ -386,9 +384,8 @@ private void GenerateScenarioOutlineExamplesAsIndividualMethods( foreach (var example in exampleSet.TableBody.Select((r, i) => new { Row = r, Index = i })) { var variantName = useFirstColumnAsName ? example.Row.Cells.First().Value : string.Format("Variant {0}", example.Index); - var currentPickleId = _pickleJar.CurrentPickleId; - GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, currentPickleId, exampleSet.Tags.ToArray(), variantName); - _pickleJar.NextPickle(); + GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, _pickleIndex, exampleSet.Tags.ToArray(), variantName); + _pickleIndex++; } exampleSetIndex++; @@ -403,11 +400,11 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex { foreach (var row in examples.TableBody) { - var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleJar.CurrentPickleId }); + var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleIndex.ToString() }); _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); - _pickleJar.NextPickle(); + _pickleIndex++; } } } @@ -468,7 +465,7 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), GeneratorConstants.PICKLEID_PARAMETER_NAME)); + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), GeneratorConstants.PICKLEINDEX_PARAMETER_NAME)); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } @@ -481,7 +478,7 @@ private void GenerateScenarioOutlineTestVariant( string exampleSetTitle, string exampleSetIdentifier, Gherkin.Ast.TableRow row, - string pickleId, + int pickleIndex, Tag[] exampleSetTags, string variantName) { @@ -489,7 +486,7 @@ private void GenerateScenarioOutlineTestVariant( //call test implementation with the params var argumentExpressions = row.Cells.Select(paramCell => new CodePrimitiveExpression(paramCell.Value)).Cast().ToList(); - argumentExpressions.Add(new CodePrimitiveExpression(pickleId)); + argumentExpressions.Add(new CodePrimitiveExpression(pickleIndex.ToString())); argumentExpressions.Add(_scenarioPartHelper.GetStringArrayExpression(exampleSetTags)); var statements = new List(); diff --git a/Reqnroll.Parser/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs similarity index 97% rename from Reqnroll.Parser/CucumberMessagesConverter.cs rename to Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs index 38be9c877..995711506 100644 --- a/Reqnroll.Parser/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs @@ -3,7 +3,7 @@ using Gherkin.CucumberMessages; using Gherkin.CucumberMessages.Types; -namespace Reqnroll.Parser +namespace Reqnroll.Parser.CucmberMessageSupport { public class CucumberMessagesConverter : ICucumberMessagesConverters { diff --git a/Reqnroll.Parser/GherkinDocumentVisitor.cs b/Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs similarity index 98% rename from Reqnroll.Parser/GherkinDocumentVisitor.cs rename to Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs index ed9e6eba1..77bff6f48 100644 --- a/Reqnroll.Parser/GherkinDocumentVisitor.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs @@ -1,7 +1,7 @@ using System; using Gherkin.Ast; -namespace Reqnroll.Parser +namespace Reqnroll.Parser.CucmberMessageSupport { abstract class GherkinDocumentVisitor { diff --git a/Reqnroll.Parser/ICucumberMessageConverter.cs b/Reqnroll.Parser/CucumberMessageSupport/ICucumberMessageConverter.cs similarity index 90% rename from Reqnroll.Parser/ICucumberMessageConverter.cs rename to Reqnroll.Parser/CucumberMessageSupport/ICucumberMessageConverter.cs index b08a3773c..aaf58d0ca 100644 --- a/Reqnroll.Parser/ICucumberMessageConverter.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/ICucumberMessageConverter.cs @@ -3,7 +3,7 @@ using System.Text; using Gherkin.CucumberMessages.Types; -namespace Reqnroll.Parser +namespace Reqnroll.Parser.CucmberMessageSupport { public interface ICucumberMessagesConverters { diff --git a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs similarity index 82% rename from Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs rename to Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs index 2569928a0..72b094592 100644 --- a/Reqnroll.Parser/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace Reqnroll.Parser +namespace Reqnroll.Parser.CucmberMessageSupport { internal class PatchMissingLocationElementsTransformation : ScenarioTransformationVisitor { @@ -30,7 +30,7 @@ protected override Scenario GetTransformedScenario(Scenario scenario) scenario.Keyword, scenario.Name, scenario.Description, - scenario.Steps.Select( s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), + scenario.Steps.Select(s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), scenario.Examples.ToArray()); } @@ -56,8 +56,8 @@ protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenar private Examples PatchExamplesLocations(Examples e) { var headerCells = e.TableHeader.Cells; - var tableHeader = new Gherkin.Ast.TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new Gherkin.Ast.TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); - var rows = e.TableBody.Select(r => new Gherkin.Ast.TableRow(PatchLocation(r.Location), r.Cells.Select(c => new Gherkin.Ast.TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); + var tableHeader = new TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); + var rows = e.TableBody.Select(r => new TableRow(PatchLocation(r.Location), r.Cells.Select(c => new TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); return new Examples(e.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); } diff --git a/Reqnroll.Parser/ScenarioTransformationVisitor.cs b/Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs similarity index 98% rename from Reqnroll.Parser/ScenarioTransformationVisitor.cs rename to Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs index 621732af3..85f9dd691 100644 --- a/Reqnroll.Parser/ScenarioTransformationVisitor.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs @@ -3,7 +3,7 @@ using System.Linq; using Gherkin.Ast; -namespace Reqnroll.Parser +namespace Reqnroll.Parser.CucmberMessageSupport { abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor { diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs index 6aff88e48..fedbe3226 100644 --- a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -40,14 +40,13 @@ public class Profile public string OutputFileName { get; set; } public IDGenerationStyle IDGenerationStyle { get; set; } - public Profile(string profileName, string basePath, string outputDirectory, string outputFileName, string idGenerationStyle = "UUID") + public Profile(string profileName, string basePath, string outputDirectory, string outputFileName, IDGenerationStyle idGenerationStyle ) { ProfileName = string.IsNullOrEmpty(profileName) ? "DEFAULT" : profileName; BasePath = basePath ?? ""; OutputDirectory = outputDirectory ?? ""; OutputFileName = outputFileName ?? ""; - - IDGenerationStyle = CucumberConfiguration.ParseIdGenerationStyle(idGenerationStyle); + IDGenerationStyle = idGenerationStyle; } } diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 414549380..a75eda603 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -3,6 +3,7 @@ using Reqnroll.Tracing; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; @@ -40,6 +41,7 @@ public void SetEnabled(bool value) private ResolvedConfiguration ResolveConfiguration() { + //Debugger.Launch(); var config = ApplyHierarchicalConfiguration(); var resolved = ApplyEnvironmentOverrides(config); @@ -101,7 +103,7 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) result.OutputFileName = ((Success)fileNameValue).Result; if (idGenStyleValue is Success) - result.IDGenerationStyle = CucumberConfiguration.ParseIdGenerationStyle(((Success)idGenStyleValue).Result); + result.IDGenerationStyle = IdGenerationStyleEnumConverter.ParseIdGenerationStyle(((Success)idGenStyleValue).Result); var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; @@ -160,16 +162,6 @@ private void EnsureOutputDirectory(ResolvedConfiguration config) Directory.CreateDirectory(Path.Combine(config.BaseDirectory, config.OutputDirectory)); } } - public static IDGenerationStyle ParseIdGenerationStyle(string idGenerationStyle) - { - if (string.IsNullOrEmpty(idGenerationStyle)) - idGenerationStyle = "UUID"; - - if ("INCREMENTING".Equals(idGenerationStyle, StringComparison.OrdinalIgnoreCase)) - return IDGenerationStyle.Incrementing; - else - return IDGenerationStyle.UUID; - } } } diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json index 8f7c6e917..28aa58c5f 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json +++ b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json @@ -24,6 +24,9 @@ }, "OutputFileName": { "type": "string" + }, + "IDGenerationStyle": { + "type": "string" } } } diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs index 1ff1bed5e..35dd0a86f 100644 --- a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -19,7 +19,7 @@ public ConfigurationDTO GetConfiguration() string defaultOutputDirectory = _environmentWrapper.GetCurrentDirectory(); string defaultOutputFileName = "reqnroll_report.ndjson"; - var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName, "UUID"); + var defaultProfile = new Profile("DEFAULT", defaultOutputDirectory, string.Empty, defaultOutputFileName, IDGenerationStyle.UUID); res.FileOutputEnabled = false; res.Profiles.Add(defaultProfile); return res; diff --git a/Reqnroll/CucumberMessages/Configuration/IdGenerationStyleEnumConverter.cs b/Reqnroll/CucumberMessages/Configuration/IdGenerationStyleEnumConverter.cs new file mode 100644 index 000000000..d1e7ea2f5 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/IdGenerationStyleEnumConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal class IdGenerationStyleEnumConverter : JsonConverter + { + public override IDGenerationStyle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return ParseIdGenerationStyle(value); + } + + public override void Write(Utf8JsonWriter writer, IDGenerationStyle value, JsonSerializerOptions options) + { + if (value == IDGenerationStyle.Incrementing) + writer.WriteStringValue("INCREMENTING"); + else + writer.WriteStringValue("UUID"); + } + public static IDGenerationStyle ParseIdGenerationStyle(string idGenerationStyle) + { + if (string.IsNullOrEmpty(idGenerationStyle)) + idGenerationStyle = "UUID"; + + if ("INCREMENTING".Equals(idGenerationStyle, StringComparison.OrdinalIgnoreCase)) + return IDGenerationStyle.Incrementing; + else + return IDGenerationStyle.UUID; + } + + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs index d3521255d..708219c46 100644 --- a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs @@ -9,10 +9,14 @@ internal class RCM_ConfigFile_ConfigurationSource : IConfigurationSource public ConfigurationDTO GetConfiguration() { + var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + jsonOptions.Converters.Add(new IdGenerationStyleEnumConverter()); + ConfigurationDTO configurationDTO = null; if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) { - configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!; + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), + jsonOptions); } return configurationDTO; } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index ff723529a..3e97fc913 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -1,7 +1,7 @@ using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; -using Reqnroll.CucumberMessages.PayloadProcessing; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System; @@ -42,17 +42,17 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { yield return CucumberMessageFactory.ToMeta(featureStartedEvent); - Gherkin.CucumberMessages.Types.Source gherkinSource = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + Gherkin.CucumberMessages.Types.Source gherkinSource = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source; Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); yield return Envelope.Create(messageSource); - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = System.Text.Json.JsonSerializer.Deserialize(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); + Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); yield return Envelope.Create(gherkinDocument); - var gherkinPickles = System.Text.Json.JsonSerializer.Deserialize>(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles); + var gherkinPickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles; var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); string lastID = ExtractLastID(pickles); diff --git a/Reqnroll/CucumberMessages/IStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/IStepTracker.cs similarity index 62% rename from Reqnroll/CucumberMessages/IStepTracker.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/IStepTracker.cs index caf8eb4a0..9b254207b 100644 --- a/Reqnroll/CucumberMessages/IStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/IStepTracker.cs @@ -1,4 +1,4 @@ -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.ExecutionTracking { public interface IStepTracker { @@ -6,5 +6,5 @@ public interface IStepTracker } - + } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs similarity index 95% rename from Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs index 6f5beaf06..3113eb656 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessagEnumConverter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Reqnroll.CucumberMessages.PayloadProcessing +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum { diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageExtensions.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs similarity index 97% rename from Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageExtensions.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs index 6cb1a658c..2925c5c4e 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageExtensions.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { public static class CucumberMessageExtensions { @@ -72,8 +72,8 @@ public static string Id(this object message) public static object Content(this Envelope envelope) { object result = null; - if (envelope.Attachment != null) { result = envelope.Attachment; } - else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } else if (envelope.Hook != null) { result = envelope.Hook; } else if (envelope.Meta != null) { result = envelope.Meta; } else if (envelope.ParameterType != null) { result = envelope.ParameterType; } diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs similarity index 77% rename from Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs index f4f630479..8ea334765 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Reqnroll.CucumberMessages.PayloadProcessing +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { /// /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace @@ -16,7 +16,7 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing /// internal class CucumberMessageTransformer { - internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessages.Types.Source gherkinSource) + internal static Io.Cucumber.Messages.Types.Source ToSource(global::Gherkin.CucumberMessages.Types.Source gherkinSource) { var result = new Io.Cucumber.Messages.Types.Source ( @@ -27,7 +27,7 @@ internal static Io.Cucumber.Messages.Types.Source ToSource(Gherkin.CucumberMessa return result; } - internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) + internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(global::Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) { var result = new Io.Cucumber.Messages.Types.GherkinDocument ( @@ -38,7 +38,7 @@ internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(Ghe return result; } - private static Io.Cucumber.Messages.Types.Feature ToFeature(Gherkin.CucumberMessages.Types.Feature feature) + private static Io.Cucumber.Messages.Types.Feature ToFeature(global::Gherkin.CucumberMessages.Types.Feature feature) { if (feature == null) { @@ -59,7 +59,7 @@ private static Io.Cucumber.Messages.Types.Feature ToFeature(Gherkin.CucumberMess ; } - private static Io.Cucumber.Messages.Types.Location ToLocation(Gherkin.CucumberMessages.Types.Location location) + private static Io.Cucumber.Messages.Types.Location ToLocation(global::Gherkin.CucumberMessages.Types.Location location) { if (location == null) { @@ -69,7 +69,7 @@ private static Io.Cucumber.Messages.Types.Location ToLocation(Gherkin.CucumberMe } - private static Io.Cucumber.Messages.Types.Tag ToTag(Gherkin.CucumberMessages.Types.Tag tag) + private static Io.Cucumber.Messages.Types.Tag ToTag(global::Gherkin.CucumberMessages.Types.Tag tag) { if (tag == null) { @@ -78,7 +78,7 @@ private static Io.Cucumber.Messages.Types.Tag ToTag(Gherkin.CucumberMessages.Typ return new Io.Cucumber.Messages.Types.Tag(ToLocation(tag.Location), tag.Name, tag.Id); } - private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(Gherkin.CucumberMessages.Types.FeatureChild child) + private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(global::Gherkin.CucumberMessages.Types.FeatureChild child) { if (child == null) { @@ -92,7 +92,7 @@ private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(Gherkin.Cu ); } - private static Io.Cucumber.Messages.Types.Scenario ToScenario(Gherkin.CucumberMessages.Types.Scenario scenario) + private static Io.Cucumber.Messages.Types.Scenario ToScenario(global::Gherkin.CucumberMessages.Types.Scenario scenario) { if (scenario == null) { @@ -112,7 +112,7 @@ private static Io.Cucumber.Messages.Types.Scenario ToScenario(Gherkin.CucumberMe ); } - private static Io.Cucumber.Messages.Types.Examples ToExamples(Gherkin.CucumberMessages.Types.Examples examples) + private static Io.Cucumber.Messages.Types.Examples ToExamples(global::Gherkin.CucumberMessages.Types.Examples examples) { if (examples == null) { @@ -130,7 +130,7 @@ private static Io.Cucumber.Messages.Types.Examples ToExamples(Gherkin.CucumberMe examples.Id ); } - private static Io.Cucumber.Messages.Types.TableCell ToTableCell(Gherkin.CucumberMessages.Types.TableCell cell) + private static Io.Cucumber.Messages.Types.TableCell ToTableCell(global::Gherkin.CucumberMessages.Types.TableCell cell) { return new Io.Cucumber.Messages.Types.TableCell( ToLocation(cell.Location), @@ -138,7 +138,7 @@ private static Io.Cucumber.Messages.Types.TableCell ToTableCell(Gherkin.Cucumber ); } - private static Io.Cucumber.Messages.Types.TableRow ToTableRow(Gherkin.CucumberMessages.Types.TableRow row) + private static Io.Cucumber.Messages.Types.TableRow ToTableRow(global::Gherkin.CucumberMessages.Types.TableRow row) { return new Io.Cucumber.Messages.Types.TableRow( ToLocation(row.Location), @@ -146,7 +146,7 @@ private static Io.Cucumber.Messages.Types.TableRow ToTableRow(Gherkin.CucumberMe row.Id ); } - private static Io.Cucumber.Messages.Types.Step ToStep(Gherkin.CucumberMessages.Types.Step step) + private static Io.Cucumber.Messages.Types.Step ToStep(global::Gherkin.CucumberMessages.Types.Step step) { if (step == null) { @@ -164,7 +164,7 @@ private static Io.Cucumber.Messages.Types.Step ToStep(Gherkin.CucumberMessages.T ); } - private static Io.Cucumber.Messages.Types.Background ToBackground(Gherkin.CucumberMessages.Types.Background background) + private static Io.Cucumber.Messages.Types.Background ToBackground(global::Gherkin.CucumberMessages.Types.Background background) { if (background == null) { @@ -180,7 +180,7 @@ private static Io.Cucumber.Messages.Types.Background ToBackground(Gherkin.Cucumb ); } - private static Io.Cucumber.Messages.Types.Rule ToRule(Gherkin.CucumberMessages.Types.Rule rule) + private static Io.Cucumber.Messages.Types.Rule ToRule(global::Gherkin.CucumberMessages.Types.Rule rule) { if (rule == null) { @@ -197,7 +197,7 @@ private static Io.Cucumber.Messages.Types.Rule ToRule(Gherkin.CucumberMessages.T ); } - private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(Gherkin.CucumberMessages.Types.RuleChild child) + private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(global::Gherkin.CucumberMessages.Types.RuleChild child) { return new Io.Cucumber.Messages.Types.RuleChild( ToBackground(child.Background), @@ -205,34 +205,34 @@ private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(Gherkin.Cucumber ); } - private static List ToComments(IReadOnlyCollection comments) + private static List ToComments(IReadOnlyCollection comments) { return comments.Select(ToComment).ToList(); } - private static Io.Cucumber.Messages.Types.Comment ToComment(Gherkin.CucumberMessages.Types.Comment comment) + private static Io.Cucumber.Messages.Types.Comment ToComment(global::Gherkin.CucumberMessages.Types.Comment comment) { return new Io.Cucumber.Messages.Types.Comment( ToLocation(comment.Location), comment.Text ); } - private static StepKeywordType ToKeyWordType(Gherkin.StepKeywordType keywordType) + private static StepKeywordType ToKeyWordType(global::Gherkin.StepKeywordType keywordType) { return keywordType switch { //case Gherkin.StepKeywordType.Unspecified: // return Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN; - Gherkin.StepKeywordType.Context => StepKeywordType.CONTEXT, - Gherkin.StepKeywordType.Conjunction => StepKeywordType.CONJUNCTION, - Gherkin.StepKeywordType.Action => StepKeywordType.ACTION, - Gherkin.StepKeywordType.Outcome => StepKeywordType.OUTCOME, - Gherkin.StepKeywordType.Unknown => StepKeywordType.UNKNOWN, + global::Gherkin.StepKeywordType.Context => StepKeywordType.CONTEXT, + global::Gherkin.StepKeywordType.Conjunction => StepKeywordType.CONJUNCTION, + global::Gherkin.StepKeywordType.Action => StepKeywordType.ACTION, + global::Gherkin.StepKeywordType.Outcome => StepKeywordType.OUTCOME, + global::Gherkin.StepKeywordType.Unknown => StepKeywordType.UNKNOWN, _ => throw new ArgumentException($"Invalid keyword type: {keywordType}"), }; } - private static Io.Cucumber.Messages.Types.DocString ToDocString(Gherkin.CucumberMessages.Types.DocString docString) + private static Io.Cucumber.Messages.Types.DocString ToDocString(global::Gherkin.CucumberMessages.Types.DocString docString) { return new Io.Cucumber.Messages.Types.DocString( ToLocation(docString.Location), @@ -242,7 +242,7 @@ private static Io.Cucumber.Messages.Types.DocString ToDocString(Gherkin.Cucumber ); } - private static Io.Cucumber.Messages.Types.DataTable ToDataTable(Gherkin.CucumberMessages.Types.DataTable dataTable) + private static Io.Cucumber.Messages.Types.DataTable ToDataTable(global::Gherkin.CucumberMessages.Types.DataTable dataTable) { return new Io.Cucumber.Messages.Types.DataTable( ToLocation(dataTable.Location), @@ -250,12 +250,12 @@ private static Io.Cucumber.Messages.Types.DataTable ToDataTable(Gherkin.Cucumber ); } - internal static List ToPickles(IReadOnlyCollection pickles) + internal static List ToPickles(IEnumerable pickles) { return pickles.Select(ToPickle).ToList(); } - private static Io.Cucumber.Messages.Types.Pickle ToPickle(Gherkin.CucumberMessages.Types.Pickle pickle) + private static Io.Cucumber.Messages.Types.Pickle ToPickle(global::Gherkin.CucumberMessages.Types.Pickle pickle) { return new Io.Cucumber.Messages.Types.Pickle( pickle.Id, @@ -267,14 +267,14 @@ private static Io.Cucumber.Messages.Types.Pickle ToPickle(Gherkin.CucumberMessag pickle.AstNodeIds.ToList() ); } - private static Io.Cucumber.Messages.Types.PickleTag ToPickleTag(Gherkin.CucumberMessages.Types.PickleTag pickleTag) + private static Io.Cucumber.Messages.Types.PickleTag ToPickleTag(global::Gherkin.CucumberMessages.Types.PickleTag pickleTag) { return new Io.Cucumber.Messages.Types.PickleTag( pickleTag.Name, pickleTag.AstNodeId ); } - private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(Gherkin.CucumberMessages.Types.PickleStep pickleStep) + private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(global::Gherkin.CucumberMessages.Types.PickleStep pickleStep) { return new Io.Cucumber.Messages.Types.PickleStep( ToPickleStepArgument(pickleStep.Argument), @@ -284,7 +284,7 @@ private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(Gherkin.Cucumb pickleStep.Text ); } - private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgument(Gherkin.CucumberMessages.Types.PickleStepArgument pickleStepArgument) + private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgument(global::Gherkin.CucumberMessages.Types.PickleStepArgument pickleStepArgument) { if (pickleStepArgument == null) { @@ -296,18 +296,18 @@ private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgumen ); } - private static PickleStepType ToPickleStepType(Gherkin.StepKeywordType pickleStepType) + private static PickleStepType ToPickleStepType(global::Gherkin.StepKeywordType pickleStepType) { return pickleStepType switch { - Gherkin.StepKeywordType.Unknown => PickleStepType.UNKNOWN, - Gherkin.StepKeywordType.Action => PickleStepType.ACTION, - Gherkin.StepKeywordType.Outcome => PickleStepType.OUTCOME, - Gherkin.StepKeywordType.Context => PickleStepType.CONTEXT, + global::Gherkin.StepKeywordType.Unknown => PickleStepType.UNKNOWN, + global::Gherkin.StepKeywordType.Action => PickleStepType.ACTION, + global::Gherkin.StepKeywordType.Outcome => PickleStepType.OUTCOME, + global::Gherkin.StepKeywordType.Context => PickleStepType.CONTEXT, _ => throw new ArgumentException($"Invalid pickle step type: {pickleStepType}") }; } - private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gherkin.CucumberMessages.Types.PickleDocString pickleDocString) + private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(global::Gherkin.CucumberMessages.Types.PickleDocString pickleDocString) { if (pickleDocString == null) { @@ -319,7 +319,7 @@ private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(Gher ); } - private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(Gherkin.CucumberMessages.Types.PickleTable pickleTable) + private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(global::Gherkin.CucumberMessages.Types.PickleTable pickleTable) { if (pickleTable == null) { @@ -330,14 +330,14 @@ private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(Gherkin.Cucu ); } - private static Io.Cucumber.Messages.Types.PickleTableRow ToPickleTableRow(Gherkin.CucumberMessages.Types.PickleTableRow pickleTableRow) + private static Io.Cucumber.Messages.Types.PickleTableRow ToPickleTableRow(global::Gherkin.CucumberMessages.Types.PickleTableRow pickleTableRow) { return new Io.Cucumber.Messages.Types.PickleTableRow( pickleTableRow.Cells.Select(ToPickleTableCell).ToList() ); } - private static Io.Cucumber.Messages.Types.PickleTableCell ToPickleTableCell(Gherkin.CucumberMessages.Types.PickleTableCell pickleTableCell) + private static Io.Cucumber.Messages.Types.PickleTableCell ToPickleTableCell(global::Gherkin.CucumberMessages.Types.PickleTableCell pickleTableCell) { return new Io.Cucumber.Messages.Types.PickleTableCell( pickleTableCell.Value diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs similarity index 99% rename from Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs index 7a0eff6be..5ffbc99dd 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessageVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs @@ -4,7 +4,7 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages.PayloadPatching; +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; public class CucumberMessageVisitor { diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs similarity index 99% rename from Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs index 24a8c4186..14d5a3ed1 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages.PayloadPatching +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor { diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs similarity index 97% rename from Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs index 1fe636728..f5b434dbd 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/ICucumberMessageVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs @@ -1,6 +1,6 @@ using Io.Cucumber.Messages.Types; -namespace Reqnroll.CucumberMessages.PayloadPatching; +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; // This interface is used to support the implementation of an External Vistor pattern against the Cucumber Messages. // Visitors impmlement this interface and then invoke it using the helper class below. diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs new file mode 100644 index 000000000..69a6c912e --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using Gherkin.Ast; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + abstract class GherkinDocumentVisitor + { + protected virtual void AcceptDocument(GherkinDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var featureChild in feature.Children) + { + if (featureChild is Rule rule) AcceptRule(rule); + else if (featureChild is Background background) AcceptBackground(background); + else if (featureChild is Scenario scenarioOutline && scenarioOutline.Examples != null && scenarioOutline.Examples.Count() > 0) AcceptScenarioOutline(scenarioOutline); + else if (featureChild is Scenario scenario) AcceptScenario(scenario); + } + OnFeatureVisited(feature); + } + + protected virtual void AcceptStep(Step step) + { + OnStepVisited(step); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + OnScenarioVisited(scenario); + } + + protected virtual void AcceptScenarioOutline(Scenario scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var step in scenarioOutline.Steps) + { + AcceptStep(step); + } + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var ruleChild in rule.Children) + { + if (ruleChild is Background background) AcceptBackground(background); + else if (ruleChild is Scenario scenarioOutline && scenarioOutline.Examples != null && scenarioOutline.Examples.Count() > 0) AcceptScenarioOutline(scenarioOutline); + else if (ruleChild is Scenario scenario) AcceptScenario(scenario); + } + OnRuleVisited(rule); + } + + protected virtual void OnDocumentVisiting(GherkinDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(GherkinDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs new file mode 100644 index 000000000..2ce819a64 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gherkin.Ast; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor + { + protected GherkinDocument _sourceDocument; + private GherkinDocument _transformedDocument; + private Feature _transformedFeature; + private bool _hasTransformedScenarioInFeature = false; + private bool _hasTransformedScenarioInCurrentRule = false; + private readonly List _featureChildren = new(); + private readonly List _ruleChildren = new(); + private List _currentChildren; + + public GherkinDocument TransformDocument(GherkinDocument document) + { + Reset(); + AcceptDocument(document); + return _transformedDocument ?? document; + } + + private void Reset() + { + _sourceDocument = null; + _transformedDocument = null; + _transformedFeature = null; + _featureChildren.Clear(); + _ruleChildren.Clear(); + _hasTransformedScenarioInFeature = false; + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _featureChildren; + } + + protected abstract Scenario GetTransformedScenarioOutline(Scenario scenarioOutline); + protected abstract Scenario GetTransformedScenario(Scenario scenario); + + protected override void OnScenarioOutlineVisited(Scenario scenarioOutline) + { + var transformedScenarioOutline = GetTransformedScenarioOutline(scenarioOutline); + OnScenarioVisitedInternal(scenarioOutline, transformedScenarioOutline); + } + + protected override void OnScenarioVisited(Scenario scenario) + { + var transformedScenario = GetTransformedScenario(scenario); + OnScenarioVisitedInternal(scenario, transformedScenario); + } + + private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedScenario) + { + if (transformedScenario == null) + { + _currentChildren.Add(scenario); + return; + } + + _hasTransformedScenarioInFeature = true; + _hasTransformedScenarioInCurrentRule = true; + _currentChildren.Add(transformedScenario); + } + + protected override void OnBackgroundVisited(Background background) + { + _currentChildren.Add(background); + } + + protected override void OnRuleVisiting(Rule rule) + { + _ruleChildren.Clear(); + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _ruleChildren; + } + + protected override void OnRuleVisited(Rule rule) + { + _currentChildren = _featureChildren; + if (_hasTransformedScenarioInCurrentRule) + { + var transformedRule = new Rule( + rule.Tags?.ToArray() ?? Array.Empty(), + rule.Location, + rule.Keyword, + rule.Name, + rule.Description, + _ruleChildren.ToArray()); + _featureChildren.Add(transformedRule); + } + else + { + _featureChildren.Add(rule); + } + } + + protected override void OnFeatureVisited(Feature feature) + { + if (_hasTransformedScenarioInFeature) + _transformedFeature = new Feature( + feature.Tags?.ToArray() ?? Array.Empty(), + feature.Location, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + _featureChildren.ToArray()); + } + + protected override void OnDocumentVisiting(GherkinDocument document) + { + _sourceDocument = document; + } + + protected override void OnDocumentVisited(GherkinDocument document) + { + if (_transformedFeature != null) + _transformedDocument = new GherkinDocument(_transformedFeature, document.Comments.ToArray()); + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index 44e99073a..50309709f 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -1,4 +1,5 @@ using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using System; using System.Text.Json; @@ -22,7 +23,7 @@ public class NdjsonSerializer options.Converters.Add(new CucumberMessageEnumConverter()); options.Converters.Add(new CucumberMessageEnumConverter()); options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; - options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default; return options; }); diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index f07d4d308..cd2e66fc4 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -1,4 +1,5 @@ using Reqnroll.BoDi; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.Tracing; using System; using System.Collections.Concurrent; @@ -15,7 +16,6 @@ namespace Reqnroll.CucumberMessages.PubSub public interface ICucumberMessageBroker { bool Enabled { get; } - void Complete(string cucumberMessageSource); void Publish(ReqnrollCucumberMessage featureMessages); } @@ -44,22 +44,5 @@ public void Publish(ReqnrollCucumberMessage message) } } - // using an empty CucumberMessage to indicate completion - public void Complete(string cucumberMessageSource) - { - var _traceListener = _objectContainer.Resolve(); - - var completionMessage = new ReqnrollCucumberMessage - { - CucumberMessageSource = cucumberMessageSource - }; - - foreach (var sink in RegisteredSinks.Value) - { - _traceListener.WriteTestOutput($"Broker publishing completion for {cucumberMessageSource}"); - - sink.Publish(completionMessage); - } - } } } diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 3d486c23e..ef8b14bb7 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -48,6 +48,7 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); + } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) @@ -106,7 +107,6 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - _broker.Complete(featureName); } else { diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index b2fc9560c..97cc9b35d 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -80,6 +80,7 @@ private void CloseFileSink() private void LaunchFileSink(TestRunStartedEvent testRunStarted) { + Debugger.Launch(); ICucumberConfiguration config = _configuration; if (!config.Enabled) diff --git a/Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/ICucumberMessagePublisher.cs similarity index 83% rename from Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs rename to Reqnroll/CucumberMessages/PubSub/ICucumberMessagePublisher.cs index 633ad6025..57b6d3539 100644 --- a/Reqnroll/CucumberMessages/ICucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/ICucumberMessagePublisher.cs @@ -1,6 +1,6 @@ using Reqnroll.Events; -namespace Reqnroll.CucumberMessages +namespace Reqnroll.CucumberMessages.PubSub { public interface ICucumberMessagePublisher { diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index e275af1cb..82857c963 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -1,22 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; +using System.Text.Json; namespace Reqnroll.CucumberMessages.RuntimeSupport { public class FeatureLevelCucumberMessages { - public FeatureLevelCucumberMessages(string source, string gkerkinDocument, string pickles) + public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedgherkinDocument, string serializedPickles, string location) { - Source = source; - GherkinDocument = gkerkinDocument; - Pickles = pickles; - PickleJar = new PickleJar(pickles); + GherkinDocument = System.Text.Json.JsonSerializer.Deserialize(serializedgherkinDocument); + //TODO: make the type of IDGenerator configurable + Source = JsonSerializer.Deserialize(serializedSourceMessage); + Pickles = JsonSerializer.Deserialize>(serializedPickles); + Location = location; + PickleJar = new PickleJar(Pickles); } - public string Source { get; } - public string GherkinDocument { get; } - public string Pickles { get; } + public string Location { get; } + public Gherkin.CucumberMessages.Types.Source Source { get; } + public Gherkin.CucumberMessages.Types.GherkinDocument GherkinDocument { get; } + public IEnumerable Pickles { get; } public PickleJar PickleJar { get; } + } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs new file mode 100644 index 000000000..c9210e912 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs @@ -0,0 +1,16 @@ +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + internal class IncrementingToUUIDConverter : CucumberMessage_TraversalVisitorBase + { + Dictionary _mapping = new Dictionary(); + public IncrementingToUUIDConverter() + { + } + + } +} diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index 3cc736658..f47fd9486 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -8,8 +8,6 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport { public class PickleJar { - public const string PICKLEJAR_VARIABLE_NAME = "m_pickleJar"; - public int _PickleCounter = 0; public bool HasPickles { get; } @@ -37,9 +35,9 @@ public string CurrentPickleId } public Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } - public PickleStepSequence PickleStepSequenceFor(string pickleId) + public PickleStepSequence PickleStepSequenceFor(string pickleIndex) { - return new PickleStepSequence(HasPickles, Pickles.Where(p => p.Id == pickleId).First()); + return new PickleStepSequence(HasPickles, Pickles.ElementAt(int.Parse(pickleIndex))); } public void NextPickle() diff --git a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs index a2218e32f..6df495453 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs @@ -1,6 +1,6 @@ using Io.Cucumber.Messages.Types; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; -using Reqnroll.CucumberMessages.PayloadPatching; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using System; using System.Collections.Generic; using System.Linq; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index a9d0152ee..5e5933ec8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -5,7 +5,8 @@ { "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", - "outputDirectory": "CucumberMessages" + "outputDirectory": "CucumberMessages", + "IDGenerationStyle": "INCREMENTING" } ] } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 75e9b74a3..45020f3d2 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -1,10 +1,9 @@ using FluentAssertions; -using Reqnroll.CucumberMessages; using Io.Cucumber.Messages.Types; using System.ComponentModel.Design; using FluentAssertions.Execution; using System.Reflection; -using Reqnroll.CucumberMessages.PayloadPatching; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; namespace CucumberMessages.CompatibilityTests { From 3a11a31171e83f7b24d825c2627b89a93cf069c7 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:43:07 -0500 Subject: [PATCH 175/218] Generation is now aware of configured IDGeneratorStyle. Reworked the AST visitor into a Gherkin.Types visitor. --- .../Generation/UnitTestFeatureGenerator.cs | 9 +- .../Gherkin/GherkinDocumentVisitor.cs | 144 ----------- .../GherkinTypesGherkinDocumentVisitor.cs | 244 ++++++++++++++++++ .../Gherkin/ScenarioTransformationVisitor.cs | 4 +- .../FeatureLevelCucumberMessages.cs | 1 - .../RuntimeSupport/IdGeneratorFactory.cs | 8 + 6 files changed, 261 insertions(+), 149 deletions(-) delete mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index d9d8e620e..204221161 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -8,6 +8,7 @@ using System.Reflection; using Gherkin.CucumberMessages; using Reqnroll.Configuration; +using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; @@ -28,13 +29,15 @@ public class UnitTestFeatureGenerator : IFeatureGenerator private readonly IUnitTestGeneratorProvider _testGeneratorProvider; private readonly UnitTestMethodGenerator _unitTestMethodGenerator; private readonly LinePragmaHandler _linePragmaHandler; + private readonly ICucumberConfiguration _cucumberConfiguration; public UnitTestFeatureGenerator( IUnitTestGeneratorProvider testGeneratorProvider, CodeDomHelper codeDomHelper, ReqnrollConfiguration reqnrollConfiguration, IDecoratorRegistry decoratorRegistry, - ITraceListener traceListener) + ITraceListener traceListener, + ICucumberConfiguration cucumberConfiguration) { _testGeneratorProvider = testGeneratorProvider; _codeDomHelper = codeDomHelper; @@ -44,6 +47,7 @@ public UnitTestFeatureGenerator( _linePragmaHandler = new LinePragmaHandler(_reqnrollConfiguration, _codeDomHelper); _scenarioPartHelper = new ScenarioPartHelper(_reqnrollConfiguration, _codeDomHelper); _unitTestMethodGenerator = new UnitTestMethodGenerator(testGeneratorProvider, decoratorRegistry, _codeDomHelper, _scenarioPartHelper, _reqnrollConfiguration); + _cucumberConfiguration = cucumberConfiguration; } public string TestClassNameFormat { get; set; } = "{0}Feature"; @@ -233,7 +237,8 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo //TODO: make the type of IDGenerator configurable - var messageConverter = new CucumberMessagesConverter(new IncrementingIdGenerator()); + var IDGenStyle = _cucumberConfiguration.IDGenerationStyle; + var messageConverter = new CucumberMessagesConverter(IdGeneratorFactory.Create(IDGenStyle)); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs deleted file mode 100644 index 69a6c912e..000000000 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentVisitor.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Linq; -using Gherkin.Ast; - -namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin -{ - abstract class GherkinDocumentVisitor - { - protected virtual void AcceptDocument(GherkinDocument document) - { - OnDocumentVisiting(document); - if (document.Feature != null) - { - AcceptFeature(document.Feature); - } - OnDocumentVisited(document); - } - - protected virtual void AcceptFeature(Feature feature) - { - OnFeatureVisiting(feature); - foreach (var featureChild in feature.Children) - { - if (featureChild is Rule rule) AcceptRule(rule); - else if (featureChild is Background background) AcceptBackground(background); - else if (featureChild is Scenario scenarioOutline && scenarioOutline.Examples != null && scenarioOutline.Examples.Count() > 0) AcceptScenarioOutline(scenarioOutline); - else if (featureChild is Scenario scenario) AcceptScenario(scenario); - } - OnFeatureVisited(feature); - } - - protected virtual void AcceptStep(Step step) - { - OnStepVisited(step); - } - - protected virtual void AcceptScenario(Scenario scenario) - { - OnScenarioVisiting(scenario); - foreach (var step in scenario.Steps) - { - AcceptStep(step); - } - OnScenarioVisited(scenario); - } - - protected virtual void AcceptScenarioOutline(Scenario scenarioOutline) - { - OnScenarioOutlineVisiting(scenarioOutline); - foreach (var step in scenarioOutline.Steps) - { - AcceptStep(step); - } - OnScenarioOutlineVisited(scenarioOutline); - } - - protected virtual void AcceptBackground(Background background) - { - OnBackgroundVisiting(background); - foreach (var step in background.Steps) - { - AcceptStep(step); - } - OnBackgroundVisited(background); - } - - protected virtual void AcceptRule(Rule rule) - { - OnRuleVisiting(rule); - foreach (var ruleChild in rule.Children) - { - if (ruleChild is Background background) AcceptBackground(background); - else if (ruleChild is Scenario scenarioOutline && scenarioOutline.Examples != null && scenarioOutline.Examples.Count() > 0) AcceptScenarioOutline(scenarioOutline); - else if (ruleChild is Scenario scenario) AcceptScenario(scenario); - } - OnRuleVisited(rule); - } - - protected virtual void OnDocumentVisiting(GherkinDocument document) - { - //nop - } - - protected virtual void OnDocumentVisited(GherkinDocument document) - { - //nop - } - - protected virtual void OnFeatureVisiting(Feature feature) - { - //nop - } - - protected virtual void OnFeatureVisited(Feature feature) - { - //nop - } - - protected virtual void OnBackgroundVisiting(Background background) - { - //nop - } - - protected virtual void OnBackgroundVisited(Background background) - { - //nop - } - - protected virtual void OnRuleVisiting(Rule rule) - { - //nop - } - - protected virtual void OnRuleVisited(Rule rule) - { - //nop - } - - protected virtual void OnScenarioOutlineVisiting(Scenario scenarioOutline) - { - //nop - } - - protected virtual void OnScenarioOutlineVisited(Scenario scenarioOutline) - { - //nop - } - - protected virtual void OnScenarioVisiting(Scenario scenario) - { - //nop - } - - protected virtual void OnScenarioVisited(Scenario scenario) - { - //nop - } - - protected virtual void OnStepVisited(Step step) - { - //nop - } - } -} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs new file mode 100644 index 000000000..9017f831e --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs @@ -0,0 +1,244 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + abstract class GherkinTypesGherkinDocumentVisitor + { + protected virtual void AcceptDocument(GherkinDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var tag in feature.Tags) + AcceptTag(tag); + + foreach (var featureChild in feature.Children) + { + if (featureChild.Rule != null) AcceptRule(featureChild.Rule); + else if (featureChild.Background != null) AcceptBackground(featureChild.Background); + else if (featureChild.Scenario != null && IsScenarioOutline(featureChild.Scenario)) AcceptScenarioOutline(featureChild.Scenario); + else if (featureChild.Scenario != null) AcceptScenario(featureChild.Scenario); + } + OnFeatureVisited(feature); + } + private bool IsScenarioOutline(Scenario scenario) + { + return scenario.Examples != null && scenario.Examples.Any(); + } + protected virtual void AcceptStep(Step step) + { + OnStepVisiting(step); + if (step.DataTable != null) + AcceptDataTable(step.DataTable); + OnStepVisited(step); + } + + protected virtual void AcceptDataTable(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + OnDataTableVisiting(dataTable); + foreach (var row in dataTable.Rows) + { + AcceptTableRow(row); + } + OnDataTableVisited(dataTable); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + NavigateScenarioInner(scenario); + OnScenarioVisited(scenario); + } + + private void NavigateScenarioInner(Scenario scenario) + { + foreach (var tag in scenario.Tags) + { + AcceptTag(tag); + } + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + } + + protected virtual void AcceptScenarioOutline(Scenario scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var examples in scenarioOutline.Examples) + { + AcceptExamples(examples); + } + + NavigateScenarioInner(scenarioOutline); + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var tag in rule.Tags) + AcceptTag(tag); + + foreach (var ruleChild in rule.Children) + { + if (ruleChild.Background != null) AcceptBackground(ruleChild.Background); + else if (ruleChild.Scenario != null && IsScenarioOutline(ruleChild.Scenario)) AcceptScenarioOutline(ruleChild.Scenario); + else if (ruleChild.Scenario != null) AcceptScenario(ruleChild.Scenario); + } + OnRuleVisited(rule); + } + + protected virtual void AcceptTableRow(TableRow row) + { + OnTableRowVisited(row); + } + + protected virtual void AcceptTag(Tag tag) + { + OnTagVisited(tag); + } + + protected virtual void AcceptExamples(Examples examples) + { + OnExamplesVisiting(examples); + foreach (var tag in examples.Tags) + AcceptTag(tag); + AcceptTableHeader(examples.TableHeader); + foreach (var row in examples.TableBody) + AcceptTableRow(row); + OnExamplesVisited(examples); + } + + protected virtual void AcceptTableHeader(TableRow header) + { + OnTableHeaderVisited(header); + } + + protected virtual void OnDocumentVisiting(GherkinDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(GherkinDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisiting(Step step) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + + protected virtual void OnDataTableVisiting(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + //nop + } + + protected virtual void OnDataTableVisited(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + //nop + } + + protected virtual void OnTableRowVisited(TableRow row) + { + //nop + } + + protected virtual void OnTagVisited(Tag tag) + { + //nop + } + + protected virtual void OnExamplesVisiting(Examples examples) + { + //nop + } + + protected virtual void OnExamplesVisited(Examples examples) + { + //nop + } + + protected virtual void OnTableHeaderVisited(TableRow header) + { + //nop + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs index 2ce819a64..2f0862702 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using Gherkin.Ast; +using Gherkin.CucumberMessages.Types; namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { - abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor + abstract class ScenarioTransformationVisitor : GherkinTypesGherkinDocumentVisitor { protected GherkinDocument _sourceDocument; private GherkinDocument _transformedDocument; diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 82857c963..0d4c921f6 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -8,7 +8,6 @@ public class FeatureLevelCucumberMessages public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedgherkinDocument, string serializedPickles, string location) { GherkinDocument = System.Text.Json.JsonSerializer.Deserialize(serializedgherkinDocument); - //TODO: make the type of IDGenerator configurable Source = JsonSerializer.Deserialize(serializedSourceMessage); Pickles = JsonSerializer.Deserialize>(serializedPickles); Location = location; diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs index 716c3d039..2eaabb890 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs @@ -1,4 +1,5 @@ using Gherkin.CucumberMessages; +using Reqnroll.CucumberMessages.Configuration; using System; namespace Reqnroll.CucumberMessages.RuntimeSupport { @@ -15,5 +16,12 @@ public static IIdGenerator Create(string previousId) return new SeedableIncrementingIdGenerator(int.Parse(previousId)); } } + public static IIdGenerator Create(IDGenerationStyle style) + { + return style switch { + IDGenerationStyle.Incrementing => new IncrementingIdGenerator(), + IDGenerationStyle.UUID => new GuidIdGenerator(), + _ => throw new NotImplementedException() }; + } } } From 44924c76278da0f5d6eb75862e953ef02bd0a29b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:35:39 -0500 Subject: [PATCH 176/218] UUID id generation now working. ID Generation Style is configurable. Modified parameter naming convention used during code generation to match recommendation from @gaspar --- .../DefaultDependencyProvider.cs | 6 + .../Generation/GeneratorConstants.cs | 4 +- .../Generation/UnitTestFeatureGenerator.cs | 1 - .../Configuration/CucumberConfiguration.cs | 2 + .../Configuration/ICucumberConfiguration.cs | 6 +- .../ExecutionTracking/FeatureTracker.cs | 5 + .../ExecutionTracking/HookStepTracker.cs | 1 + .../TestCaseCucumberMessageTracker.cs | 8 +- .../ExecutionTracking/TestStepTracker.cs | 1 + .../Cucumber}/CucumberMessageFactory.cs | 8 +- .../Gherkin/GherkinDocumentIDStyleReWriter.cs | 132 ++++++++++++++++++ .../Gherkin/GherkinTypesPickleVisitor.cs | 130 +++++++++++++++++ .../Gherkin/PickleIDStyleReWriter.cs | 76 ++++++++++ .../Gherkin/ScenarioTransformationVisitor.cs | 122 ---------------- .../PubSub/CucumberMessagePublisher.cs | 4 +- .../PubSub/FileOutputPlugin.cs | 1 - .../FeatureLevelCucumberMessages.cs | 26 +++- .../IncrementingToUUIDConverter.cs | 16 --- Reqnroll/ScenarioInfo.cs | 9 +- .../CucumberCompatibilityTestBase.cs | 6 + .../CucumberCompatibilityTests.cs | 22 +++ .../CucumberMessagesValidator.cs | 6 +- ...ionCucumberMessagePropertySelectionRule.cs | 2 +- .../CustomTestGeneratorProviderTest.cs | 2 +- .../FeatureGeneratorProviderTests.cs | 2 +- .../IUnitTestGeneratorProviderExtensions.cs | 2 +- .../SimpleCucumberMessagesConfiguration.cs | 22 +++ .../TestGeneratorTest.cs | 2 +- .../TestGeneratorTestsBase.cs | 2 +- 29 files changed, 460 insertions(+), 166 deletions(-) rename Reqnroll/CucumberMessages/{ExecutionTracking => PayloadProcessing/Cucumber}/CucumberMessageFactory.cs (97%) create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs create mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs delete mode 100644 Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs delete mode 100644 Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs create mode 100644 Tests/Reqnroll.GeneratorTests/SimpleCucumberMessagesConfiguration.cs diff --git a/Reqnroll.Generator/DefaultDependencyProvider.cs b/Reqnroll.Generator/DefaultDependencyProvider.cs index cf9a95e90..0dea7b598 100644 --- a/Reqnroll.Generator/DefaultDependencyProvider.cs +++ b/Reqnroll.Generator/DefaultDependencyProvider.cs @@ -1,5 +1,7 @@ using Reqnroll.BoDi; using Reqnroll.Configuration; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.Configuration; using Reqnroll.Generator.Generation; using Reqnroll.Generator.Interfaces; @@ -10,6 +12,7 @@ using Reqnroll.Plugins; using Reqnroll.Tracing; using Reqnroll.Utils; +using System.IO; namespace Reqnroll.Generator { @@ -46,6 +49,9 @@ public virtual void RegisterDefaults(ObjectContainer container) container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 7b14ca75e..ab60a6c06 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -18,9 +18,9 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; - public const string PICKLEINDEX_PARAMETER_NAME = "generatedParameter_pickleIndex"; + public const string PICKLEINDEX_PARAMETER_NAME = "__pickleIndex"; public const string PICKLEINDEX_VARIABLE_NAME = "m_pickleIndex"; public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; - public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "pickleStepSequence"; + public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "__pickleStepSequence"; } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 204221161..5ca45bc49 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -236,7 +236,6 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte { sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo - //TODO: make the type of IDGenerator configurable var IDGenStyle = _cucumberConfiguration.IDGenerationStyle; var messageConverter = new CucumberMessagesConverter(IdGeneratorFactory.Create(IDGenStyle)); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index a75eda603..26eb89d40 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -11,6 +11,7 @@ namespace Reqnroll.CucumberMessages.Configuration { public class CucumberConfiguration : ICucumberConfiguration { + public static CucumberConfiguration Current { get; private set; } public bool Enabled => _enablementOverrideFlag && _resolvedConfiguration.Value.Enabled; public string BaseDirectory => _resolvedConfiguration.Value.BaseDirectory; public string OutputDirectory => _resolvedConfiguration.Value.OutputDirectory; @@ -29,6 +30,7 @@ public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper e _trace = traceListener; _environmentWrapper = environmentWrapper; _resolvedConfiguration = new Lazy(ResolveConfiguration); + Current = this; } #region Override API diff --git a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs index 67c499aab..ae7840545 100644 --- a/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/ICucumberConfiguration.cs @@ -4,8 +4,8 @@ public interface ICucumberConfiguration { bool Enabled { get; } string BaseDirectory { get; } - public string OutputDirectory { get; } - public string OutputFileName { get; } - public IDGenerationStyle IDGenerationStyle { get; } + string OutputDirectory { get; } + string OutputFileName { get; } + IDGenerationStyle IDGenerationStyle { get; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 3e97fc913..d9b962634 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -24,6 +24,7 @@ public class FeatureTracker internal Dictionary StepDefinitionsByPattern = new(); public string FeatureName { get; set; } public bool Enabled { get; private set; } + public Dictionary PickleIds { get; } = new(); public FeatureTracker(FeatureStartedEvent featureStartedEvent) { @@ -57,6 +58,10 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature string lastID = ExtractLastID(pickles); IDGenerator = IdGeneratorFactory.Create(lastID); + for(int i = 0; i < pickles.Count; i++) + { + PickleIds.Add(i.ToString(), pickles[i].Id); + } foreach (var pickle in pickles) { diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs index 7f70317e7..9c4136f7a 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs @@ -1,4 +1,5 @@ using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.Events; using System.Collections.Generic; using System.Linq; diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs index 755fc8895..3c8703c2e 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs @@ -1,6 +1,7 @@ using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System; @@ -23,15 +24,18 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) Enabled = featureTracker.Enabled; IDGenerator = featureTracker.IDGenerator; StepDefinitionsByPattern = featureTracker.StepDefinitionsByPattern; + PickleIdList = featureTracker.PickleIds; } // Feature FeatureInfo and Pickle ID make up a unique identifier for tracking execution of Test Cases public string FeatureName { get; set; } public string PickleId { get; set; } = string.Empty; - public string TestCaseTrackerId { get { return FeatureName + PickleId; } } + public string TestCaseTrackerId { get { return FeatureName +@"/" + PickleId; } } public string TestCaseId { get; set; } public string TestCaseStartedId { get; private set; } + private readonly Dictionary PickleIdList; + // When this class is first created (on FeatureStarted), it will not yet be assigned a Scenario/Pickle; // When a Scenario is started, the Publisher will assign the Scenario to the first UnAssigned TestCaseCucumberMessageTracker it finds // This property will indicate that state @@ -164,7 +168,7 @@ internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFini internal void PreProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - PickleId = scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + PickleId = PickleIdList[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = TestCaseTrackerId; TestCaseId = IDGenerator.GetNewId(); TestCaseStartedId = IDGenerator.GetNewId(); diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs index ff1669ee6..5093201ad 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs @@ -1,6 +1,7 @@ using Io.Cucumber.Messages.Types; using Reqnroll.Assist; using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.Events; using System; using System.Collections.Generic; diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs similarity index 97% rename from Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs index 4e608c9c7..bee3e3dfa 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -4,6 +4,7 @@ using Reqnroll.Analytics; using Reqnroll.Bindings; using Reqnroll.CommonModels; +using Reqnroll.CucumberMessages.ExecutionTracking; using Reqnroll.CucumberMessages.PayloadProcessing; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.EnvironmentAccess; @@ -17,8 +18,13 @@ using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; -namespace Reqnroll.CucumberMessages.ExecutionTracking +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { + /// + /// This class provides functions to convert execution level detail (events) into Cucumber message elements + /// + /// These are called after execution is completed for a Feature. + /// internal class CucumberMessageFactory { public static TestRunStarted ToTestRunStarted(FeatureStartedEvent featureStartedEvent) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs new file mode 100644 index 000000000..30a431129 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs @@ -0,0 +1,132 @@ +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.RuntimeSupport; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + internal class GherkinDocumentIDStyleReWriter : GherkinTypesGherkinDocumentVisitor + { + private IIdGenerator _idGenerator; + public Dictionary IdMap = new(); + + public GherkinDocument ReWriteIds(GherkinDocument document, IDGenerationStyle targetStyle) + { + var existingIdStyle = ProbeForIdGenerationStyle(document); + + if (existingIdStyle == targetStyle) + return document; + + _idGenerator = IdGeneratorFactory.Create(targetStyle); + + AcceptDocument(document); + return document; + } + + private IDGenerationStyle ProbeForIdGenerationStyle(GherkinDocument document) + { + if (document.Feature == null) return IDGenerationStyle.UUID; + var child = document.Feature.Children.FirstOrDefault(); + if (child == null) return IDGenerationStyle.UUID; + + if (child.Rule != null) + return ParseStyle(child.Rule.Id); + + if (child.Background != null) + return ParseStyle(child.Background.Id); + + if (child.Scenario != null) + return ParseStyle(child.Scenario.Id); + + return IDGenerationStyle.UUID; + } + + private IDGenerationStyle ParseStyle(string id) + { + if (Guid.TryParse(id, out var _)) + return IDGenerationStyle.UUID; + + return IDGenerationStyle.Incrementing; + } + + protected override void OnTagVisited(Tag tag) + { + base.OnTagVisited(tag); + var oldId = tag.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + tag.Id = newId; + } + protected override void OnScenarioOutlineVisited(Scenario scenarioOutline) + { + base.OnScenarioOutlineVisited(scenarioOutline); + var oldId = scenarioOutline.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + scenarioOutline.Id = newId; + } + + protected override void OnScenarioVisited(Scenario scenario) + { + base.OnScenarioVisited(scenario); + var oldId = scenario.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + scenario.Id = newId; + } + + protected override void OnRuleVisited(Rule rule) + { + base.OnRuleVisited(rule); + var oldId = rule.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + rule.Id = newId; + } + protected override void OnBackgroundVisited(Background background) + { + base.OnBackgroundVisited(background); + var oldId = background.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + background.Id = newId; + + } + protected override void OnStepVisited(Step step) + { + base.OnStepVisited(step); + var oldId = step.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + step.Id = newId; + } + protected override void OnExamplesVisited(Examples examples) + { + base.OnExamplesVisited(examples); + var oldId = examples.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + examples.Id = newId; + } + protected override void OnTableHeaderVisited(TableRow header) + { + base.OnTableHeaderVisited(header); + var oldId = header.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + header.Id = newId; + } + protected override void OnTableRowVisited(TableRow row) + { + base.OnTableRowVisited(row); + var oldId = row.Id; + var newId = _idGenerator.GetNewId(); + IdMap[oldId] = newId; + row.Id = newId; + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs new file mode 100644 index 000000000..ccb6a1a74 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs @@ -0,0 +1,130 @@ +using Gherkin.CucumberMessages.Types; +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + abstract class GherkinTypesPickleVisitor + { + + public virtual void AcceptPickle(Pickle pickle) + { + OnVisitingPickle(pickle); + + foreach (var tag in pickle.Tags) + { + AcceptTag(tag); + } + foreach (var step in pickle.Steps) + { + AcceptStep(step); + } + OnVisitedPickle(pickle); + } + + protected virtual void AcceptStep(PickleStep step) + { + OnVisitingPickleStep(step); + AcceptPickleStepArgument(step.Argument); + OnVisitedPickleStep(step); + } + + protected virtual void AcceptPickleStepArgument(PickleStepArgument argument) + { + OnVisitingPickleStepArgument(argument); + AcceptPickleTable(argument.DataTable); + OnVisitedPickleStepArgument(argument); + } + + protected virtual void AcceptPickleTable(PickleTable dataTable) + { + OnVisitingPickleTable(dataTable); + foreach (var row in dataTable.Rows) + { + AcceptPickleTableRow(row); + } + OnVisitedPickleTable(dataTable); + } + + protected virtual void AcceptPickleTableRow(PickleTableRow row) + { + OnVisitingPickleTableRow(row); + foreach (var cell in row.Cells) + { + AcceptPickleTableCell(cell); + } + OnVisitedPickleTableRow(row); + } + + protected virtual void AcceptPickleTableCell(PickleTableCell cell) + { + OnVisitedPickleTableCell(cell); + } + protected virtual void AcceptTag(PickleTag tag) + { + OnVisitedPickleTag(tag); + } + + protected virtual void OnVisitingPickle(Pickle pickle) + { + //nop + } + + protected virtual void OnVisitedPickle(Pickle pickle) + { + //nop + } + + protected virtual void OnVisitedPickleTag(PickleTag tag) + { + //nop + } + + protected virtual void OnVisitingPickleStep(PickleStep step) + { + //nop + } + + protected virtual void OnVisitedPickleStep(PickleStep step) + { + //nop + } + + protected virtual void OnVisitingPickleStepArgument(PickleStepArgument argument) + { + //nop + } + + protected virtual void OnVisitedPickleStepArgument(PickleStepArgument argument) + { + //nop + } + + protected virtual void OnVisitingPickleTable(PickleTable dataTable) + { + //nop + } + + protected virtual void OnVisitedPickleTable(PickleTable dataTable) + { + //nop + } + + protected virtual void OnVisitingPickleTableRow(PickleTableRow row) + { + //nop + } + + protected virtual void OnVisitedPickleTableRow(PickleTableRow row) + { + //nop + } + + protected virtual void OnVisitedPickleTableCell(PickleTableCell cell) + { + //nop + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs new file mode 100644 index 000000000..88283223a --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs @@ -0,0 +1,76 @@ +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.RuntimeSupport; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + internal class PickleIDStyleReWriter : GherkinTypesPickleVisitor + { + private Dictionary _idMap; + private IEnumerable _originalPickles; + private IDGenerationStyle _idStyle; + private IIdGenerator _idGenerator; + + public IEnumerable ReWriteIds(IEnumerable pickles, Dictionary idMap, IDGenerationStyle targetStyle) + { + if (pickles == null || pickles.Count() == 0) return pickles; + + _idMap = idMap; + _originalPickles = pickles; + _idStyle = targetStyle; + var existingIdStyle = ProbeForIdGenerationStyle(pickles.First()); + + if (existingIdStyle == targetStyle) + return pickles; + + _idGenerator = IdGeneratorFactory.Create(targetStyle); + + + foreach (var pickle in _originalPickles) + { + AcceptPickle(pickle); + } + + return _originalPickles; + } + + private IDGenerationStyle ProbeForIdGenerationStyle(Pickle pickle) + { + if (Guid.TryParse(pickle.Id, out var _)) + return IDGenerationStyle.UUID; + + return IDGenerationStyle.Incrementing; + } + + protected override void OnVisitedPickle(Pickle pickle) + { + base.OnVisitedPickle(pickle); + + if (_idMap.TryGetValue(pickle.Id, out var newId)) + pickle.Id = newId; + pickle.AstNodeIds = pickle.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList().AsReadOnly(); + } + + protected override void OnVisitedPickleStep(PickleStep step) + { + base.OnVisitedPickleStep(step); + if (_idMap.TryGetValue(step.Id, out var newId)) + step.Id = newId; + step.AstNodeIds = step.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList().AsReadOnly(); + } + + protected override void OnVisitedPickleTag(PickleTag tag) + { + base.OnVisitedPickleTag(tag); + + if (_idMap.TryGetValue(tag.AstNodeId, out var newId)) + tag.AstNodeId = newId; + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs deleted file mode 100644 index 2f0862702..000000000 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/ScenarioTransformationVisitor.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Gherkin.CucumberMessages.Types; - -namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin -{ - abstract class ScenarioTransformationVisitor : GherkinTypesGherkinDocumentVisitor - { - protected GherkinDocument _sourceDocument; - private GherkinDocument _transformedDocument; - private Feature _transformedFeature; - private bool _hasTransformedScenarioInFeature = false; - private bool _hasTransformedScenarioInCurrentRule = false; - private readonly List _featureChildren = new(); - private readonly List _ruleChildren = new(); - private List _currentChildren; - - public GherkinDocument TransformDocument(GherkinDocument document) - { - Reset(); - AcceptDocument(document); - return _transformedDocument ?? document; - } - - private void Reset() - { - _sourceDocument = null; - _transformedDocument = null; - _transformedFeature = null; - _featureChildren.Clear(); - _ruleChildren.Clear(); - _hasTransformedScenarioInFeature = false; - _hasTransformedScenarioInCurrentRule = false; - _currentChildren = _featureChildren; - } - - protected abstract Scenario GetTransformedScenarioOutline(Scenario scenarioOutline); - protected abstract Scenario GetTransformedScenario(Scenario scenario); - - protected override void OnScenarioOutlineVisited(Scenario scenarioOutline) - { - var transformedScenarioOutline = GetTransformedScenarioOutline(scenarioOutline); - OnScenarioVisitedInternal(scenarioOutline, transformedScenarioOutline); - } - - protected override void OnScenarioVisited(Scenario scenario) - { - var transformedScenario = GetTransformedScenario(scenario); - OnScenarioVisitedInternal(scenario, transformedScenario); - } - - private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedScenario) - { - if (transformedScenario == null) - { - _currentChildren.Add(scenario); - return; - } - - _hasTransformedScenarioInFeature = true; - _hasTransformedScenarioInCurrentRule = true; - _currentChildren.Add(transformedScenario); - } - - protected override void OnBackgroundVisited(Background background) - { - _currentChildren.Add(background); - } - - protected override void OnRuleVisiting(Rule rule) - { - _ruleChildren.Clear(); - _hasTransformedScenarioInCurrentRule = false; - _currentChildren = _ruleChildren; - } - - protected override void OnRuleVisited(Rule rule) - { - _currentChildren = _featureChildren; - if (_hasTransformedScenarioInCurrentRule) - { - var transformedRule = new Rule( - rule.Tags?.ToArray() ?? Array.Empty(), - rule.Location, - rule.Keyword, - rule.Name, - rule.Description, - _ruleChildren.ToArray()); - _featureChildren.Add(transformedRule); - } - else - { - _featureChildren.Add(rule); - } - } - - protected override void OnFeatureVisited(Feature feature) - { - if (_hasTransformedScenarioInFeature) - _transformedFeature = new Feature( - feature.Tags?.ToArray() ?? Array.Empty(), - feature.Location, - feature.Language, - feature.Keyword, - feature.Name, - feature.Description, - _featureChildren.ToArray()); - } - - protected override void OnDocumentVisiting(GherkinDocument document) - { - _sourceDocument = document; - } - - protected override void OnDocumentVisited(GherkinDocument document) - { - if (_transformedFeature != null) - _transformedDocument = new GherkinDocument(_transformedFeature, document.Comments.ToArray()); - } - } -} diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index ef8b14bb7..f90e74256 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using Reqnroll.CucumberMessages.ExecutionTracking; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; namespace Reqnroll.CucumberMessages.PubSub { @@ -127,7 +128,8 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve { if (featureTracker.Enabled) { - var id = featureName + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleId; + var pickleId = featureTracker.PickleIds[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var id = featureName + @"/" + pickleId; var tccmt = new TestCaseCucumberMessageTracker(featureTracker); tccmt.ProcessEvent(scenarioStartedEvent); traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 97cc9b35d..b2fc9560c 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -80,7 +80,6 @@ private void CloseFileSink() private void LaunchFileSink(TestRunStartedEvent testRunStarted) { - Debugger.Launch(); ICucumberConfiguration config = _configuration; if (!config.Enabled) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 0d4c921f6..149e4eeb6 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -1,15 +1,22 @@ -using System.Collections.Generic; +using Gherkin.CucumberMessages.Types; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing.Gherkin; +using System.Collections.Generic; using System.Text.Json; namespace Reqnroll.CucumberMessages.RuntimeSupport { public class FeatureLevelCucumberMessages { - public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedgherkinDocument, string serializedPickles, string location) + public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedGherkinDocument, string serializedPickles, string location) { - GherkinDocument = System.Text.Json.JsonSerializer.Deserialize(serializedgherkinDocument); Source = JsonSerializer.Deserialize(serializedSourceMessage); - Pickles = JsonSerializer.Deserialize>(serializedPickles); + var gherkinDocument = System.Text.Json.JsonSerializer.Deserialize(serializedGherkinDocument); + var pickles = JsonSerializer.Deserialize>(serializedPickles); + ReWriteIds(gherkinDocument, pickles, out var newGherkinDocument, out var newPickles); + + GherkinDocument = newGherkinDocument; + Pickles = newPickles; Location = location; PickleJar = new PickleJar(Pickles); } @@ -20,5 +27,16 @@ public FeatureLevelCucumberMessages(string serializedSourceMessage, string seria public IEnumerable Pickles { get; } public PickleJar PickleJar { get; } + private void ReWriteIds(GherkinDocument gherkinDocument, IEnumerable pickles, out GherkinDocument newGherkinDocument, out IEnumerable newPickles) + { + var targetIdStyle = CucumberConfiguration.Current.IDGenerationStyle; + var gherkinDocumentIDStyleReWriter = new GherkinDocumentIDStyleReWriter(); + newGherkinDocument = gherkinDocumentIDStyleReWriter.ReWriteIds(gherkinDocument, targetIdStyle); + var idMap = gherkinDocumentIDStyleReWriter.IdMap; + + var pickleIDStyleReWriter = new PickleIDStyleReWriter(); + newPickles = pickleIDStyleReWriter.ReWriteIds(pickles, idMap, targetIdStyle); + } + } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs deleted file mode 100644 index c9210e912..000000000 --- a/Reqnroll/CucumberMessages/RuntimeSupport/IncrementingToUUIDConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Reqnroll.CucumberMessages.RuntimeSupport -{ - internal class IncrementingToUUIDConverter : CucumberMessage_TraversalVisitorBase - { - Dictionary _mapping = new Dictionary(); - public IncrementingToUUIDConverter() - { - } - - } -} diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index 584160ccb..c4d8203d0 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -38,20 +38,21 @@ public class ScenarioInfo public string Description { get; } /// - /// The PickleId of the Scenario when exported as a Cucumber Message "pickle". + /// The PickleIdIndex of the test Scenario when exported as a Cucumber Message "pickle". + /// The index is the sequential number of the pickle in the list of pickles generated from the feature file. /// - public string PickleId { get; } + public string PickleIdIndex { get; } // The list of step PickleIds in the step sequence for this test case. public PickleStepSequence PickleStepSequence { get; private set; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleId = null, PickleStepSequence stepSequence = null) + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleIndex = null, PickleStepSequence stepSequence = null) { Title = title; Description = description; Tags = tags ?? Array.Empty(); Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); - PickleId = pickleId; + PickleIdIndex = pickleIndex; PickleStepSequence = stepSequence; } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 1f068e851..7fc97dfc7 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -42,6 +42,7 @@ protected void ResetCucumberMessages(string? fileToDelete = null) DisableCucumberMessages(); DeletePreviousMessagesOutput(fileToDelete); ResetCucumberMessagesOutputFileName(); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE, null); } protected void ResetCucumberMessagesOutputFileName() @@ -113,5 +114,10 @@ protected static string ActualsResultLocationDirectory() return resultLocation; } + protected void SetEnvironmentVariableForGUIDIdGeneration() + { + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE, CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_UUID); + } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 42fea24a9..0cac5c5db 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -59,6 +59,28 @@ When I eat 5 cukes ShouldAllScenariosPass(); } + + [TestMethod] + public void CanGenerateGUIDIds_SmokeTest() + { + ResetCucumberMessages("CanGenerateGUIDIds_SmokeTest.ndjson"); + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CanGenerateGUIDIds_SmokeTest.ndjson"); + SetEnvironmentVariableForGUIDIdGeneration(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] public void SmokeTestMultipleFeatures() { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 45020f3d2..92d449f1a 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -113,7 +113,7 @@ private void CompareMessageType() if (!(typeof(T) == typeof(TestStepFinished))) { - actual.Should().BeEquivalentTo(expected, options => options.WithTracing()); + actual.Should().BeEquivalentTo(expected, options => options.WithTracing(),"When comparing " + typeof(T).Name + "s"); } else { @@ -128,9 +128,9 @@ private void CompareMessageType() var expected_hookRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId != null).ToList(); var expected_stepRelatedTestStepFinished = expected.OfType().Where(tsf => expecteds_elementsByID[tsf.TestStepId].As().HookId == null).ToList(); - actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing()); + actual_stepRelatedTestStepFinished.Should().BeEquivalentTo(expected_stepRelatedTestStepFinished, options => options.WithTracing(), "when comparing TestStepFinished messages"); - actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing()); + actual_hookRelatedTestStepFinished.Should().BeEquivalentTo(expected_hookRelatedTestStepFinished, options => options.WithoutStrictOrdering().WithTracing(), "when comparing Hook TestStepFinished messages"); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 1cd13c029..502eeefb2 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -17,7 +17,7 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec { // Properties to skip - this is the default set of properties that are not comparable across platforms // Id: Ids are not assigned in the same order across platforms. - // AstNodeIds, PickleId, HookId, PickleStepId, StepDefinitionIds, TestStepId, TestCaseStartedId, TestCaseId, WorkerId: Ids are not assigned in the same order across platforms. + // AstNodeIds, PickleIdIndex, HookId, PickleStepId, StepDefinitionIds, TestStepId, TestCaseStartedId, TestCaseId, WorkerId: Ids are not assigned in the same order across platforms. // Location, Line and Column (in Location elements) are not always comparable (eg, CCK refers to source line #s in typescript) // Uri is not always comparable (eg, CCK refers to source file paths in typescript) // JavaMethod and JavaStackTraceElement contents are specific to the platform. CCK does not include these as it generates Uri references to source rather than Method references diff --git a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs index a984dc039..a67dc9195 100644 --- a/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs +++ b/Tests/Reqnroll.GeneratorTests/CustomTestGeneratorProviderTest.cs @@ -56,7 +56,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(IUnitTestGenerato runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener(), new SimpleCucumberMessagesConfiguration()); } /// diff --git a/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs b/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs index 3d37a9e3b..1dcabf975 100644 --- a/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs +++ b/Tests/Reqnroll.GeneratorTests/FeatureGeneratorProviderTests.cs @@ -18,7 +18,7 @@ private static UnitTestFeatureGeneratorProvider CreateUnitTestFeatureGeneratorPr Configuration.ReqnrollConfiguration generatorReqnrollConfiguration = ConfigurationLoader.GetDefault(); CodeDomHelper codeDomHelper = new CodeDomHelper(CodeDomProviderLanguage.CSharp); UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator( - new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener()); + new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener(), new SimpleCucumberMessagesConfiguration()); return new UnitTestFeatureGeneratorProvider(unitTestFeatureGenerator); } diff --git a/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs b/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs index 75c3e5cc1..ad6590139 100644 --- a/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs +++ b/Tests/Reqnroll.GeneratorTests/IUnitTestGeneratorProviderExtensions.cs @@ -22,7 +22,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(this IUnitTestGen runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener(), new SimpleCucumberMessagesConfiguration()); } public static IFeatureGenerator CreateFeatureGenerator(this IUnitTestGeneratorProvider testGeneratorProvider, string[] addNonParallelizableMarkerForTags = null) diff --git a/Tests/Reqnroll.GeneratorTests/SimpleCucumberMessagesConfiguration.cs b/Tests/Reqnroll.GeneratorTests/SimpleCucumberMessagesConfiguration.cs new file mode 100644 index 000000000..6b50b72ac --- /dev/null +++ b/Tests/Reqnroll.GeneratorTests/SimpleCucumberMessagesConfiguration.cs @@ -0,0 +1,22 @@ +using Reqnroll.CucumberMessages.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reqnroll.GeneratorTests +{ + internal class SimpleCucumberMessagesConfiguration : ICucumberConfiguration + { + public bool Enabled => false; + + public string BaseDirectory => ""; + + public string OutputDirectory => ""; + + public string OutputFileName => "reqnroll_report.ndjson"; + + public IDGenerationStyle IDGenerationStyle => IDGenerationStyle.Incrementing; + } +} diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs index 2a1b542fe..55aa0a414 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTest.cs @@ -52,7 +52,7 @@ public static UnitTestFeatureGenerator CreateUnitTestConverter(IUnitTestGenerato runtimeConfiguration.AllowRowTests = true; runtimeConfiguration.AllowDebugGeneratedFiles = true; - return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener()); + return new UnitTestFeatureGenerator(testGeneratorProvider, codeDomHelper, runtimeConfiguration, new DecoratorRegistryStub(), new DefaultListener(), new SimpleCucumberMessagesConfiguration()); } /// diff --git a/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs b/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs index 4ea012ef8..6c2939c14 100644 --- a/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs +++ b/Tests/Reqnroll.GeneratorTests/TestGeneratorTestsBase.cs @@ -85,7 +85,7 @@ protected TestGenerator CreateTestGenerator(ProjectSettings projectSettings) { Configuration.ReqnrollConfiguration generatorReqnrollConfiguration = ConfigurationLoader.GetDefault(); CodeDomHelper codeDomHelper = new CodeDomHelper(CodeDomProviderLanguage.CSharp); - UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator(new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener()); + UnitTestFeatureGenerator unitTestFeatureGenerator = new UnitTestFeatureGenerator(new NUnit3TestGeneratorProvider(codeDomHelper), codeDomHelper, generatorReqnrollConfiguration, new DecoratorRegistryStub(), new DefaultListener(), new SimpleCucumberMessagesConfiguration()); var gherkinParserFactory = new ReqnrollGherkinParserFactory(); From 5f7dccb866cdf3a0638baa352e2f3b02975c25ad Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:56:59 -0500 Subject: [PATCH 177/218] Comments added to most classes. --- .../Generation/ScenarioPartHelper.cs | 3 +-- .../Generation/UnitTestFeatureGenerator.cs | 19 ++++++++--------- .../Generation/UnitTestMethodGenerator.cs | 12 +++++++++++ .../CucumberMessagesConverter.cs | 10 +++++++++ ...chMissingLocationElementsTransformation.cs | 4 ++++ .../Configuration/ConfigurationDTO.cs | 3 ++- .../Configuration/CucumberConfiguration.cs | 12 ++++++++++- .../DefaultConfigurationSource.cs | 7 +++++++ .../Configuration/IDGenerationStyle.cs | 1 - .../RCM_ConfigFile_ConfigurationSource.cs | 1 + .../ExecutionTracking/FeatureTracker.cs | 19 +++++++++++++++++ .../ExecutionTracking/HookStepTracker.cs | 4 ++++ .../StepExecutionTrackerBase.cs | 3 +++ .../TestCaseCucumberMessageTracker.cs | 13 ++++++------ .../ExecutionTracking/TestStepTracker.cs | 3 +++ .../Cucumber/CucumberMessagEnumConverter.cs | 5 +++++ .../CucumberMessage_TraversalVisitorBase.cs | 5 +++++ .../Gherkin/GherkinDocumentIDStyleReWriter.cs | 9 ++++++++ .../GherkinTypesGherkinDocumentVisitor.cs | 3 +++ .../Gherkin/GherkinTypesPickleVisitor.cs | 3 +++ .../Gherkin/PickleIDStyleReWriter.cs | 4 ++++ .../PubSub/CucumberMessageBroker.cs | 6 ++++++ .../PubSub/CucumberMessagePublisher.cs | 21 +++++++++++-------- .../PubSub/FileOutputPlugin.cs | 11 +++++++--- .../AttachmentAddedEventWrapper.cs | 1 + .../FeatureLevelCucumberMessages.cs | 4 ++++ .../RuntimeSupport/OutputAddedEventWrapper.cs | 3 +++ .../RuntimeSupport/PickleJar.cs | 6 ++++-- .../EnvironmentInfoProvider.cs | 4 +++- .../EnvironmentAccess/EnvironmentWrapper.cs | 4 ---- Reqnroll/ErrorHandling/BindingException.cs | 6 +++++- Reqnroll/Events/ExecutionEvent.cs | 4 ++++ 32 files changed, 171 insertions(+), 42 deletions(-) diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index dba549b04..eae2c8a60 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -108,7 +108,6 @@ public void GenerateStep(TestClassGenerationContext generationContext, List { new CodePrimitiveExpression(formatText) }; + var formatArguments = new List {new CodePrimitiveExpression(formatText) }; formatArguments.AddRange(arguments.Select(id => new CodeVariableReferenceExpression(id))); return new CodeMethodInvokeExpression( diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 5ca45bc49..0d3d45ca8 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -1,12 +1,9 @@ using System; using System.CodeDom; -using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using Gherkin.CucumberMessages; using Reqnroll.Configuration; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.RuntimeSupport; @@ -37,6 +34,8 @@ public UnitTestFeatureGenerator( ReqnrollConfiguration reqnrollConfiguration, IDecoratorRegistry decoratorRegistry, ITraceListener traceListener, + + // Adding a dependency on the Cucumber configuration subsystem. Eventually remove this as Cucumber Config is folded into overall Reqnroll Config. ICucumberConfiguration cucumberConfiguration) { _testGeneratorProvider = testGeneratorProvider; @@ -226,6 +225,10 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Statements.Add(onFeatureStartExpression); } + // Generation of Cucumber Messages relies on access to the parsed AST. Parsing the AST is not practical (given its structure). + // So we generate the Cucumber messages that are shared across the feature (Source, GherkinDocument and Pickles) and serialize them to strings. + // These strings are generated into the test class as constructor arguments for a new runtime type (FeatureLevelCucumberMessages) which is attached to the FeatureInfo. + // The runtime will later rehydrate these Messages and emit them when the test is run. private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) { string featureSourceMessageString = null; @@ -235,7 +238,8 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte try { sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); - //Generate Feature level Cucumber Messages, serialize them to strings, create a FeatureLevelCucumberMessages object and add it to featureInfo + + // Cucumber IDs can be UUIDs or stringified integers. This is configurable by the user. var IDGenStyle = _cucumberConfiguration.IDGenerationStyle; var messageConverter = new CucumberMessagesConverter(IdGeneratorFactory.Create(IDGenStyle)); var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); @@ -246,14 +250,12 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); - // Save the Pickles to the GenerationContext so that the Pickle and Step Ids can be injected as arguments into the Scenario and Step method signatures - //generationContext.CustomData.Add(PICKLEJAR, new PickleJar()); } catch(Exception e) { _traceListener.WriteToolOutput($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. - //generationContext.CustomData.Add(PICKLEJAR, new PickleJar(new List())); + // This effectively turns OFF the Cucumber Messages support for this feature. return; } // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object @@ -275,9 +277,6 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), featureLevelCucumberMessagesExpression)); - ///At runtime, pull the PickleStepIds from the pickleJar for the given runtime pickleID; partition them out by Background, RuleBackground, and Scenario steps - ///keep an index of which step is being generated and use that to index in to the PickleStepIds at runtime ... eg "stepIds[codeprimitive(stepIndex)]" - // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( null, diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 389bf0e7f..398ad5ab9 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -24,6 +24,10 @@ public class UnitTestMethodGenerator private readonly ScenarioPartHelper _scenarioPartHelper; private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; + + // When generating test methods, the pickle index tells the runtime which Pickle this test method/case corresponds to. + // The index is used during Cucumber Message generation to look up the PickleID and include it in the emitted Cucumber Messages. + // As test methods are generated, the pickle index is incremented. private int _pickleIndex = 0; public UnitTestMethodGenerator(IUnitTestGeneratorProvider unitTestGeneratorProvider, IDecoratorRegistry decoratorRegistry, CodeDomHelper codeDomHelper, ScenarioPartHelper scenarioPartHelper, ReqnrollConfiguration reqnrollConfiguration) @@ -136,6 +140,9 @@ private void GenerateTestBody( CodeMemberMethod testMethod, CodeExpression additionalTagsExpression = null, ParameterSubstitution paramToIdentifier = null, + + // This flag indicates whether the pickleIndex will be given to the method as an argument (coming from a RowTest) (if true) + // or should be generated as a local variable (if false). bool pickleIdIncludedInParameters = false) { var scenarioDefinition = scenarioDefinitionInFeatureFile.ScenarioDefinition; @@ -197,6 +204,9 @@ private void GenerateTestBody( AddVariableForArguments(testMethod, paramToIdentifier); + // Cucumber Messages support uses two new variables: pickleIndex and pickleStepSequence + // The pickleIndex tells the runtime which Pickle this test corresponds to. The pickleStepSequence is used to provide an index into the Pickle Step IDs when generating Step related Messages. + // When Backgrounds and Rule Backgrounds are used, we don't know ahead of time how many Steps there are in the Pickle. AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, _pickleIndex); AddVariableForPickleStepSequence(testMethod); @@ -282,6 +292,8 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex var backgroundMethodCallExpression = new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), generationContext.FeatureBackgroundMethod.Name, + // When invoking a Background, we pass the PickleStepSequence along so that we can track how many steps are generated by the Background. + // The test method will then emit steps with Pickle Step indexes from there. new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)); _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(backgroundMethodCallExpression); diff --git a/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs index 995711506..3f7b36372 100644 --- a/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs @@ -5,6 +5,12 @@ namespace Reqnroll.Parser.CucmberMessageSupport { + /// + /// Utility class that converts Reqnroll AST types in to CucumberMessages.Types types. + /// It uses two classes from the Gherking project: AstMessagesConverter and PickleCompiler + /// + /// Once the Gherkin project implementation directly emits CucumberMessages (eliminating the use of the Gherkin.CucumberMessages.Types namespace), this class can be removed + /// public class CucumberMessagesConverter : ICucumberMessagesConverters { private IIdGenerator _idGenerator; @@ -13,6 +19,10 @@ public CucumberMessagesConverter(IIdGenerator idGenerator) { _idGenerator = idGenerator; } + + // This method transforms an AST ReqnrollDocument into a CucumberMessages.Types.GherkinDocument + // Before doing so, it patches any missing Location elements in the AST. These might be missing because our ExternalData Plugin does not emit Location elements for the Example table rows it generates + // public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) { var NullLocationPatcher = new PatchMissingLocationElementsTransformation(); diff --git a/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs index 72b094592..647b2f358 100644 --- a/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs +++ b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs @@ -6,6 +6,10 @@ namespace Reqnroll.Parser.CucmberMessageSupport { + /// + /// This class is used to patch missing location elements for features, scenarios and scenario outlines. + /// It is based upon the AST visitor implementation found in the External Data plugin. It may be worth finding a way to generalize the visitor base classes but this is sufficient for now. + /// internal class PatchMissingLocationElementsTransformation : ScenarioTransformationVisitor { protected override void OnFeatureVisited(Feature feature) diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs index fedbe3226..6dabf060e 100644 --- a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -8,9 +8,10 @@ namespace Reqnroll.CucumberMessages.Configuration { /// - /// This class holds configuration information from a configuration source. + /// These classes holds configuration information from a configuration source. /// Each configuration source may provide one or more Profiles (such as Dev or Prod). /// The default profile is always named 'DEFAULT'. + /// These are JSON serialized and correspond to the json schema in CucumberMessages-config-schema.json /// public class ConfigurationDTO { diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 26eb89d40..a7df470cd 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -9,6 +9,17 @@ namespace Reqnroll.CucumberMessages.Configuration { + /// + /// This class is responsible for determining the configuration of the Cucumber Messages subsystem. + /// It is wired into the object container as a singleton and is a dependency of the PubSub classes. + /// + /// When any consumer of this class asks for one of the properties of ICucumberConfiguration, + /// the class will resolve the configuration (only once). + /// + /// A default configuration is provided (by DefaultConfigurationSource). + /// It is supplemented by one or more profiles from the configuration file. (RCM_ConfigFile_ConfigurationSource) + /// Then Environmment Variable Overrides are applied. + /// public class CucumberConfiguration : ICucumberConfiguration { public static CucumberConfiguration Current { get; private set; } @@ -43,7 +54,6 @@ public void SetEnabled(bool value) private ResolvedConfiguration ResolveConfiguration() { - //Debugger.Launch(); var config = ApplyHierarchicalConfiguration(); var resolved = ApplyEnvironmentOverrides(config); diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs index 35dd0a86f..0fe7fa521 100644 --- a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -5,6 +5,13 @@ namespace Reqnroll.CucumberMessages.Configuration { + + /// + /// Defaults are: + /// - FileOutputEnabled = false + /// - ActiveProfileName = "DEFAULT" + /// - A default profile with a default output directory and file name and UUID ID generation + /// internal class DefaultConfigurationSource : IConfigurationSource { private IEnvironmentWrapper _environmentWrapper; diff --git a/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs b/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs index f21f83ea2..803ec58ef 100644 --- a/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs +++ b/Reqnroll/CucumberMessages/Configuration/IDGenerationStyle.cs @@ -8,6 +8,5 @@ public enum IDGenerationStyle { Incrementing, UUID - } } diff --git a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs index 708219c46..75200530a 100644 --- a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs @@ -3,6 +3,7 @@ namespace Reqnroll.CucumberMessages.Configuration { + // TODO: add support for an environment variable override of the configuration filename. internal class RCM_ConfigFile_ConfigurationSource : IConfigurationSource { private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index d9b962634..bb2e860cc 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -11,21 +11,32 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking { + /// + /// FeatureTracker is responsible for tracking the execution of a Feature. + /// There will be one instance of this class per gherkin Feature. + /// public class FeatureTracker { + // Static Messages are those generated during code generation (Source, GherkinDocument & Pickles) + // and the StepTransformations, StepDefinitions and Hook messages which are global to the entire Solution. internal IEnumerable StaticMessages => _staticMessages.Value; private Lazy> _staticMessages; // ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } + // This dictionary tracks the StepDefintions(ID) by their method signature // used during TestCase creation to map from a Step Definition binding to its ID internal Dictionary StepDefinitionsByPattern = new(); public string FeatureName { get; set; } public bool Enabled { get; private set; } + + // This dictionary maps from (string) PickleIDIndex to (string) PickleID public Dictionary PickleIds { get; } = new(); + + // This constructor is used by the Publisher when it sees a Feature (by name) for the first time public FeatureTracker(FeatureStartedEvent featureStartedEvent) { FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; @@ -58,6 +69,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature string lastID = ExtractLastID(pickles); IDGenerator = IdGeneratorFactory.Create(lastID); + for(int i = 0; i < pickles.Count; i++) { PickleIds.Add(i.ToString(), pickles[i].Id); @@ -106,6 +118,13 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(featureStartedEvent)); } + + // This method is used to identify the last ID generated from the set generated during code gen. + // It takes advantage of the design assumption that Pickles are generated last, and that PickleSteps are generated before the ID of the Pickle itself. + // Therefore, the ID of the last Pickle is last ID generated. + // Subsequent Messages can be assigned IDs starting from that one (assuming incrementing integer IDs). + // + // Note: Should the method of assigning IDs ever change (or their sequence of assignment) in the code generator, then this method may need to change as well. private string ExtractLastID(List pickles) { return pickles.Last().Id; diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs index 9c4136f7a..5fdb2a203 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs @@ -6,6 +6,10 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking { + /// + /// This class is used to track execution of Hook Steps + /// + /// public class HookStepTracker : StepExecutionTrackerBase { public string HookBindingSignature { get; private set; } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs index 986c4568c..b962212c6 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs @@ -2,6 +2,9 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking { + /// + /// Base class for tracking execution of steps (StepDefinition Methods and Hooks) + /// public class StepExecutionTrackerBase : IStepTracker { public string TestStepID { get; set; } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs index 3c8703c2e..dd3fef169 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs @@ -27,7 +27,7 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) PickleIdList = featureTracker.PickleIds; } - // Feature FeatureInfo and Pickle ID make up a unique identifier for tracking execution of Test Cases + // Feature FeatureName and Pickle ID make up a unique identifier for tracking execution of Test Cases public string FeatureName { get; set; } public string PickleId { get; set; } = string.Empty; public string TestCaseTrackerId { get { return FeatureName +@"/" + PickleId; } } @@ -36,11 +36,6 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) private readonly Dictionary PickleIdList; - // When this class is first created (on FeatureStarted), it will not yet be assigned a Scenario/Pickle; - // When a Scenario is started, the Publisher will assign the Scenario to the first UnAssigned TestCaseCucumberMessageTracker it finds - // This property will indicate that state - public bool IsUnassigned { get { return PickleId == string.Empty; } } - public bool Enabled { get; set; } //This will be false if the feature could not be pickled public bool Finished { get; set; } @@ -51,6 +46,8 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } + // We keep two dictionaries to track the Test Steps and Hooks. + // The first dictionary tracks the Test Steps by their ID, the second will have two entries for each Test Step - one for the Started event and one for the Finished event private Dictionary StepsById { get; set; } = new(); private Dictionary StepsByEvent { get; set; } = new(); public List Steps @@ -62,7 +59,9 @@ public List Steps } public ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } - + // Message generation is a two-stage process. We use these two stages with both the XXXStarted and XXXFinished Events. + // In the first stage (PreProcess) we capture what information we can about the step/hook that provides enough information to tie everything together. + // In the second stage (PostProcess) (run after all events have been executed for a TestCase) we generate the Cucumber Messages for the Test Case // This queue holds ExecutionEvents that will be processed in stage 2 private Queue _events = new(); diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs index 5093201ad..5c2f81f8e 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs @@ -16,6 +16,9 @@ public class StepArgument public string Type; } + /// + /// This class is used to track the execution of Test StepDefinition Methods + /// public class TestStepTracker : StepExecutionTrackerBase { private StepStartedEvent stepStartedEvent; diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs index 3113eb656..a9581b75a 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs @@ -7,6 +7,11 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { + /// + /// Gherkin Cucumber Message enums use attributes to provide a Text value to represent the enum value + /// This class is used to convert the enum to and from a string during serialization + /// + /// internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum { private readonly Dictionary _enumToString = new(); diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs index 14d5a3ed1..04e192f3e 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs @@ -7,6 +7,11 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { + /// + /// Base implementation of a visitor pattern over the Cucumber Message types + /// + /// (consumer of this, for now, is within the Test class for Messages; but provided here for future use) + /// public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor { private void Accept(object message) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs index 30a431129..6792ec08e 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs @@ -9,6 +9,15 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { + /// + /// The ID Generation Style is configurable (by either the config file or by Environment Variable override). + /// + /// This class confirms that the ID Style that was used during code generation is consistent + /// with that which is configured to be used during TEST execution. + /// While it's not likely they would be different, it's possible. + /// + /// If they are possible, we use a visitor pattern to re-write the IDs to the test-time chosen style. + /// internal class GherkinDocumentIDStyleReWriter : GherkinTypesGherkinDocumentVisitor { private IIdGenerator _idGenerator; diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs index 9017f831e..63275849e 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs @@ -5,6 +5,9 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { + /// + /// Absstract base class for implementing a visitor for + /// abstract class GherkinTypesGherkinDocumentVisitor { protected virtual void AcceptDocument(GherkinDocument document) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs index ccb6a1a74..ecd53fb45 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs @@ -6,6 +6,9 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { + /// + /// Abstract base class for visiting Gherkin Pickle (and nested types) + /// abstract class GherkinTypesPickleVisitor { diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs index 88283223a..8f5833107 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs @@ -10,6 +10,10 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { + /// + /// If ID rewriting is required (see cref="GherkinDocumentIDStyleReWriter"), + /// this class will re-write the IDs in the given s. + /// internal class PickleIDStyleReWriter : GherkinTypesPickleVisitor { private Dictionary _idMap; diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index cd2e66fc4..1f7dee6bf 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -19,6 +19,12 @@ public interface ICucumberMessageBroker void Publish(ReqnrollCucumberMessage featureMessages); } + /// + /// Cucumber Message implementation is a simple Pub/Sub implementation. + /// This broker mediates between the (singleton) CucumberMessagePublisher and (one or more) CucumberMessageSinks + /// + /// The pub/sub mechanism is considered to be turned "OFF" if no sinks are registered + /// public class CucumberMessageBroker : ICucumberMessageBroker { private IObjectContainer _objectContainer; diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index f90e74256..5c8ed4761 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -13,18 +13,27 @@ namespace Reqnroll.CucumberMessages.PubSub { + /// + /// Cucumber Message Publisher + /// This class is responsible for publishing CucumberMessages to the CucumberMessageBroker + /// + /// It uses the set of ExecutionEvents to track overall execution of Features and steps and drive generation of messages + /// + /// It uses the IRuntimePlugin interface to force the runtime to load it during startup (although it is not an external plugin per se). + /// public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin { private Lazy _brokerFactory; private ICucumberMessageBroker _broker; private IObjectContainer objectContainer; + + // Started Features by name private ConcurrentDictionary StartedFeatures = new(); private ConcurrentDictionary testCaseTrackersById = new(); bool Enabled = false; public CucumberMessagePublisher() { - //Debugger.Launch(); } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { @@ -54,14 +63,10 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { - //Debugger.Launch(); _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; - // This checks to confirm that the Feature was successfully serialized into the required GherkinDocument and Pickles; - // if not, then this is disabled for this feature - // if true, then it checks with the _broker to confirm that a listener/sink has been registered Enabled = _broker.Enabled; if (!Enabled) { @@ -77,6 +82,8 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); var ft = new FeatureTracker(featureStartedEvent); + + // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) { foreach (var msg in ft.StaticMessages) @@ -188,8 +195,6 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. - // Bypass them by checking for null var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingStartedEvent); @@ -200,8 +205,6 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - // FeatureContext and FeatureInfo will not be available for BeforeTestRun, AfterTestRun, BeforeFeature, AfterFeature hooks. - // Bypass them by checking for null var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) tccmt.ProcessEvent(hookBindingFinishedEvent); diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index b2fc9560c..d6973fe74 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -25,6 +25,12 @@ namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin { + /// + /// The FileOutputPlugin is the subscriber to the CucumberMessageBroker. + /// It receives Cucumber Messages and writes them to a file. + /// + /// File writing is done on a background thread. + /// public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private Task? fileWritingTask; @@ -99,13 +105,13 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) public void Publish(ReqnrollCucumberMessage message) { - //var contentType = message.Envelope == null ? "End of Messages Marker" : message.Envelope.Content().GetType().Name; - //trace?.WriteTestOutput($"FileOutputPlugin Publish. Cucumber Message: {message.CucumberMessageSource}: {contentType}"); postedMessages.Add(message); } private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) { + + // Consider refactoring this to a Using() block. var fileStream = File.CreateText(Path.Combine(baseDirectory, fileName)); @@ -116,7 +122,6 @@ private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string f if (message.Envelope != null) { var cm = Serialize(message.Envelope); - //trace?.WriteTestOutput($"FileOutputPlugin ConsumeAndWriteToFiles. Cucumber Message: {featureName}: {cm.Substring(0, 20)}"); Write(fileStream, cm); } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs index 827a56290..7a4c4a3fd 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/AttachmentAddedEventWrapper.cs @@ -2,6 +2,7 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport { + // This class acts as an addendum to AttachmentAddedEvent and provides the ability to convey which Pickle, TestCase, and TestStep were responsible for the Attachment being added. internal class AttachmentAddedEventWrapper : ExecutionEvent { public AttachmentAddedEventWrapper(AttachmentAddedEvent attachmentAddedEvent, string pickleStepId) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 149e4eeb6..161aedf82 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -6,6 +6,10 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport { + /// + /// This class is used at Code Generation time to provide serialized representations of the Source, GherkinDocument, and Pickles + /// to be used at runtime. + /// public class FeatureLevelCucumberMessages { public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedGherkinDocument, string serializedPickles, string location) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs index ca23002d2..c359071e0 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/OutputAddedEventWrapper.cs @@ -2,6 +2,9 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport { + /// + /// Wraps an to provide a + /// internal class OutputAddedEventWrapper : ExecutionEvent { internal OutputAddedEvent OutputAddedEvent; diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index f47fd9486..266136205 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -6,6 +6,10 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport { + /// + /// These classes are used at runtime to track which Pickle and PickleStep is being executed + /// and to provide the appropriate PickleId and PickleStepId to the TestCaseTracker and StepTracker. + /// public class PickleJar { public int _PickleCounter = 0; @@ -13,8 +17,6 @@ public class PickleJar public bool HasPickles { get; } public IEnumerable Pickles { get; set; } - //public PickleJar(IEnumerable picklesJSON) : this(picklesJSON.Select(s => System.Text.Json.JsonSerializer.Deserialize(s)).ToList()) - //{ } public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs index 417f7eb27..f45ebab66 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; namespace Reqnroll.EnvironmentAccess { + /// + /// This provides an abstraction for obtaining platform and runtime information. Used by Anaytics and Cucumber Messages + /// public class EnvironmentInfoProvider : IEnvironmentInfoProvider { private IEnvironmentWrapper EnvironmentWrapper { get; set; } diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 40d9f0ef3..4ac9ebb9f 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; using Reqnroll.CommonModels; namespace Reqnroll.EnvironmentAccess @@ -39,6 +36,5 @@ public void SetEnvironmentVariable(string name, string value) } public string GetCurrentDirectory() => Environment.CurrentDirectory; - } } diff --git a/Reqnroll/ErrorHandling/BindingException.cs b/Reqnroll/ErrorHandling/BindingException.cs index 3bb5fe4cd..972d52683 100644 --- a/Reqnroll/ErrorHandling/BindingException.cs +++ b/Reqnroll/ErrorHandling/BindingException.cs @@ -1,6 +1,5 @@ using Reqnroll.Bindings; using System; -using System.Collections; using System.Collections.Generic; using System.Runtime.Serialization; @@ -29,6 +28,11 @@ protected BindingException( } } + /// + /// This subclass is added for support of Cucumber Messages. + /// When emitting the Cucumber Message that describes an ambigous matching situation, the Messsage will contain the list of possible matches. + /// We use this subclass of BindingException to convey that information. + /// [Serializable] public class AmbiguousBindingException : BindingException { diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e211c8524..e9b591e2d 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -4,6 +4,10 @@ namespace Reqnroll.Events { + // Cucumber Messages implementation note: Added various forms of context information to + // many of the ExecutionEvents. This allows the CucumberMessages implementation to + // align events with the Scenarios and Features to which they belong. + public class ExecutionEvent : IExecutionEvent { public DateTime Timestamp { get; } From f86db0fd3f05cb14e6104629652e5c769aeb8788 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:57:08 -0500 Subject: [PATCH 178/218] Removed progress tracing. Left in trace output of Warnings related to mis-configuration and exceptions. --- .../Configuration/CucumberConfiguration.cs | 5 ----- .../PubSub/CucumberMessageBroker.cs | 4 ---- .../PubSub/CucumberMessagePublisher.cs | 18 +++++++++--------- .../PubSub/FileOutputPlugin.cs | 5 ----- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index a7df470cd..f621f6937 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -64,11 +64,6 @@ private ResolvedConfiguration ResolveConfiguration() _trace!.WriteToolOutput($"WARNING: Cucumber Messages: Output filename was empty. Setting filename to {resolved.OutputFileName}"); } EnsureOutputDirectory(resolved); - - string logEntry; - logEntry = $"Cucumber Messages: FileOutput Initialized. Output Path: {Path.Combine(resolved.BaseDirectory, resolved.OutputDirectory, resolved.OutputFileName)}"; - - _trace!.WriteTestOutput(logEntry); return resolved; } private ConfigurationDTO ApplyHierarchicalConfiguration() diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index 1f7dee6bf..548163016 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -40,12 +40,8 @@ public CucumberMessageBroker(IObjectContainer objectContainer) } public void Publish(ReqnrollCucumberMessage message) { - var _traceListener = _objectContainer.Resolve(); - foreach (var sink in RegisteredSinks.Value) { - _traceListener.WriteTestOutput($"Broker publishing {message.CucumberMessageSource}: {message.Envelope.Content()}"); - sink.Publish(message); } } diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 5c8ed4761..ee3b7c040 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -70,17 +70,15 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) Enabled = _broker.Enabled; if (!Enabled) { - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: Broker is disabled for {featureName}."); return; } if (StartedFeatures.ContainsKey(featureName)) { - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName} already started"); + // Already started, don't repeat the following steps return; } - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureStartedEventHandler: {featureName}"); var ft = new FeatureTracker(featureStartedEvent); // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. @@ -95,16 +93,13 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { - var traceListener = objectContainer.Resolve(); if (!Enabled) { - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Broker is disabled for {featureFinishedEvent.FeatureContext.FeatureInfo.Title}."); return; } var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; if (!StartedFeatures.ContainsKey(featureName) || !StartedFeatures[featureName].Enabled) { - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: {featureName} was not started or is Disabled."); return; } var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); @@ -118,7 +113,14 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve } else { - traceListener.WriteTestOutput($"Cucumber Message Publisher: FeatureFinishedEventHandler: Error: {featureTestCases.Count(tc => !tc.Finished)} test cases not marked as finished for Feature {featureName}. TestRunFinished event will not be sent."); + // If the feature has no steps, then we should send the finished message; + if (featureTestCases.Count == 0) + { + var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(true, featureFinishedEvent)); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + + // else is an error of a nonsensical state; should we throw an exception? } // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? @@ -139,12 +141,10 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve var id = featureName + @"/" + pickleId; var tccmt = new TestCaseCucumberMessageTracker(featureTracker); tccmt.ProcessEvent(scenarioStartedEvent); - traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} {id} started"); testCaseTrackersById.TryAdd(id, tccmt); } else { - traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker is disabled"); return; } } diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index d6973fe74..582e8eb24 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -78,7 +78,6 @@ private void Close(TestRunFinishedEvent @event) private void CloseFileSink() { if (disposedValue) return; - trace?.WriteTestOutput("FileOutputPlugin Closing File Sink long running thread."); postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; @@ -90,7 +89,6 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) if (!config.Enabled) { - trace!.WriteTestOutput("Cucumber Messages is DISABLED."); // By returning here, we don't launch the File writing thread, // and this class is not registered as a CucumberMessageSink, which indicates to the Broker that Messages are disabled. return; @@ -98,7 +96,6 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) string baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); string fileName = SanitizeFileName(config.OutputFileName); - trace?.WriteToolOutput($"Cuccumber Messages: Starting File Sink long running thread. Writing to: {baseDirectory}"); fileWritingTask = Task.Factory.StartNew( () => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } @@ -138,7 +135,6 @@ private void Write(StreamWriter fileStream, string cucumberMessage) { try { - trace?.WriteTestOutput($"FileOutputPlugin Write. Cucumber Message: {cucumberMessage.Substring(0, 20)}"); fileStream!.WriteLine(cucumberMessage); } catch (System.Exception ex) @@ -149,7 +145,6 @@ private void Write(StreamWriter fileStream, string cucumberMessage) private void CloseStream(StreamWriter fileStream) { - trace?.WriteTestOutput($"FileOutputPlugin Closing File Stream."); fileStream?.Flush(); fileStream?.Close(); fileStream?.Dispose(); From bcba147d5040f429965f23a1fb557347387f4ec4 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:27:43 -0500 Subject: [PATCH 179/218] Generate Messages As CodeDom Expressions The generator now emits the compile-time Messages as CodeDom Expressions and includes them as the initializer for the FeatureCucumberMessages constructor. aka, the messages get translated as code that builds up the object model of the messages again at run-time. --- ...umberGherkinDocumentExpressionGenerator.cs | 410 ++++++++++++++++++ .../CucumberPicklesExpressionGenerator.cs | 178 ++++++++ .../Generation/UnitTestFeatureGenerator.cs | 93 ++-- .../Configuration/CucumberConfiguration.cs | 3 +- .../ExecutionTracking/FeatureTracker.cs | 17 +- .../Cucumber/CucumberMessageTransformer.cs | 8 +- .../CucumberMessage_TraversalVisitorBase.cs | 15 +- .../Gherkin/GherkinDocumentIDStyleReWriter.cs | 100 +++-- .../Gherkin/PickleIDStyleReWriter.cs | 58 ++- .../PubSub/CucumberMessagePublisher.cs | 1 - .../PubSub/FileOutputPlugin.cs | 1 - .../FeatureLevelCucumberMessages.cs | 30 +- .../RuntimeSupport/PickleJar.cs | 4 +- .../CucumberCompatibilityTestBase.cs | 2 +- 14 files changed, 805 insertions(+), 115 deletions(-) create mode 100644 Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs create mode 100644 Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs diff --git a/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs new file mode 100644 index 000000000..41169d446 --- /dev/null +++ b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs @@ -0,0 +1,410 @@ +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Io.Cucumber.Messages.Types; +using Reqnroll.Generator.CodeDom; +using System; +using System.Linq; +using System.CodeDom; +using System.Collections.Generic; + +namespace Reqnroll.Generator.Generation +{ + + /// + /// This class is responsible for generating a CodeDom expression that represents code that will recreate the given Cucumber Gherkin Document. + /// + internal class CucumberGherkinDocumentExpressionGenerator : CucumberMessage_TraversalVisitorBase + { + CodeDomHelper _codeDomHelper; + + Io.Cucumber.Messages.Types.GherkinDocument _gherkinDocument; + CodeExpression _gherkinDocumentExpression; + + CodeExpression _feature; + List _CommentsList; + CodeExpression _location; + List _TagsList; + List _FeatureChildrenList; + CodeExpression _background; + CodeExpression _scenario; + CodeExpression _rule; + List _RuleChildrenList; + List _StepsList; + List _ExamplesList; + CodeExpression _dataTable; + CodeExpression _DocString; + List _TableRowsList; + List _TableCellsList; + + public CucumberGherkinDocumentExpressionGenerator(CodeDomHelper codeDomHelper) + { + _codeDomHelper = codeDomHelper; + } + private void Reset() + { + _feature = null; + _CommentsList = new List(); + _location = null; + _TagsList = new List(); + _FeatureChildrenList = new List(); + _background = null; + _scenario = null; + _rule = null; + _RuleChildrenList = new List(); + _StepsList = new List(); + _ExamplesList = new List(); + _dataTable = null; + _DocString = null; + _TableRowsList = new List(); + _TableCellsList = new List(); + + } + + public CodeExpression GenerateGherkinDocumentExpression(GherkinDocument gherkinDocument) + { + Reset(); + + _gherkinDocument = gherkinDocument; + _CommentsList = new List(); + + Visit(gherkinDocument); + + var commentsListExpr = new CodeTypeReference(typeof(List)); + var initializer = new CodeArrayCreateExpression(typeof(Comment), _CommentsList.ToArray()); + + _gherkinDocumentExpression = new CodeObjectCreateExpression(typeof(GherkinDocument), + new CodePrimitiveExpression(_gherkinDocument.Uri), + _feature, + new CodeObjectCreateExpression(commentsListExpr, initializer)); + + return _gherkinDocumentExpression; + } + + + public override void Visit(Feature feature) + { + var location = _location; + var featureChildren = _FeatureChildrenList; + _FeatureChildrenList = new List(); + var tags = _TagsList; + _TagsList = new List(); + + base.Visit(feature); + + var tagsListExpr = new CodeTypeReference(typeof(List)); + var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + + var FClistExpr = new CodeTypeReference(typeof(List)); + var initializer = new CodeArrayCreateExpression(typeof(FeatureChild), _FeatureChildrenList.ToArray()); + + _feature = new CodeObjectCreateExpression(typeof(Feature), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(feature.Language), + new CodePrimitiveExpression(feature.Keyword), + new CodePrimitiveExpression(feature.Name), + new CodePrimitiveExpression(feature.Description), + new CodeObjectCreateExpression(FClistExpr, initializer)); + + _location = location; + _FeatureChildrenList = featureChildren; + _TagsList = tags; + } + + public override void Visit(Comment comment) + { + var location = _location; + + base.Visit(comment); + + _CommentsList.Add(new CodeObjectCreateExpression(typeof(Comment), + _location, + new CodePrimitiveExpression(comment.Text))); + + _location = location; + } + + public override void Visit(Tag tag) + { + var location = _location; + + base.Visit(tag); + + _TagsList.Add(new CodeObjectCreateExpression(typeof(Tag), + _location, + new CodePrimitiveExpression(tag.Name), + new CodePrimitiveExpression(tag.Id))); + + _location = location; + + } + + public override void Visit(Location location) + { + base.Visit(location); + var columnExprTypeExpr = new CodeTypeReference(typeof(Nullable<>)); + columnExprTypeExpr.TypeArguments.Add(typeof(long)); + + _location = new CodeObjectCreateExpression(typeof(Location), + new CodePrimitiveExpression(location.Line), + location.Column == null ? new CodeObjectCreateExpression(columnExprTypeExpr) :new CodeObjectCreateExpression(columnExprTypeExpr, new CodePrimitiveExpression(location.Column))); + + } + + public override void Visit(FeatureChild featureChild) + { + var rule = _rule; + var scenario = _scenario; + var background = _background; + + _rule = null; + _scenario = null; + _background = null; + + base.Visit(featureChild); + + _FeatureChildrenList.Add(new CodeObjectCreateExpression(typeof(FeatureChild), + _rule ?? new CodePrimitiveExpression(null), + _background ?? new CodePrimitiveExpression(null), + _scenario ?? new CodePrimitiveExpression(null))); + + _rule = rule; + _scenario = scenario; + _background = background; + } + + public override void Visit(Io.Cucumber.Messages.Types.Rule rule) + { + var location = _location; + var ruleChildren = _RuleChildrenList; + _RuleChildrenList = new List(); + var tags = _TagsList; + _TagsList = new List(); + + base.Visit(rule); + + var tagsListExpr = new CodeTypeReference(typeof(List)); + var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + + var ruleChildrenListExpr = new CodeTypeReference(typeof(List)); + var ruleChildrenInitializer = new CodeArrayCreateExpression(typeof(RuleChild), _RuleChildrenList.ToArray()); + + _rule = new CodeObjectCreateExpression(typeof(Io.Cucumber.Messages.Types.Rule), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(rule.Keyword), + new CodePrimitiveExpression(rule.Name), + new CodePrimitiveExpression(rule.Description), + new CodeObjectCreateExpression(ruleChildrenListExpr, ruleChildrenInitializer), + new CodePrimitiveExpression(rule.Id)); + + _location = location; + _RuleChildrenList = ruleChildren; + _TagsList = tags; + } + + public override void Visit(RuleChild ruleChild) + { + var background = _background; + var scenario = _scenario; + + _background = null; + _scenario = null; + + base.Visit(ruleChild); + + _RuleChildrenList.Add(new CodeObjectCreateExpression(typeof(RuleChild), + _background ?? new CodePrimitiveExpression(null), + _scenario ?? new CodePrimitiveExpression(null))); + + _background = background; + _scenario = scenario; + } + + public override void Visit(Scenario scenario) + { + var location = _location; + var tags = _TagsList; + var steps = _StepsList; + var examples = _ExamplesList; + _TagsList = new List(); + _StepsList = new List(); + _ExamplesList = new List(); + + base.Visit(scenario); + + var tagsListExpr = new CodeTypeReference(typeof(List)); + var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + + var stepsListExpr = new CodeTypeReference(typeof(List)); + var stepsinitializer = new CodeArrayCreateExpression(typeof(Step), _StepsList.ToArray()); + + var examplesListExpr = new CodeTypeReference(typeof(List)); + var examplesinitializer = new CodeArrayCreateExpression(typeof(Examples), _ExamplesList.ToArray()); + + _scenario = new CodeObjectCreateExpression(typeof(Scenario), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(scenario.Keyword), + new CodePrimitiveExpression(scenario.Name), + new CodePrimitiveExpression(scenario.Description), + new CodeObjectCreateExpression(stepsListExpr, stepsinitializer), + new CodeObjectCreateExpression(examplesListExpr, examplesinitializer), + new CodePrimitiveExpression(scenario.Id)); + + _location = location; + _TagsList = tags; + _StepsList = steps; + _ExamplesList = examples; + } + + public override void Visit(Examples examples) + { + var location = _location; + var tags = _TagsList; + var table = _TableRowsList; + _TagsList = new List(); + _TableRowsList = new List(); + + // When visting Examples, all TableRow intances that get visited (both TableHeaderRow and TableBodyRows) will get added to the _TableRowsList. + // Therefore, when we create the Examples create expression, we'll pull the Header out of the _TableRowsList as the first item + // and the Body out of the _TableRowsList as the rest of the items. + + base.Visit(examples); + + var tagsListExpr = new CodeTypeReference(typeof(List)); + var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + var tableHeaderRow = _TableRowsList.First(); + var tableBodyListExpr = new CodeTypeReference(typeof(List)); + var tableBodyInitializer = new CodeArrayCreateExpression(typeof(TableRow), _TableRowsList.Skip(1).ToArray()); + + _ExamplesList.Add(new CodeObjectCreateExpression(typeof(Examples), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(examples.Keyword), + new CodePrimitiveExpression(examples.Name), + new CodePrimitiveExpression(examples.Description), + tableHeaderRow, + new CodeObjectCreateExpression(tableBodyListExpr, tableBodyInitializer), + new CodePrimitiveExpression(examples.Id))); + + _location = location; + _TagsList = tags; + _TableRowsList = table; + } + + public override void Visit(Background background) + { + var location = _location; + var steps = _StepsList; + _StepsList = new List(); + + base.Visit(background); + var stepListExpr = new CodeTypeReference(typeof(List)); + var initializer = new CodeArrayCreateExpression(typeof(Step), _StepsList.ToArray()); + + _background = new CodeObjectCreateExpression(typeof(Background), + _location, + new CodePrimitiveExpression(background.Keyword), + new CodePrimitiveExpression(background.Name), + new CodePrimitiveExpression(background.Description), + new CodeObjectCreateExpression(stepListExpr, initializer), + new CodePrimitiveExpression(background.Id)); + + _location = location; + _StepsList = steps; + } + + public override void Visit(Step step) + { + var location = _location; + var docString = _DocString; + var dataTable = _dataTable; + + _DocString = null; + _dataTable = null; + + base.Visit(step); + + _StepsList.Add(new CodeObjectCreateExpression(typeof(Step), + _location, + new CodePrimitiveExpression(step.Keyword), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(StepKeywordType)), step.KeywordType.ToString()), + new CodePrimitiveExpression(step.Text), + _DocString ?? new CodePrimitiveExpression(null), + _dataTable ?? new CodePrimitiveExpression(null), + new CodePrimitiveExpression(step.Id))); + + _location = location; + _DocString = docString; + _dataTable = dataTable; + } + + public override void Visit(DocString docString) + { + var location = _location; + + base.Visit(docString); + + _DocString = new CodeObjectCreateExpression(typeof(DocString), + _location, + new CodePrimitiveExpression(docString.MediaType), + new CodePrimitiveExpression(docString.Content), + new CodePrimitiveExpression(docString.Delimiter)); + + _location = location; + } + + public override void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) + { + var location = _location; + var rows = _TableRowsList; + _TableRowsList = new List(); + + base.Visit(dataTable); + + var listExpr = new CodeTypeReference(typeof(List)); + var initializer = new CodeArrayCreateExpression(typeof(TableRow), _TableRowsList.ToArray()); + + _dataTable = new CodeObjectCreateExpression(typeof(Io.Cucumber.Messages.Types.DataTable), + _location, + new CodeObjectCreateExpression(listExpr, initializer)); + + _location = location; + _TableRowsList = rows; + } + + public override void Visit(TableRow row) + { + var location = _location; + var cells = _TableCellsList; + _TableCellsList = new List(); + + base.Visit(row); + + var CellListExpr = new CodeTypeReference(typeof(List)); + + var initializer = new CodeArrayCreateExpression(typeof(TableCell), _TableCellsList.ToArray()); + + _TableRowsList.Add(new CodeObjectCreateExpression(typeof(TableRow), + _location, + new CodeObjectCreateExpression(CellListExpr, initializer), + new CodePrimitiveExpression(row.Id))); + + _location = location; + _TableCellsList = cells; + } + + public override void Visit(TableCell cell) + { + var location = _location; + + base.Visit(cell); + + _TableCellsList.Add(new CodeObjectCreateExpression(typeof(TableCell), + _location, + new CodePrimitiveExpression(cell.Value))); + + _location = location; + } + } +} diff --git a/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs new file mode 100644 index 000000000..e9f5c9018 --- /dev/null +++ b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs @@ -0,0 +1,178 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Generator.CodeDom; +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.Generator.Generation +{ + /// + /// Generates a CodeDom expression to create a list of Cucumber Pickles + /// + internal class CucumberPicklesExpressionGenerator : CucumberMessage_TraversalVisitorBase + { + private CodeDomHelper _codeDomHelper; + private List _PickleList; + private List _PickleSteps; + private List _PickleTags; + CodeExpression _PickleStepArgument; + CodeExpression _PickleDocString; + CodeExpression _PickleTable; + private List _TableRows; + private List _PickleCells; + + public CucumberPicklesExpressionGenerator(CodeDomHelper codeDomHelper) + { + _codeDomHelper = codeDomHelper; + } + + private void Reset() + { + _PickleList = new List(); + _PickleSteps = new List(); + _PickleTags = new List(); + _PickleStepArgument = null; + _PickleDocString = null; + _PickleTable = null; + } + + public CodeExpression GeneratePicklesExpression(IEnumerable pickles) + { + Reset(); + foreach (var pickle in pickles) + { + Visit(pickle); + } + var commentsListExpr = new CodeTypeReference(typeof(List)); + var initializer = new CodeArrayCreateExpression(typeof(Pickle), _PickleList.ToArray()); + + return new CodeObjectCreateExpression(commentsListExpr, initializer); + } + + public override void Visit(Pickle pickle) + { + var steps = _PickleSteps; + _PickleSteps = new List(); + + var tags = _PickleTags; + _PickleTags = new List(); + + base.Visit(pickle); + + var stepsExpr = new CodeTypeReference(typeof(List)); + var stepsinitializer = new CodeArrayCreateExpression(typeof(PickleStep), _PickleSteps.ToArray()); + + var tagsExpr = new CodeTypeReference(typeof(List)); + var tagsinitializer = new CodeArrayCreateExpression(typeof(PickleTag), _PickleTags.ToArray()); + + var astIdsExpr = new CodeTypeReference(typeof(List)); + var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), pickle.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); + + _PickleList.Add(new CodeObjectCreateExpression(typeof(Pickle), + new CodePrimitiveExpression(pickle.Id), + new CodePrimitiveExpression(pickle.Uri), + new CodePrimitiveExpression(pickle.Name), + new CodePrimitiveExpression(pickle.Language), + new CodeObjectCreateExpression(stepsExpr, stepsinitializer), + new CodeObjectCreateExpression(tagsExpr, tagsinitializer), + new CodeObjectCreateExpression(astIdsExpr, astIdsInitializer) + )); + + _PickleSteps = steps; + _PickleTags = tags; + } + + public override void Visit(PickleStep step) + { + var arg = _PickleStepArgument; + _PickleStepArgument = null; + + base.Visit(step); + + var astIdsExpr = new CodeTypeReference(typeof(List)); + var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), step.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); + + _PickleSteps.Add(new CodeObjectCreateExpression(typeof(PickleStep), + _PickleStepArgument ?? new CodePrimitiveExpression(null), + new CodeObjectCreateExpression(astIdsExpr, astIdsInitializer), + new CodePrimitiveExpression(step.Id), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(PickleStepType)), step.Type.ToString()), + new CodePrimitiveExpression(step.Text))); + + _PickleStepArgument = arg; + } + + public override void Visit(PickleDocString docString) + { + _PickleDocString = new CodeObjectCreateExpression(typeof(PickleDocString), + new CodePrimitiveExpression(docString.MediaType), + new CodePrimitiveExpression(docString.Content)); + } + + public override void Visit(PickleStepArgument argument) + { + var docString = _PickleDocString; + var table = _PickleTable; + + _PickleDocString = null; + _PickleTable = null; + + base.Visit(argument); + + _PickleStepArgument = new CodeObjectCreateExpression(typeof(PickleStepArgument), + _PickleDocString ?? new CodePrimitiveExpression(null), + _PickleTable ?? new CodePrimitiveExpression(null)); + + _PickleDocString = docString; + _PickleTable = table; + } + + public override void Visit(PickleTable pickleTable) + { + var rows = _TableRows; + _TableRows = new List(); + + base.Visit(pickleTable); + + var rowsExpr = new CodeTypeReference(typeof(List)); + var rowsInitializer = new CodeArrayCreateExpression(typeof(PickleTableRow), _TableRows.ToArray()); + + _PickleTable = new CodeObjectCreateExpression(typeof(PickleTable), + new CodeObjectCreateExpression(rowsExpr, rowsInitializer)); + + _TableRows = rows; + } + + public override void Visit(PickleTableRow row) + { + var cells = _PickleCells; + _PickleCells = new List(); + + base.Visit(row); + + var cellsExpr = new CodeTypeReference(typeof(List)); + var cellsInitializer = new CodeArrayCreateExpression(typeof(PickleTableCell), _PickleCells.ToArray()); + + _TableRows.Add(new CodeObjectCreateExpression(typeof(PickleTableRow), + new CodeObjectCreateExpression(cellsExpr, cellsInitializer))); + + _PickleCells = cells; + } + + public override void Visit(PickleTableCell cell) + { + _PickleCells.Add(new CodeObjectCreateExpression(typeof(PickleTableCell), + new CodePrimitiveExpression(cell.Value))); + } + + public override void Visit(PickleTag tag) + { + _PickleTags.Add(new CodeObjectCreateExpression(typeof(PickleTag), + new CodePrimitiveExpression(tag.Name), + new CodePrimitiveExpression(tag.AstNodeId))); + } + } +} diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 0d3d45ca8..38fdd32db 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -128,7 +128,7 @@ private void SetupScenarioCleanupMethod(TestClassGenerationContext generationCon var scenarioCleanupMethod = generationContext.ScenarioCleanupMethod; scenarioCleanupMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; - scenarioCleanupMethod.Name = GeneratorConstants.SCENARIO_CLEANUP_NAME; + scenarioCleanupMethod.Name = GeneratorConstants.SCENARIO_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioCleanupMethod); @@ -225,38 +225,75 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Statements.Add(onFeatureStartExpression); } - // Generation of Cucumber Messages relies on access to the parsed AST. Parsing the AST is not practical (given its structure). - // So we generate the Cucumber messages that are shared across the feature (Source, GherkinDocument and Pickles) and serialize them to strings. - // These strings are generated into the test class as constructor arguments for a new runtime type (FeatureLevelCucumberMessages) which is attached to the FeatureInfo. - // The runtime will later rehydrate these Messages and emit them when the test is run. + // Generation of Cucumber Messages relies on access to the parsed AST. private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) { - string featureSourceMessageString = null; - string featureGherkinDocumentMessageString = null; - string featurePickleMessagesString = null; - string sourceFileLocation = null; - try + CodeObjectCreateExpression sourceExpression; + CodeExpression gherkinDocumentExpression; + CodeExpression picklesExpression; + CodeDelegateCreateExpression sourceFunc; + CodeDelegateCreateExpression gherkinDocumentFunc; + CodeDelegateCreateExpression picklesFunc; + + string sourceFileLocation; + try { sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); // Cucumber IDs can be UUIDs or stringified integers. This is configurable by the user. var IDGenStyle = _cucumberConfiguration.IDGenerationStyle; var messageConverter = new CucumberMessagesConverter(IdGeneratorFactory.Create(IDGenStyle)); - var featureSourceMessage = messageConverter.ConvertToCucumberMessagesSource(generationContext.Document); - var featureGherkinDocumentMessage = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); - var featurePickleMessages = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocumentMessage); - - // Serialize the Cucumber Messages to strings - featureSourceMessageString = System.Text.Json.JsonSerializer.Serialize(featureSourceMessage); - featureGherkinDocumentMessageString = System.Text.Json.JsonSerializer.Serialize(featureGherkinDocumentMessage); - featurePickleMessagesString = System.Text.Json.JsonSerializer.Serialize(featurePickleMessages); + var featureSource = Reqnroll.CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToSource(messageConverter.ConvertToCucumberMessagesSource(generationContext.Document)); + var featureGherkinDocument = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickles = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocument); + var featureGherkinDocumentMessage = CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToGherkinDocument(featureGherkinDocument); + var featurePickleMessages = CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToPickles(featurePickles); + + // generate a CodeDom expression to create the Source object from the featureSourceMessage + sourceExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(Io.Cucumber.Messages.Types.Source)), + new CodePrimitiveExpression(featureSource.Uri), + new CodePrimitiveExpression(featureSource.Data), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(Io.Cucumber.Messages.Types.SourceMediaType)), featureSource.MediaType.ToString())); + + // generate a CodeDom expression to create the GherkinDocument object from the featureGherkinDocumentMessage + var gherkinDocumentExpressionGenerator = new CucumberGherkinDocumentExpressionGenerator(_codeDomHelper); + gherkinDocumentExpression = gherkinDocumentExpressionGenerator.GenerateGherkinDocumentExpression(featureGherkinDocumentMessage); + + // generate a CodeDom expression to create the Pickles object from the featurePickleMessages + var pickleExpressionGenerator = new CucumberPicklesExpressionGenerator(_codeDomHelper); + picklesExpression = pickleExpressionGenerator.GeneratePicklesExpression(featurePickleMessages); + + // wrap these expressions in Func + sourceFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "SourceFunc"); + var sourceFuncBody = new CodeMemberMethod(); + sourceFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; + sourceFuncBody.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Source)); + sourceFuncBody.Statements.Add(new CodeMethodReturnStatement(sourceExpression)); + sourceFuncBody.Name = sourceFunc.MethodName; + generationContext.TestClass.Members.Add(sourceFuncBody); + + gherkinDocumentFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "GherkinDocumentFunc"); + var gherkinDocumentFuncBody = new CodeMemberMethod(); + gherkinDocumentFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; + gherkinDocumentFuncBody.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.GherkinDocument)); + gherkinDocumentFuncBody.Statements.Add(new CodeMethodReturnStatement(gherkinDocumentExpression)); + gherkinDocumentFuncBody.Name = gherkinDocumentFunc.MethodName; + generationContext.TestClass.Members.Add(gherkinDocumentFuncBody); + + picklesFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func>)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "PicklesFunc"); + var picklesFuncBody = new CodeMemberMethod(); + picklesFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; + picklesFuncBody.ReturnType = new CodeTypeReference(typeof(System.Collections.Generic.IEnumerable)); + picklesFuncBody.Statements.Add(new CodeMethodReturnStatement(picklesExpression)); + picklesFuncBody.Name = picklesFunc.MethodName; + generationContext.TestClass.Members.Add(picklesFuncBody); } - catch(Exception e) + catch (Exception e) { _traceListener.WriteToolOutput($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. // This effectively turns OFF the Cucumber Messages support for this feature. - return; + return; } // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object var CucumberMessagesInitializeMethod = new CodeMemberMethod(); @@ -267,9 +304,9 @@ private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationConte // Create a FeatureLevelCucumberMessages object and add it to featureInfo var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), - new CodePrimitiveExpression(featureSourceMessageString), - new CodePrimitiveExpression(featureGherkinDocumentMessageString), - new CodePrimitiveExpression(featurePickleMessagesString), + sourceFunc, + gherkinDocumentFunc, + picklesFunc, new CodePrimitiveExpression(sourceFileLocation)); CucumberMessagesInitializeMethod.Statements.Add( @@ -296,7 +333,7 @@ private void SetupTestClassCleanupMethod(TestClassGenerationContext generationCo testClassCleanupMethod.Name = GeneratorConstants.TESTCLASS_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testClassCleanupMethod); - + _testGeneratorProvider.SetTestClassCleanupMethod(generationContext); var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); @@ -332,7 +369,7 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont testInitializeMethod.Name = GeneratorConstants.TEST_INITIALIZE_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testInitializeMethod); - + _testGeneratorProvider.SetTestInitializeMethod(generationContext); } @@ -344,11 +381,11 @@ private void SetupTestCleanupMethod(TestClassGenerationContext generationContext testCleanupMethod.Name = GeneratorConstants.TEST_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testCleanupMethod); - + _testGeneratorProvider.SetTestCleanupMethod(generationContext); var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); - + //await testRunner.OnScenarioEndAsync(); var expression = new CodeMethodInvokeExpression( testRunnerField, @@ -383,7 +420,7 @@ private void SetupScenarioStartMethod(TestClassGenerationContext generationConte scenarioStartMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; scenarioStartMethod.Name = GeneratorConstants.SCENARIO_START_NAME; - + _codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioStartMethod); //await testRunner.OnScenarioStartAsync(); diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index f621f6937..2eed6dcdb 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -3,7 +3,6 @@ using Reqnroll.Tracing; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; @@ -22,7 +21,7 @@ namespace Reqnroll.CucumberMessages.Configuration /// public class CucumberConfiguration : ICucumberConfiguration { - public static CucumberConfiguration Current { get; private set; } + public static ICucumberConfiguration Current { get; private set; } public bool Enabled => _enablementOverrideFlag && _resolvedConfiguration.Value.Enabled; public string BaseDirectory => _resolvedConfiguration.Value.BaseDirectory; public string OutputDirectory => _resolvedConfiguration.Value.OutputDirectory; diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index bb2e860cc..8d85aaeaf 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -6,6 +6,7 @@ using Reqnroll.Events; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; @@ -54,18 +55,18 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { yield return CucumberMessageFactory.ToMeta(featureStartedEvent); - Gherkin.CucumberMessages.Types.Source gherkinSource = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source; - Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); - yield return Envelope.Create(messageSource); + //Gherkin.CucumberMessages.Types.Source gherkinSource = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source; + //Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); + yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; - GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - yield return Envelope.Create(gherkinDocument); + //Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; + //GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); + yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - var gherkinPickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles; - var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); + var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles.ToList(); + //var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); string lastID = ExtractLastID(pickles); IDGenerator = IdGeneratorFactory.Create(lastID); diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs index 8ea334765..76b2aa797 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs @@ -14,9 +14,9 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber /// /// once the Gherkin project is updated to directly consume and produce Cucumber messages, this class can be removed /// - internal class CucumberMessageTransformer + public class CucumberMessageTransformer { - internal static Io.Cucumber.Messages.Types.Source ToSource(global::Gherkin.CucumberMessages.Types.Source gherkinSource) + public static Io.Cucumber.Messages.Types.Source ToSource(global::Gherkin.CucumberMessages.Types.Source gherkinSource) { var result = new Io.Cucumber.Messages.Types.Source ( @@ -27,7 +27,7 @@ internal static Io.Cucumber.Messages.Types.Source ToSource(global::Gherkin.Cucum return result; } - internal static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(global::Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) + public static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(global::Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) { var result = new Io.Cucumber.Messages.Types.GherkinDocument ( @@ -250,7 +250,7 @@ private static Io.Cucumber.Messages.Types.DataTable ToDataTable(global::Gherkin. ); } - internal static List ToPickles(IEnumerable pickles) + public static List ToPickles(IEnumerable pickles) { return pickles.Select(ToPickle).ToList(); } diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs index 04e192f3e..2113749ec 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs @@ -14,7 +14,7 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber /// public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor { - private void Accept(object message) + public void Accept(object message) { if (message != null) CucumberMessageVisitor.Accept(this, message); } @@ -36,6 +36,13 @@ public virtual void Visit(GherkinDocument gherkinDocument) { OnVisiting(gherkinDocument); + if (gherkinDocument.Comments != null) + { + foreach (var comment in gherkinDocument.Comments) + { + Accept(comment); + } + } if (gherkinDocument.Feature != null) Accept(gherkinDocument.Feature); @@ -45,6 +52,11 @@ public virtual void Visit(GherkinDocument gherkinDocument) public virtual void Visit(Feature feature) { OnVisiting(feature); + Accept(feature.Location); + foreach (var tag in feature.Tags ?? new List()) + { + Accept(tag); + } foreach (var featureChild in feature.Children ?? new List()) { Accept(featureChild); @@ -67,6 +79,7 @@ public virtual void Visit(FeatureChild featureChild) public virtual void Visit(Rule rule) { OnVisiting(rule); + Accept(rule.Location); foreach (var ruleChild in rule.Children ?? new List()) { Accept(ruleChild); diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs index 6792ec08e..01c69ef7d 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs @@ -1,10 +1,12 @@ using Gherkin.CucumberMessages; -using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.CucumberMessages.RuntimeSupport; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin @@ -16,9 +18,9 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin /// with that which is configured to be used during TEST execution. /// While it's not likely they would be different, it's possible. /// - /// If they are possible, we use a visitor pattern to re-write the IDs to the test-time chosen style. + /// If they are different, we use a visitor pattern to re-write the IDs to the run-time chosen style. /// - internal class GherkinDocumentIDStyleReWriter : GherkinTypesGherkinDocumentVisitor + internal class GherkinDocumentIDStyleReWriter : CucumberMessage_TraversalVisitorBase { private IIdGenerator _idGenerator; public Dictionary IdMap = new(); @@ -32,7 +34,7 @@ public GherkinDocument ReWriteIds(GherkinDocument document, IDGenerationStyle ta _idGenerator = IdGeneratorFactory.Create(targetStyle); - AcceptDocument(document); + Visit(document); return document; } @@ -62,80 +64,98 @@ private IDGenerationStyle ParseStyle(string id) return IDGenerationStyle.Incrementing; } - protected override void OnTagVisited(Tag tag) + public override void OnVisited(Tag tag) { - base.OnTagVisited(tag); + base.OnVisited(tag); var oldId = tag.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - tag.Id = newId; - } - protected override void OnScenarioOutlineVisited(Scenario scenarioOutline) - { - base.OnScenarioOutlineVisited(scenarioOutline); - var oldId = scenarioOutline.Id; - var newId = _idGenerator.GetNewId(); - IdMap[oldId] = newId; - scenarioOutline.Id = newId; + //tag.Id = newId; + SetPrivateProperty(tag, "Id", newId); } - protected override void OnScenarioVisited(Scenario scenario) + public override void OnVisited(Scenario scenario) { - base.OnScenarioVisited(scenario); + base.OnVisited(scenario); var oldId = scenario.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - scenario.Id = newId; + //scenario.Id = newId; + + SetPrivateProperty(scenario, "Id", newId); } - protected override void OnRuleVisited(Rule rule) + public override void OnVisited(Rule rule) { - base.OnRuleVisited(rule); + base.OnVisited(rule); var oldId = rule.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - rule.Id = newId; + //rule.Id = newId; + SetPrivateProperty(rule, "Id", newId); } - protected override void OnBackgroundVisited(Background background) + public override void OnVisited(Background background) { - base.OnBackgroundVisited(background); + base.OnVisited(background); var oldId = background.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - background.Id = newId; + //background.Id = newId; + + SetPrivateProperty(background, "Id", newId); } - protected override void OnStepVisited(Step step) + public override void OnVisited(Step step) { - base.OnStepVisited(step); + base.OnVisited(step); var oldId = step.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - step.Id = newId; + //step.Id = newId; + + SetPrivateProperty(step, "Id", newId); } - protected override void OnExamplesVisited(Examples examples) + public override void OnVisited(Examples examples) { - base.OnExamplesVisited(examples); + base.OnVisited(examples); var oldId = examples.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - examples.Id = newId; + //examples.Id = newId; + + SetPrivateProperty(examples, "Id", newId); } - protected override void OnTableHeaderVisited(TableRow header) + public override void OnVisited(TableRow row) { - base.OnTableHeaderVisited(header); - var oldId = header.Id; + base.OnVisited(row); + var oldId = row.Id; var newId = _idGenerator.GetNewId(); IdMap[oldId] = newId; - header.Id = newId; + //row.Id = newId; + + SetPrivateProperty(row, "Id", newId); } - protected override void OnTableRowVisited(TableRow row) + public static void SetPrivateProperty(T instance, string propertyName, string newValue) { - base.OnTableRowVisited(row); - var oldId = row.Id; - var newId = _idGenerator.GetNewId(); - IdMap[oldId] = newId; - row.Id = newId; + // Get the PropertyInfo object for the property + PropertyInfo propInfo = typeof(T).GetProperty(propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (propInfo == null) + { + throw new ArgumentException($"Property {propertyName} not found."); + } + + // Get the SetMethod (setter) of the property + MethodInfo setMethod = propInfo.GetSetMethod(true); + + if (setMethod == null) + { + throw new ArgumentException($"Property {propertyName} does not have a setter."); + } + + // Invoke the setter method to set the new value + setMethod.Invoke(instance, new object[] { newValue }); } } } diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs index 8f5833107..7dd45ac2f 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs @@ -1,10 +1,12 @@ using Gherkin.CucumberMessages; -using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Reqnroll.CucumberMessages.RuntimeSupport; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Xml.Linq; @@ -14,7 +16,7 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin /// If ID rewriting is required (see cref="GherkinDocumentIDStyleReWriter"), /// this class will re-write the IDs in the given s. /// - internal class PickleIDStyleReWriter : GherkinTypesPickleVisitor + internal class PickleIDStyleReWriter : CucumberMessage_TraversalVisitorBase { private Dictionary _idMap; private IEnumerable _originalPickles; @@ -38,7 +40,7 @@ public IEnumerable ReWriteIds(IEnumerable pickles, Dictionary _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList().AsReadOnly(); + SetPrivateProperty(pickle, "Id", newId); //pickle.Id = newId; + var mappedAstIds = pickle.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList(); + pickle.AstNodeIds.Clear(); + pickle.AstNodeIds.AddRange(mappedAstIds); } - protected override void OnVisitedPickleStep(PickleStep step) + public override void OnVisited(PickleStep step) { - base.OnVisitedPickleStep(step); + base.OnVisited(step); if (_idMap.TryGetValue(step.Id, out var newId)) - step.Id = newId; - step.AstNodeIds = step.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList().AsReadOnly(); + SetPrivateProperty(step, "Id", newId); //step.Id = newId; + var mappedAstIds = step.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList(); + step.AstNodeIds.Clear(); + step.AstNodeIds.AddRange(mappedAstIds); } - protected override void OnVisitedPickleTag(PickleTag tag) + public override void OnVisited(PickleTag tag) { - base.OnVisitedPickleTag(tag); + base.OnVisited(tag); if (_idMap.TryGetValue(tag.AstNodeId, out var newId)) - tag.AstNodeId = newId; + SetPrivateProperty(tag, "AstNodeId", newId); //tag.AstNodeId = newId; } + + public static void SetPrivateProperty(T instance, string propertyName, string newValue) + { + // Get the PropertyInfo object for the property + PropertyInfo propInfo = typeof(T).GetProperty(propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (propInfo == null) + { + throw new ArgumentException($"Property {propertyName} not found."); + } + + // Get the SetMethod (setter) of the property + MethodInfo setMethod = propInfo.GetSetMethod(true); + + if (setMethod == null) + { + throw new ArgumentException($"Property {propertyName} does not have a setter."); + } + + // Invoke the setter method to set the new value + setMethod.Invoke(instance, new object[] { newValue }); + } + } } diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index ee3b7c040..4021d6471 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -4,7 +4,6 @@ using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; using System.Collections.Concurrent; -using System.Diagnostics; using System; using System.Collections.Generic; using System.Linq; diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 582e8eb24..29eaab18a 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -16,7 +16,6 @@ using System.Linq; using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; -using System.Diagnostics; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PubSub; using Reqnroll.CucumberMessages.PayloadProcessing; diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 161aedf82..ff79f8f1d 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -1,6 +1,7 @@ -using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PayloadProcessing.Gherkin; +using System; using System.Collections.Generic; using System.Text.Json; @@ -12,23 +13,26 @@ namespace Reqnroll.CucumberMessages.RuntimeSupport /// public class FeatureLevelCucumberMessages { - public FeatureLevelCucumberMessages(string serializedSourceMessage, string serializedGherkinDocument, string serializedPickles, string location) + public FeatureLevelCucumberMessages(Func source, Func gherkinDocument, Func> pickles, string location) { - Source = JsonSerializer.Deserialize(serializedSourceMessage); - var gherkinDocument = System.Text.Json.JsonSerializer.Deserialize(serializedGherkinDocument); - var pickles = JsonSerializer.Deserialize>(serializedPickles); - ReWriteIds(gherkinDocument, pickles, out var newGherkinDocument, out var newPickles); - - GherkinDocument = newGherkinDocument; - Pickles = newPickles; - Location = location; + if (CucumberConfiguration.Current.Enabled) + { + Source = source(); + + ReWriteIds(gherkinDocument(), pickles(), out var newGherkinDocument, out var newPickles); + + GherkinDocument = newGherkinDocument; + Pickles = newPickles; + Location = location; + } PickleJar = new PickleJar(Pickles); + } public string Location { get; } - public Gherkin.CucumberMessages.Types.Source Source { get; } - public Gherkin.CucumberMessages.Types.GherkinDocument GherkinDocument { get; } - public IEnumerable Pickles { get; } + public Io.Cucumber.Messages.Types.Source Source { get; } + public Io.Cucumber.Messages.Types.GherkinDocument GherkinDocument { get; } + public IEnumerable Pickles { get; } public PickleJar PickleJar { get; } private void ReWriteIds(GherkinDocument gherkinDocument, IEnumerable pickles, out GherkinDocument newGherkinDocument, out IEnumerable newPickles) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index 266136205..3d441fe64 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -1,4 +1,4 @@ -using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; using System; using System.Collections.Generic; using System.Linq; @@ -39,7 +39,7 @@ public string CurrentPickleId public PickleStepSequence PickleStepSequenceFor(string pickleIndex) { - return new PickleStepSequence(HasPickles, Pickles.ElementAt(int.Parse(pickleIndex))); + return new PickleStepSequence(HasPickles, HasPickles ? Pickles.ElementAt(int.Parse(pickleIndex)): null); } public void NextPickle() diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index 7fc97dfc7..c5066ce54 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -34,7 +34,7 @@ protected void SetCucumberMessagesOutputFileName(string fileName) protected void DisableCucumberMessages() { - Environment.SetEnvironmentVariable("REQNROLL_CUCUMBER_MESSAGES_ENABLED", "false"); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE, "false"); } protected void ResetCucumberMessages(string? fileToDelete = null) From 91ab8578d3e21ead7557083f976ff73e86fcf120 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:28:53 -0500 Subject: [PATCH 180/218] Publishes only a single TestRunStartedMessage per run (not per Feature). --- .../ExecutionTracking/FeatureTracker.cs | 2 -- .../Cucumber/CucumberMessageFactory.cs | 4 +-- .../PubSub/CucumberMessagePublisher.cs | 34 +++++++++++++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 8d85aaeaf..431ff0f4e 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -116,8 +116,6 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature if (!StepDefinitionsByPattern.ContainsKey(hookId)) StepDefinitionsByPattern.Add(hookId, hook.Id); yield return Envelope.Create(hook); } - - yield return Envelope.Create(CucumberMessageFactory.ToTestRunStarted(featureStartedEvent)); } // This method is used to identify the last ID generated from the set generated during code gen. diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs index bee3e3dfa..a3b96570a 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -27,9 +27,9 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber /// internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(FeatureStartedEvent featureStartedEvent) + public static TestRunStarted ToTestRunStarted(DateTime timestamp) { - return new TestRunStarted(Converters.ToTimestamp(featureStartedEvent.Timestamp)); + return new TestRunStarted(Converters.ToTimestamp(timestamp)); } public static TestRunFinished ToTestRunFinished(bool testRunStatus, FeatureFinishedEvent testRunFinishedEvent) diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 4021d6471..c2bc2f37d 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -9,6 +9,7 @@ using System.Linq; using Reqnroll.CucumberMessages.ExecutionTracking; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Io.Cucumber.Messages.Types; namespace Reqnroll.CucumberMessages.PubSub { @@ -36,13 +37,18 @@ public CucumberMessagePublisher() } public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) { - runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => { - objectContainer = args.ObjectContainer; - _brokerFactory = new Lazy(() => objectContainer.Resolve()); - var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); - HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + var pluginLifecycleEvents = args.ObjectContainer.Resolve(); + pluginLifecycleEvents.BeforeTestRun += PublisherStartup; }; + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + objectContainer = args.ObjectContainer; + _brokerFactory = new Lazy(() => objectContainer.Resolve()); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); + }; } public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) @@ -57,7 +63,23 @@ public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventP testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); testThreadEventPublisher.AddHandler(OutputAddedEventHandler); - + + } + + // This method will get called after TestRunStartedEvent has been published and after any BeforeTestRun hooks have been called + // The TestRunStartedEvent will be used by the FileOutputPlugin to launch the File writing thread and establish Messages configuration + // Running this after the BeforeTestRun hooks will allow them to programmatically configure CucumberMessages + private void PublisherStartup(object sender, RuntimePluginBeforeTestRunEventArgs args) + { + _broker = _brokerFactory.Value; + + Enabled = _broker.Enabled; + + if (!Enabled) + { + return; + } + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now)) }); } private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) From 19dea3ce7813ae3d7ca03c3eea30f42399af1453 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:32:53 -0500 Subject: [PATCH 181/218] Modifications to make generation of events in a multi-Feature test assembly compliant with Cucumber Messages expectations. --- .../ExecutionTracking/FeatureTracker.cs | 161 +++++++++++++++--- .../TestCaseCucumberMessageTracker.cs | 9 +- .../Cucumber/CucumberMessageFactory.cs | 18 +- ...Writer.cs => GherkinDocumentIDReWriter.cs} | 57 ++++++- ...IDStyleReWriter.cs => PickleIDReWriter.cs} | 32 ++-- .../PubSub/CucumberMessagePublisher.cs | 117 +++++++------ .../FeatureLevelCucumberMessages.cs | 19 +-- .../RuntimeSupport/IdGeneratorFactory.cs | 2 +- .../RuntimeSupport/IdReWriter.cs | 22 +++ .../SeedableIncrementingIdGenerator.cs | 7 + 10 files changed, 315 insertions(+), 129 deletions(-) rename Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/{GherkinDocumentIDStyleReWriter.cs => GherkinDocumentIDReWriter.cs} (70%) rename Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/{PickleIDStyleReWriter.cs => PickleIDReWriter.cs} (76%) create mode 100644 Reqnroll/CucumberMessages/RuntimeSupport/IdReWriter.cs diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 431ff0f4e..60281611b 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -2,11 +2,10 @@ using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; -using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; @@ -21,7 +20,8 @@ public class FeatureTracker // Static Messages are those generated during code generation (Source, GherkinDocument & Pickles) // and the StepTransformations, StepDefinitions and Hook messages which are global to the entire Solution. internal IEnumerable StaticMessages => _staticMessages.Value; - private Lazy> _staticMessages; + private Lazy> _staticMessages; + // ID Generator to use when generating IDs for TestCase messages and beyond // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. @@ -29,23 +29,36 @@ public class FeatureTracker // This dictionary tracks the StepDefintions(ID) by their method signature // used during TestCase creation to map from a Step Definition binding to its ID - internal Dictionary StepDefinitionsByPattern = new(); + // This dictionary is shared across all Features (via the Publisher) + // The same is true of hte StepTransformations and StepDefinitionBindings used for Undefined Parameter Types + internal ConcurrentDictionary StepDefinitionsByPattern = new(); + private ConcurrentBag StepTransformRegistry; + private ConcurrentBag UndefinedParameterTypes; + + // This dictionary maps from (string) PickkleID to the TestCase tracker + private ConcurrentDictionary testCaseTrackersById = new(); + public string FeatureName { get; set; } public bool Enabled { get; private set; } // This dictionary maps from (string) PickleIDIndex to (string) PickleID public Dictionary PickleIds { get; } = new(); + public bool FeatureExecutionSuccess { get; private set; } // This constructor is used by the Publisher when it sees a Feature (by name) for the first time - public FeatureTracker(FeatureStartedEvent featureStartedEvent) + public FeatureTracker(FeatureStartedEvent featureStartedEvent, IIdGenerator idGenerator, ConcurrentDictionary stepDefinitionPatterns, ConcurrentBag stepTransformRegistry, ConcurrentBag undefinedParameterTypes) { + StepDefinitionsByPattern = stepDefinitionPatterns; + StepTransformRegistry = stepTransformRegistry; + UndefinedParameterTypes = undefinedParameterTypes; + IDGenerator = idGenerator; FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; var featureHasCucumberMessages = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages != null; Enabled = featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null ? true : false; - PreProcessEvent(featureStartedEvent); + ProcessEvent(featureStartedEvent); } - internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) + internal void ProcessEvent(FeatureStartedEvent featureStartedEvent) { if (!Enabled) return; // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow @@ -53,29 +66,23 @@ internal void PreProcessEvent(FeatureStartedEvent featureStartedEvent) } private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) { - yield return CucumberMessageFactory.ToMeta(featureStartedEvent); - //Gherkin.CucumberMessages.Types.Source gherkinSource = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source; - //Source messageSource = CucumberMessageTransformer.ToSource(gherkinSource); yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); - - //Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; - //GherkinDocument gherkinDocument = CucumberMessageTransformer.ToGherkinDocument(gherkinDoc); - yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument); - + var gd = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles.ToList(); - //var pickles = CucumberMessageTransformer.ToPickles(gherkinPickles); - string lastID = ExtractLastID(pickles); - IDGenerator = IdGeneratorFactory.Create(lastID); + var idReWriter = new CucumberMessages.RuntimeSupport.IdReWriter(); + idReWriter.ReWriteIds(gd, pickles, IDGenerator, out var reWrittenGherkinDocument, out var reWrittenPickles); + gd = reWrittenGherkinDocument; + pickles = reWrittenPickles.ToList(); - for(int i = 0; i < pickles.Count; i++) + for (int i = 0; i < pickles.Count; i++) { PickleIds.Add(i.ToString(), pickles[i].Id); } - + yield return Envelope.Create(gd); foreach (var pickle in pickles) { yield return Envelope.Create(pickle); @@ -85,6 +92,9 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature foreach (var stepTransform in bindingRegistry.GetStepTransformations()) { + if (StepTransformRegistry.Contains(stepTransform)) + continue; + StepTransformRegistry.Add(stepTransform); var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); yield return Envelope.Create(parameterType); } @@ -95,6 +105,9 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature if (errmsg.Contains("Undefined parameter type")) { var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + if (UndefinedParameterTypes.Contains(binding)) + continue; + UndefinedParameterTypes.Add(binding); var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); yield return Envelope.Create(undefinedParameterType); } @@ -102,19 +115,26 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) { - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - if (!StepDefinitionsByPattern.ContainsKey(pattern)) StepDefinitionsByPattern.Add(pattern, stepDefinition.Id); - - yield return Envelope.Create(stepDefinition); + if (StepDefinitionsByPattern.ContainsKey(pattern)) + continue; + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); + if (StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id)) + { + yield return Envelope.Create(stepDefinition); + } } foreach (var hookBinding in bindingRegistry.GetHooks()) { - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - if (!StepDefinitionsByPattern.ContainsKey(hookId)) StepDefinitionsByPattern.Add(hookId, hook.Id); - yield return Envelope.Create(hook); + if (StepDefinitionsByPattern.ContainsKey(hookId)) + continue; + var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); + if (StepDefinitionsByPattern.TryAdd(hookId, hook.Id)) + { + yield return Envelope.Create(hook); + }; } } @@ -129,5 +149,92 @@ private string ExtractLastID(List pickles) return pickles.Last().Id; } + // When the FeatureFinished event fires, we calculate the Feature-level Execution Status + public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + var testCases = testCaseTrackersById.Values.ToList(); + + // Calculate the Feature-level Execution Status + FeatureExecutionSuccess = testCases.All(tc => tc.Finished) switch + { + true => testCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK), + _ => true + }; + } + + public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var pickleId = PickleIds[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var tccmt = new TestCaseCucumberMessageTracker(this, pickleId); + tccmt.ProcessEvent(scenarioStartedEvent); + testCaseTrackersById.TryAdd(pickleId, tccmt); + } + + public IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + var pickleId = PickleIds[scenarioFinishedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(scenarioFinishedEvent); + + return tccmt.TestCaseCucumberMessages(); + } + return Enumerable.Empty(); + } + + public void ProcessEvent(StepStartedEvent stepStartedEvent) + { + var pickleId = PickleIds[stepStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(stepStartedEvent); + } + } + public void ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var pickleId = PickleIds[stepFinishedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(stepFinishedEvent); + } + } + + public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + var pickleId = PickleIds[hookBindingStartedEvent.ContextManager.ScenarioContext.ScenarioInfo.PickleIdIndex]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent(hookBindingStartedEvent); + } + public void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + var pickleId = PickleIds[hookBindingFinishedEvent.ContextManager.ScenarioContext.ScenarioInfo.PickleIdIndex]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent(hookBindingFinishedEvent); + } + public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var pickleId = PickleIds[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(attachmentAddedEvent); + } + } + + public void ProcessEvent(OutputAddedEvent outputAddedEvent) + { + var pickleId = PickleIds[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(outputAddedEvent); + } + + } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs index dd3fef169..66082dcb7 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs @@ -18,8 +18,9 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking /// public class TestCaseCucumberMessageTracker { - public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) + public TestCaseCucumberMessageTracker(FeatureTracker featureTracker, string pickleId) { + PickleId = pickleId; FeatureName = featureTracker.FeatureName; Enabled = featureTracker.Enabled; IDGenerator = featureTracker.IDGenerator; @@ -30,7 +31,6 @@ public TestCaseCucumberMessageTracker(FeatureTracker featureTracker) // Feature FeatureName and Pickle ID make up a unique identifier for tracking execution of Test Cases public string FeatureName { get; set; } public string PickleId { get; set; } = string.Empty; - public string TestCaseTrackerId { get { return FeatureName +@"/" + PickleId; } } public string TestCaseId { get; set; } public string TestCaseStartedId { get; private set; } @@ -72,7 +72,7 @@ public List Steps // This dictionary tracks the StepDefintions(ID) by their method signature // used during TestCase creation to map from a Step Definition binding to its ID - internal Dictionary StepDefinitionsByPattern = new(); + internal ConcurrentDictionary StepDefinitionsByPattern ; // Processing of events is handled in two stages. // Stage 1: As events are recieved, critical information needed right away is extracted and stored in the TestCaseCucumberMessageTracker @@ -167,8 +167,7 @@ internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFini internal void PreProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - PickleId = PickleIdList[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; - scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = TestCaseTrackerId; + scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = PickleId; TestCaseId = IDGenerator.GetNewId(); TestCaseStartedId = IDGenerator.GetNewId(); } diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs index a3b96570a..2b91e2986 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -3,6 +3,7 @@ using Io.Cucumber.Messages.Types; using Reqnroll.Analytics; using Reqnroll.Bindings; +using Reqnroll.BoDi; using Reqnroll.CommonModels; using Reqnroll.CucumberMessages.ExecutionTracking; using Reqnroll.CucumberMessages.PayloadProcessing; @@ -15,8 +16,6 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; -using static System.Net.Mime.MediaTypeNames; namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber { @@ -32,9 +31,9 @@ public static TestRunStarted ToTestRunStarted(DateTime timestamp) return new TestRunStarted(Converters.ToTimestamp(timestamp)); } - public static TestRunFinished ToTestRunFinished(bool testRunStatus, FeatureFinishedEvent testRunFinishedEvent) + public static TestRunFinished ToTestRunFinished(bool testRunStatus, DateTime timestamp) { - return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(testRunFinishedEvent.Timestamp), null); + return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(timestamp), null); } internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { @@ -122,7 +121,7 @@ internal static ParameterType ToParameterType(IStepArgumentTransformationBinding private static SourceReference ToSourceRef(IBinding binding) { var methodName = binding.Method.Name; - var className = binding.Method.Type.Name; + var className = binding.Method.Type.AssemblyName + "." + binding.Method.Type.FullName; var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); var methodDescription = new JavaMethod(className, methodName, paramTypes); var sourceRef = SourceReference.Create(methodDescription); @@ -274,11 +273,10 @@ private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStat }; } - public static Envelope ToMeta(FeatureStartedEvent featureStartedEvent) + public static Envelope ToMeta(IObjectContainer container) { - var featureContainer = featureStartedEvent.FeatureContext.FeatureContainer; - var environmentInfoProvider = featureContainer.Resolve(); - var environmentWrapper = featureContainer.Resolve(); + var environmentInfoProvider = container.Resolve(); + var environmentWrapper = container.Resolve(); var implementation = new Product("Reqnroll", environmentInfoProvider.GetReqnrollVersion()); string targetFramework = environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; @@ -349,7 +347,7 @@ public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding st public static string CanonicalizeHookBinding(IHookBinding hookBinding) { string signature = GenerateSignature(hookBinding); - return $"{hookBinding.Method.Type.FullName}.{hookBinding.Method.Name}({signature})"; + return $"{hookBinding.Method.Type.AssemblyName}.{hookBinding.Method.Type.FullName}.{hookBinding.Method.Name}({signature})"; } private static string GenerateSignature(IBinding stepDefinition) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs similarity index 70% rename from Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs index 01c69ef7d..087b87874 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs @@ -19,25 +19,72 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin /// While it's not likely they would be different, it's possible. /// /// If they are different, we use a visitor pattern to re-write the IDs to the run-time chosen style. + /// + /// If the styles are the same AND we're using an incrementing generator, then we need to regenerate with the next series of IDs. + /// /// - internal class GherkinDocumentIDStyleReWriter : CucumberMessage_TraversalVisitorBase + internal class GherkinDocumentIDReWriter : CucumberMessage_TraversalVisitorBase { private IIdGenerator _idGenerator; public Dictionary IdMap = new(); - + public GherkinDocumentIDReWriter(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } public GherkinDocument ReWriteIds(GherkinDocument document, IDGenerationStyle targetStyle) { var existingIdStyle = ProbeForIdGenerationStyle(document); - if (existingIdStyle == targetStyle) + if (targetStyle == IDGenerationStyle.Incrementing) + { + switch ((((SeedableIncrementingIdGenerator)_idGenerator).HasBeenUsed, existingIdStyle)) + { + case (true, IDGenerationStyle.Incrementing): + case (true, IDGenerationStyle.UUID): + case (false, IDGenerationStyle.UUID): + return ReWrite(document); + + case (false, IDGenerationStyle.Incrementing): + var lastId = ProbeForLastUsedId(document); + ((SeedableIncrementingIdGenerator)_idGenerator).SetSeed(lastId); + return document; + } + } + // else targetStyle is IDGenerationStyle.UUID + if (existingIdStyle == IDGenerationStyle.UUID) return document; - _idGenerator = IdGeneratorFactory.Create(targetStyle); + // else existingIdStyle is IDGenerationStyle.Incrementing + return ReWrite(document); + } + private GherkinDocument ReWrite(GherkinDocument document) + { Visit(document); return document; } + private int ProbeForLastUsedId(GherkinDocument document) + { + if (document.Feature == null) return 0; + + var child = document.Feature.Children.LastOrDefault(); + var tags = document.Feature.Tags; + var highestTagId = tags.Count > 0 ? tags.Max(t => int.Parse(t.Id)) : 0; + + if (child == null) return highestTagId; + + if (child.Rule != null) + highestTagId = Math.Max(highestTagId, int.Parse(child.Rule.Id)); + + if (child.Background != null) + highestTagId = Math.Max(highestTagId, int.Parse(child.Background.Id)); + + if (child.Scenario != null) + highestTagId = Math.Max(highestTagId, int.Parse(child.Scenario.Id)); + + return highestTagId; + } private IDGenerationStyle ProbeForIdGenerationStyle(GherkinDocument document) { if (document.Feature == null) return IDGenerationStyle.UUID; @@ -58,7 +105,7 @@ private IDGenerationStyle ProbeForIdGenerationStyle(GherkinDocument document) private IDGenerationStyle ParseStyle(string id) { - if (Guid.TryParse(id, out var _)) + if (Guid.TryParse(id, out var _)) return IDGenerationStyle.UUID; return IDGenerationStyle.Incrementing; diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDReWriter.cs similarity index 76% rename from Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs rename to Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDReWriter.cs index 7dd45ac2f..b1f3bfe5c 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDStyleReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/PickleIDReWriter.cs @@ -13,31 +13,35 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin { /// - /// If ID rewriting is required (see cref="GherkinDocumentIDStyleReWriter"), + /// If ID rewriting is required (see cref="GherkinDocumentIDReWriter"), /// this class will re-write the IDs in the given s. /// - internal class PickleIDStyleReWriter : CucumberMessage_TraversalVisitorBase + internal class PickleIDReWriter : CucumberMessage_TraversalVisitorBase { private Dictionary _idMap; private IEnumerable _originalPickles; - private IDGenerationStyle _idStyle; + private IDGenerationStyle _targetIdStyle; + private IDGenerationStyle _existingIdStyle; private IIdGenerator _idGenerator; + public PickleIDReWriter(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } + public IEnumerable ReWriteIds(IEnumerable pickles, Dictionary idMap, IDGenerationStyle targetStyle) { if (pickles == null || pickles.Count() == 0) return pickles; _idMap = idMap; _originalPickles = pickles; - _idStyle = targetStyle; - var existingIdStyle = ProbeForIdGenerationStyle(pickles.First()); + _targetIdStyle = targetStyle; + _existingIdStyle = ProbeForIdGenerationStyle(pickles.First()); - if (existingIdStyle == targetStyle) + if (_existingIdStyle == IDGenerationStyle.UUID && targetStyle == IDGenerationStyle.UUID) return pickles; - _idGenerator = IdGeneratorFactory.Create(targetStyle); - - + //re-write the IDs (either int->UUID or UUID->int or int->int starting at a new seed) foreach (var pickle in _originalPickles) { Accept(pickle); @@ -58,8 +62,11 @@ public override void OnVisited(Pickle pickle) { base.OnVisited(pickle); - if (_idMap.TryGetValue(pickle.Id, out var newId)) - SetPrivateProperty(pickle, "Id", newId); //pickle.Id = newId; + + SetPrivateProperty(pickle, "Id", _idGenerator.GetNewId()); //pickle.Id = newId; + + // if the AstNodeIds are in the idMap, that means they were rewrittten by the GerkinDocumentIDReWriter + // otherwise, we can continue to use the ID we already have var mappedAstIds = pickle.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList(); pickle.AstNodeIds.Clear(); pickle.AstNodeIds.AddRange(mappedAstIds); @@ -68,8 +75,7 @@ public override void OnVisited(Pickle pickle) public override void OnVisited(PickleStep step) { base.OnVisited(step); - if (_idMap.TryGetValue(step.Id, out var newId)) - SetPrivateProperty(step, "Id", newId); //step.Id = newId; + SetPrivateProperty(step, "Id", _idGenerator.GetNewId()); //step.Id = newId;y var mappedAstIds = step.AstNodeIds.Select(id => _idMap.TryGetValue(id, out var newId2) ? newId2 : id).ToList(); step.AstNodeIds.Clear(); step.AstNodeIds.AddRange(mappedAstIds); diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index c2bc2f37d..d2a9c7acf 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -10,6 +10,10 @@ using Reqnroll.CucumberMessages.ExecutionTracking; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.RuntimeSupport; +using Reqnroll.CucumberMessages.Configuration; +using Gherkin.CucumberMessages; +using Reqnroll.Bindings; namespace Reqnroll.CucumberMessages.PubSub { @@ -29,7 +33,16 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi // Started Features by name private ConcurrentDictionary StartedFeatures = new(); - private ConcurrentDictionary testCaseTrackersById = new(); + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + // shared to each Feature tracker so that we keep a single list + internal ConcurrentDictionary StepDefinitionsByPattern = new(); + private ConcurrentBag StepArgumentTransforms = new(); + private ConcurrentBag UndefinedParameterTypeBindings = new(); + public IIdGenerator SharedIDGenerator { get; private set; } + + bool Enabled = false; public CucumberMessagePublisher() @@ -41,6 +54,7 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar { var pluginLifecycleEvents = args.ObjectContainer.Resolve(); pluginLifecycleEvents.BeforeTestRun += PublisherStartup; + pluginLifecycleEvents.AfterTestRun += PublisherTestRunComplete; }; runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { @@ -79,9 +93,21 @@ private void PublisherStartup(object sender, RuntimePluginBeforeTestRunEventArgs { return; } + + SharedIDGenerator = IdGeneratorFactory.Create(CucumberConfiguration.Current.IDGenerationStyle); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now)) }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = CucumberMessageFactory.ToMeta(args.ObjectContainer) }); + } + private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEventArgs e) + { + if (!Enabled) + return; + var status = StartedFeatures.Values.All(f => f.FeatureExecutionSuccess); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) }); } + private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { _broker = _brokerFactory.Value; @@ -100,7 +126,7 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) return; } - var ft = new FeatureTracker(featureStartedEvent); + var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) @@ -123,26 +149,8 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve { return; } - var featureTestCases = testCaseTrackersById.Values.Where(tc => tc.FeatureName == featureName).ToList(); - - // IF all TestCaseCucumberMessageTrackers are done, then send the messages to the CucumberMessageBroker - if (featureTestCases.All(tc => tc.Finished)) - { - var testRunStatus = featureTestCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK); - var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(testRunStatus, featureFinishedEvent)); - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - } - else - { - // If the feature has no steps, then we should send the finished message; - if (featureTestCases.Count == 0) - { - var msg = Io.Cucumber.Messages.Types.Envelope.Create(CucumberMessageFactory.ToTestRunFinished(true, featureFinishedEvent)); - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - } - - // else is an error of a nonsensical state; should we throw an exception? - } + var featureTracker = StartedFeatures[featureName]; + featureTracker.ProcessEvent(featureFinishedEvent); // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? @@ -158,11 +166,7 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve { if (featureTracker.Enabled) { - var pickleId = featureTracker.PickleIds[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; - var id = featureName + @"/" + pickleId; - var tccmt = new TestCaseCucumberMessageTracker(featureTracker); - tccmt.ProcessEvent(scenarioStartedEvent); - testCaseTrackersById.TryAdd(id, tccmt); + featureTracker.ProcessEvent(scenarioStartedEvent); } else { @@ -180,14 +184,12 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished { if (!Enabled) return; - var testCaseTrackerId = scenarioFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) + var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - tccmt.ProcessEvent(scenarioFinishedEvent); - - foreach (var msg in tccmt.TestCaseCucumberMessages()) + foreach (var msg in featureTracker.ProcessEvent(scenarioFinishedEvent)) { - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = tccmt.FeatureName, Envelope = msg }); + _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } @@ -196,9 +198,12 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { if (!Enabled) return; - var testCaseTrackerId = stepStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(stepStartedEvent); + + var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(stepStartedEvent); + } } private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) @@ -206,9 +211,11 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (!Enabled) return; - var testCaseTrackerId = stepFinishedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(stepFinishedEvent); + var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(stepFinishedEvent); + } } private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) @@ -216,9 +223,11 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (!Enabled) return; - var testCaseTrackerId = hookBindingStartedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(hookBindingStartedEvent); + var featureName = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(hookBindingStartedEvent); + } } private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) @@ -226,9 +235,11 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (!Enabled) return; - var testCaseTrackerId = hookBindingFinishedEvent.ContextManager.FeatureContext?.FeatureInfo?.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(hookBindingFinishedEvent); + var featureName = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(hookBindingFinishedEvent); + } } private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) @@ -236,9 +247,11 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (!Enabled) return; - var testCaseTrackerId = attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(attachmentAddedEvent); + var featureName = attachmentAddedEvent.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(attachmentAddedEvent); + } } private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) @@ -246,9 +259,11 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (!Enabled) return; - var testCaseTrackerId = outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId; - if (testCaseTrackerId != null && testCaseTrackersById.TryGetValue(testCaseTrackerId, out var tccmt)) - tccmt.ProcessEvent(outputAddedEvent); + var featureName = outputAddedEvent.FeatureInfo.Title; + if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(outputAddedEvent); + } } } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index ff79f8f1d..f49e8f6d5 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -18,11 +18,8 @@ public FeatureLevelCucumberMessages(Func source, Func g if (CucumberConfiguration.Current.Enabled) { Source = source(); - - ReWriteIds(gherkinDocument(), pickles(), out var newGherkinDocument, out var newPickles); - - GherkinDocument = newGherkinDocument; - Pickles = newPickles; + GherkinDocument = gherkinDocument(); + Pickles = pickles(); Location = location; } PickleJar = new PickleJar(Pickles); @@ -34,17 +31,5 @@ public FeatureLevelCucumberMessages(Func source, Func g public Io.Cucumber.Messages.Types.GherkinDocument GherkinDocument { get; } public IEnumerable Pickles { get; } public PickleJar PickleJar { get; } - - private void ReWriteIds(GherkinDocument gherkinDocument, IEnumerable pickles, out GherkinDocument newGherkinDocument, out IEnumerable newPickles) - { - var targetIdStyle = CucumberConfiguration.Current.IDGenerationStyle; - var gherkinDocumentIDStyleReWriter = new GherkinDocumentIDStyleReWriter(); - newGherkinDocument = gherkinDocumentIDStyleReWriter.ReWriteIds(gherkinDocument, targetIdStyle); - var idMap = gherkinDocumentIDStyleReWriter.IdMap; - - var pickleIDStyleReWriter = new PickleIDStyleReWriter(); - newPickles = pickleIDStyleReWriter.ReWriteIds(pickles, idMap, targetIdStyle); - } - } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs index 2eaabb890..46b25c69d 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IdGeneratorFactory.cs @@ -19,7 +19,7 @@ public static IIdGenerator Create(string previousId) public static IIdGenerator Create(IDGenerationStyle style) { return style switch { - IDGenerationStyle.Incrementing => new IncrementingIdGenerator(), + IDGenerationStyle.Incrementing => new SeedableIncrementingIdGenerator(0), IDGenerationStyle.UUID => new GuidIdGenerator(), _ => throw new NotImplementedException() }; } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/IdReWriter.cs b/Reqnroll/CucumberMessages/RuntimeSupport/IdReWriter.cs new file mode 100644 index 000000000..6dc1ba944 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/IdReWriter.cs @@ -0,0 +1,22 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing.Gherkin; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + public class IdReWriter + { + public void ReWriteIds(GherkinDocument gherkinDocument, IEnumerable pickles, IIdGenerator idGenerator, out GherkinDocument newGherkinDocument, out IEnumerable newPickles) + { + var targetIdStyle = CucumberConfiguration.Current.IDGenerationStyle; + var gherkinDocumentIDStyleReWriter = new GherkinDocumentIDReWriter(idGenerator); + newGherkinDocument = gherkinDocumentIDStyleReWriter.ReWriteIds(gherkinDocument, targetIdStyle); + var idMap = gherkinDocumentIDStyleReWriter.IdMap; + + var pickleIDStyleReWriter = new PickleIDReWriter(idGenerator); + newPickles = pickleIDStyleReWriter.ReWriteIds(pickles, idMap, targetIdStyle); + } + } +} diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs b/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs index b4e0cd825..893b16f42 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/SeedableIncrementingIdGenerator.cs @@ -11,6 +11,13 @@ public SeedableIncrementingIdGenerator(int seed) private int _counter = 0; + public void SetSeed(int seed) + { + _counter = seed; + } + + public bool HasBeenUsed { get { return _counter > 0; } } + public string GetNewId() { // Using thread-safe incrementing in case scenarios are running in parallel From abcc181b2f57d108e6334e75285bb7652aa2ae4b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:02:21 -0500 Subject: [PATCH 182/218] Default Config is now OFF. Renamed TestCaseCucumberMessageTracker to simply TestCaseTracker. --- .../Configuration/ConfigurationDTO.cs | 2 +- .../Configuration/CucumberConfiguration.cs | 2 +- .../ExecutionTracking/FeatureTracker.cs | 85 +++++++++++++------ .../ExecutionTracking/HookStepTracker.cs | 2 +- .../StepExecutionTrackerBase.cs | 4 +- ...erMessageTracker.cs => TestCaseTracker.cs} | 8 +- .../ExecutionTracking/TestStepTracker.cs | 2 +- .../Cucumber/CucumberMessageFactory.cs | 14 +-- .../PubSub/CucumberMessagePublisher.cs | 45 +++++----- Reqnroll/FeatureInfo.cs | 2 +- 10 files changed, 104 insertions(+), 62 deletions(-) rename Reqnroll/CucumberMessages/ExecutionTracking/{TestCaseCucumberMessageTracker.cs => TestCaseTracker.cs} (98%) diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs index 6dabf060e..e23f65dee 100644 --- a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -22,7 +22,7 @@ public class ConfigurationDTO public Profile ActiveProfile => Profiles.Where(p => p.ProfileName == ActiveProfileName).FirstOrDefault(); - public ConfigurationDTO() : this(true) { } + public ConfigurationDTO() : this(false) { } public ConfigurationDTO(bool enabled) : this(enabled, "DEFAULT", new List()) { } public ConfigurationDTO(bool enabled, string activeProfile, List profiles) { diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 2eed6dcdb..8681eca79 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -112,7 +112,7 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) result.IDGenerationStyle = IdGenerationStyleEnumConverter.ParseIdGenerationStyle(((Success)idGenStyleValue).Result); var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "TRUE"; + var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "FALSE"; result.Enabled = Convert.ToBoolean(enabled); diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 60281611b..37b5b0aaf 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; @@ -36,7 +37,7 @@ public class FeatureTracker private ConcurrentBag UndefinedParameterTypes; // This dictionary maps from (string) PickkleID to the TestCase tracker - private ConcurrentDictionary testCaseTrackersById = new(); + private ConcurrentDictionary testCaseTrackersById = new(); public string FeatureName { get; set; } public bool Enabled { get; private set; } @@ -92,7 +93,7 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature foreach (var stepTransform in bindingRegistry.GetStepTransformations()) { - if (StepTransformRegistry.Contains(stepTransform)) + if (StepTransformRegistry.Contains(stepTransform)) continue; StepTransformRegistry.Add(stepTransform); var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); @@ -164,61 +165,95 @@ public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - var pickleId = PickleIds[scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; - var tccmt = new TestCaseCucumberMessageTracker(this, pickleId); - tccmt.ProcessEvent(scenarioStartedEvent); - testCaseTrackersById.TryAdd(pickleId, tccmt); + // as in the Publisher, we're using defensive coding here b/c some test setups might not have complete info + var pickleIndex = scenarioStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + var tccmt = new TestCaseTracker(this, pickleId); + tccmt.ProcessEvent(scenarioStartedEvent); + testCaseTrackersById.TryAdd(pickleId, tccmt); + } } public IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { - var pickleId = PickleIds[scenarioFinishedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var pickleIndex = scenarioFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + if (String.IsNullOrEmpty(pickleIndex)) return Enumerable.Empty(); - if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { - tccmt.ProcessEvent(scenarioFinishedEvent); + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(scenarioFinishedEvent); - return tccmt.TestCaseCucumberMessages(); + return tccmt.TestCaseCucumberMessages(); + } } return Enumerable.Empty(); } public void ProcessEvent(StepStartedEvent stepStartedEvent) { - var pickleId = PickleIds[stepStartedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var pickleIndex = stepStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { - tccmt.ProcessEvent(stepStartedEvent); + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(stepStartedEvent); + } } } + public void ProcessEvent(StepFinishedEvent stepFinishedEvent) { - var pickleId = PickleIds[stepFinishedEvent.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var pickleIndex = stepFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + if (String.IsNullOrEmpty(pickleIndex)) return; + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { - tccmt.ProcessEvent(stepFinishedEvent); + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent(stepFinishedEvent); + } } } public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { - var pickleId = PickleIds[hookBindingStartedEvent.ContextManager.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var pickleIndex = hookBindingStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) - tccmt.ProcessEvent(hookBindingStartedEvent); + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent(hookBindingStartedEvent); + } } + public void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - var pickleId = PickleIds[hookBindingFinishedEvent.ContextManager.ScenarioContext.ScenarioInfo.PickleIdIndex]; + var pickleIndex = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) - tccmt.ProcessEvent(hookBindingFinishedEvent); + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent(hookBindingFinishedEvent); + } } + public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { - var pickleId = PickleIds[attachmentAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + var pickleId = attachmentAddedEvent.FeatureInfo?.CucumberMessages_PickleId; + + if (String.IsNullOrEmpty(pickleId)) return; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { @@ -228,7 +263,9 @@ public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) public void ProcessEvent(OutputAddedEvent outputAddedEvent) { - var pickleId = PickleIds[outputAddedEvent.FeatureInfo.CucumberMessages_TestCaseTrackerId]; + var pickleId = outputAddedEvent.FeatureInfo?.CucumberMessages_PickleId; + + if (String.IsNullOrEmpty(pickleId)) return; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs index 5fdb2a203..2e6e3667d 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs @@ -14,7 +14,7 @@ public class HookStepTracker : StepExecutionTrackerBase { public string HookBindingSignature { get; private set; } public HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } - public HookStepTracker(TestCaseCucumberMessageTracker tracker) : base(tracker) + public HookStepTracker(TestCaseTracker tracker) : base(tracker) { } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs index b962212c6..1f2935798 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs @@ -13,9 +13,9 @@ public class StepExecutionTrackerBase : IStepTracker public TimeSpan Duration { get; set; } public Exception Exception { get; set; } - public TestCaseCucumberMessageTracker ParentTestCase; + public TestCaseTracker ParentTestCase; - public StepExecutionTrackerBase(TestCaseCucumberMessageTracker parentScenario) + public StepExecutionTrackerBase(TestCaseTracker parentScenario) { ParentTestCase = parentScenario; } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs similarity index 98% rename from Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs rename to Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs index 66082dcb7..4ed9fee5e 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseCucumberMessageTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs @@ -16,9 +16,9 @@ namespace Reqnroll.CucumberMessages.ExecutionTracking /// This class is used to track the execution of Test Cases /// There will be one instance of this class per gherkin Pickle/TestCase. It will track info from both Feature-level and Scenario-level Execution Events for a single Test Case /// - public class TestCaseCucumberMessageTracker + public class TestCaseTracker { - public TestCaseCucumberMessageTracker(FeatureTracker featureTracker, string pickleId) + public TestCaseTracker(FeatureTracker featureTracker, string pickleId) { PickleId = pickleId; FeatureName = featureTracker.FeatureName; @@ -75,7 +75,7 @@ public List Steps internal ConcurrentDictionary StepDefinitionsByPattern ; // Processing of events is handled in two stages. - // Stage 1: As events are recieved, critical information needed right away is extracted and stored in the TestCaseCucumberMessageTracker + // Stage 1: As events are recieved, critical information needed right away is extracted and stored in the TestCaseTracker // The event is then stored in a queue for processing in stage 2 // Stage 2: When TestRunFinished is recieved, the messages are processed to generate Cucumber Messages and then sent in a single batch to the broker internal void ProcessEvent(ExecutionEvent anEvent) @@ -167,7 +167,7 @@ internal IEnumerable PostProcessEvent(FeatureFinishedEvent featureFini internal void PreProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_TestCaseTrackerId = PickleId; + scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_PickleId = PickleId; TestCaseId = IDGenerator.GetNewId(); TestCaseStartedId = IDGenerator.GetNewId(); } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs index 5c2f81f8e..d53b81d69 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs @@ -23,7 +23,7 @@ public class TestStepTracker : StepExecutionTrackerBase { private StepStartedEvent stepStartedEvent; - public TestStepTracker(TestCaseCucumberMessageTracker parentTracker) : base(parentTracker) + public TestStepTracker(TestCaseTracker parentTracker) : base(parentTracker) { } diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs index 2b91e2986..520829d8a 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -35,7 +35,7 @@ public static TestRunFinished ToTestRunFinished(bool testRunStatus, DateTime tim { return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(timestamp), null); } - internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCase ToTestCase(TestCaseTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); @@ -63,11 +63,11 @@ internal static TestCase ToTestCase(TestCaseCucumberMessageTracker testCaseTrack ); return testCase; } - internal static TestCaseStarted ToTestCaseStarted(TestCaseCucumberMessageTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) + internal static TestCaseStarted ToTestCaseStarted(TestCaseTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { return new TestCaseStarted(0, testCaseTracker.TestCaseStartedId, testCaseTracker.TestCaseId, null, Converters.ToTimestamp(scenarioStartedEvent.Timestamp)); } - internal static TestCaseFinished ToTestCaseFinished(TestCaseCucumberMessageTracker testCaseTracker, ScenarioFinishedEvent scenarioFinishedEvent) + internal static TestCaseFinished ToTestCaseFinished(TestCaseTracker testCaseTracker, ScenarioFinishedEvent scenarioFinishedEvent) { return new TestCaseFinished(testCaseTracker.TestCaseStartedId, Converters.ToTimestamp(scenarioFinishedEvent.Timestamp), false); } @@ -128,7 +128,7 @@ private static SourceReference ToSourceRef(IBinding binding) return sourceRef; } - internal static TestStep ToPickleTestStep(TestCaseCucumberMessageTracker tracker, TestStepTracker stepState) + internal static TestStep ToPickleTestStep(TestCaseTracker tracker, TestStepTracker stepState) { bool bound = stepState.Bound; bool ambiguous = stepState.Ambiguous; @@ -212,7 +212,7 @@ internal static TestStepFinished ToTestStepFinished(HookStepTracker hookStepProc return new TestStepFinished(hookStepProcessor.TestCaseStartedID, hookStepProcessor.TestStepID, ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookFinishedEvent.Timestamp)); } - internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, AttachmentAddedEventWrapper attachmentAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseTracker tracker, AttachmentAddedEventWrapper attachmentAddedEventWrapper) { return new Attachment( Base64EncodeFile(attachmentAddedEventWrapper.AttachmentAddedEvent.FilePath), @@ -224,7 +224,7 @@ internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, attachmentAddedEventWrapper.TestCaseStepID, null); } - internal static Attachment ToAttachment(TestCaseCucumberMessageTracker tracker, OutputAddedEventWrapper outputAddedEventWrapper) + internal static Attachment ToAttachment(TestCaseTracker tracker, OutputAddedEventWrapper outputAddedEventWrapper) { return new Attachment( outputAddedEventWrapper.OutputAddedEvent.Text, @@ -341,7 +341,7 @@ public static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding st { string signature = GenerateSignature(stepDefinition); - return $"{stepDefinition.SourceExpression}({signature})"; + return $"{stepDefinition.Method.Type.AssemblyName}.{stepDefinition.Method.Type.FullName}.{stepDefinition.Method.Name}({signature})"; } public static string CanonicalizeHookBinding(IHookBinding hookBinding) diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index d2a9c7acf..9152a3e23 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -112,10 +112,11 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); - var featureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + + var featureName = featureStartedEvent.FeatureContext?.FeatureInfo?.Title; Enabled = _broker.Enabled; - if (!Enabled) + if (!Enabled || String.IsNullOrEmpty(featureName)) { return; } @@ -140,11 +141,13 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { - if (!Enabled) + // For this and subsequent events, we pull up the FeatureTracker by feature name. + // If the feature name is not avaiable (such as might be the case in certain test setups), we ignore the event. + var featureName = featureFinishedEvent.FeatureContext?.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) { return; } - var featureName = featureFinishedEvent.FeatureContext.FeatureInfo.Title; if (!StartedFeatures.ContainsKey(featureName) || !StartedFeatures[featureName].Enabled) { return; @@ -158,10 +161,10 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { - if (!Enabled) + var featureName = scenarioStartedEvent.FeatureContext?.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; var traceListener = objectContainer.Resolve(); - var featureName = scenarioStartedEvent.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { if (featureTracker.Enabled) @@ -182,9 +185,10 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { - if (!Enabled) + var featureName = scenarioFinishedEvent.FeatureContext?.FeatureInfo?.Title; + + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = scenarioFinishedEvent.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { foreach (var msg in featureTracker.ProcessEvent(scenarioFinishedEvent)) @@ -196,10 +200,11 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) { - if (!Enabled) + var featureName = stepStartedEvent.FeatureContext?.FeatureInfo?.Title; + + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = stepStartedEvent.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(stepStartedEvent); @@ -208,10 +213,10 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { - if (!Enabled) + var featureName = stepFinishedEvent.FeatureContext?.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = stepFinishedEvent.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(stepFinishedEvent); @@ -220,10 +225,10 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { - if (!Enabled) + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = hookBindingStartedEvent.ContextManager.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(hookBindingStartedEvent); @@ -232,10 +237,10 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { - if (!Enabled) + var featureName = hookBindingFinishedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = hookBindingFinishedEvent.ContextManager.FeatureContext.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(hookBindingFinishedEvent); @@ -244,10 +249,10 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { - if (!Enabled) + var featureName = attachmentAddedEvent.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = attachmentAddedEvent.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(attachmentAddedEvent); @@ -256,10 +261,10 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) { - if (!Enabled) + var featureName = outputAddedEvent.FeatureInfo?.Title; + if (!Enabled || String.IsNullOrEmpty(featureName)) return; - var featureName = outputAddedEvent.FeatureInfo.Title; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(outputAddedEvent); diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index 4641b6698..4a7596321 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -20,7 +20,7 @@ public class FeatureInfo // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } // This holds the unique identifier for the tracker instance that is being used to generate cucumber messages for this Test Case - public string CucumberMessages_TestCaseTrackerId { get; set; } + public string CucumberMessages_PickleId { get; set; } public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) From 74a01ef5b2c907f344644e728961ef29d6451223 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 12 Oct 2024 12:19:51 -0500 Subject: [PATCH 183/218] Documentation first draft. Lacking navigation from main docs. --- docs/cucumber-messages/configuration | 113 +++++++++++++++++++++++++++ docs/cucumber-messages/introduction | 21 +++++ 2 files changed, 134 insertions(+) create mode 100644 docs/cucumber-messages/configuration create mode 100644 docs/cucumber-messages/introduction diff --git a/docs/cucumber-messages/configuration b/docs/cucumber-messages/configuration new file mode 100644 index 000000000..567d0753e --- /dev/null +++ b/docs/cucumber-messages/configuration @@ -0,0 +1,113 @@ +# Cucumber Messages Configuration + +There are two ways to configure Cucumber Messages support within Reqnroll. + +* [](#config-file) +* [](#environment-variable) + +## Defaults +Unless overriden by the use of a configuration file and/or environment variable, Reqnroll will use the following defaults to configure support for Cucumber Messages. +```{list-table} +:header-rows: 1 + +* - Setting + - Value + - Description +* - Enable + - true/false + - Controls whether Cucumber Messages will be created during the execution of the test.
*Default:* `false` +* - Base Path + - relative or absolute file storage path + - Specifies the root location for storage of Cucumber Messages result files.
*Default:* `.` (i.e., current folder) +* - Output Directory + - folder(/path) relative to the BasePath + - Folder relative to the Base Path for storage of Cucumber Messages results files.
*Default:* `` (none) +* - Output FileName + - filename(.ndjson) + - The file name in which Cucumber Messages will be stored. By convention, these files use the `.ndjson` extension.
*Default:* `reqnroll_report.ndjson` +* - ID Generation Style + - UUID/INCREMENTING + - Message elements within the .ndjson will be given IDs according to this style directive. Incrementing IDs are integers that start at 1.
*Default:* `UUID` +``` + +{#config-file} +## Configuration File +Reqnroll will use the values from a configuration file to control the above settings. To use a configuration file, add a json file to your project called `cucumbermessages.config.json' +An example configuration file looks like this: + +```{code-block} json +:caption: cucumbermessages.config.json +{ + "fileOutputEnabled": true, + "activeProfileName": "LOCAL", + "profiles": [ + { + "profileName": "LOCAL", + "basePath": "C:\\Users\\dev\\source\\repos\\reqnroll_project", + "outputDirectory": "CucumberMessages", + "IDGenerationStyle": "INCREMENTING" + } + ] +} +``` + +In the above example, the configuration file instructs Reqnroll to turn Cucumber Message support ON, store the file in an absolute path (C:\\Users\\dev\\source\\repos\\reqnroll_project\\CucumberMessages\\reqnroll_report.ndjson). Note that the Output Filename setting was omitted from the configuration file, thus using the default value. + +Configuration files may specify multiple groups of settings, called Profiles. Only a single profile is used at a time (designated the "active" Profile). Profiles allow you to configure Cucumber Messages for multiple situations and then switch between easily (by overriding the active profile via an Environment Variable). For example, one profile might be used while developing Reqnroll tests locally on a developer/tester workstation, while another Profile would be used when executing those tests on a build server. + +The json schema for the configuration file is: +```{code-block} json +:caption: cucumbermessages.config-schema.json +{ + "description": "This class holds configuration information from a configuration source.\nEach configuration source may provide one or more Profiles (such as Dev or Prod). \nThe default profile is always named \u0027DEFAULT\u0027.", + "type": "object", + "properties": { + "FileOutputEnabled": { + "type": "boolean" + }, + "ActiveProfileName": { + "type": "string" + }, + "Profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ProfileName": { + "type": "string" + }, + "BasePath": { + "type": "string" + }, + "OutputDirectory": { + "type": "string" + }, + "OutputFileName": { + "type": "string" + }, + "IDGenerationStyle": { + "type": "string" + } + } + } + } + } +} +``` + +{#environment-variable} +## Environment Variables + +Any of the settings discussed above, including the Active Profile Name, can be overriden by setting an Environment Variable. When an environment variable is set, it takes precedence over the same configuration setting in the configuration file. If a setting is not overriden by an evironment variable, the value will be taken from the configuration file (if set), otherwise a default (as shown above) will be used. +If an environment variable is used to override the Active Profile Name, that profile must be documented in the configuration file. If it is not, the Active Profile as set in the configuration file will continue to be used. +You may selectively override only portions of the configuration by environment variable. Reqnroll will first establish which Profile to use, load those values, then override them with other settings from environment variables. + +The available Environment Variables are: + +* REQNROLL__CUCUMBER_MESSAGES__ENABLED +* REQNROLL__CUCUMBER_MESSAGES__ACTIVE_OUTPUT_PROFILE +* REQNROLL__CUCUMBER_MESSAGES__OUTPUT_BASE_DIRECTORY +* REQNROLL__CUCUMBER_MESSAGES__OUTPUT_RELATIVE_PATH +* REQNROLL__CUCUMBER_MESSAGES__OUTPUT_FILENAME +* REQNROLL__CUCUMBER_MESSAGES__ID_GENERATION_STYLE + diff --git a/docs/cucumber-messages/introduction b/docs/cucumber-messages/introduction new file mode 100644 index 000000000..91733c9ba --- /dev/null +++ b/docs/cucumber-messages/introduction @@ -0,0 +1,21 @@ +# Cucumber Messages Introduction +Reqnroll can produce test results as Cucumber Messages. Cucumber Messages are a standardized way for Cucumber-based test tooling (like Reqnroll) to feed test results to Reporting systems. + +```{note} +For more information about Cucumber Messages and the tooling that consumes them, please see the [Cucumber Messages](https://github.com/cucumber/messages) page on Github. +``` + +Cucumber Message support in Reqnroll is 'opt-in'. By default, Reqnroll will not produce Messages. + +The most basic way of turning ON Cucumber Message support is to set an Environment Variable. + +```{code-block} pwsh +:caption: Powershell +$env:REQNROLL__CUCUMBER_MESSAGES__ENABLED=true +``` + +Once enabled, test output will be stored as an .ndjson file. + +The default name for this file is: reqnroll_report.ndjson. By default the output file is located in the execution directory of your test project, e.g., typically + `Your-Project/bin/Release/net8.0/` +The file name and storage location can be changed via configuration options (see [Configuration(./configuration.html)]). From bb642254a79d251b3205d8f9eec93f8d52999fc7 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:04:44 -0500 Subject: [PATCH 184/218] Added ability to override the configuration file's name by use of an environment variable. Strengthened Configuration resiliency when faced with partially filled out profiles. --- .../Configuration/CucumberConfiguration.cs | 11 +++++---- .../CucumberConfigurationConstants.cs | 1 + .../RCM_ConfigFile_ConfigurationSource.cs | 24 +++++++++++++------ .../CucumberCompatibilityTests.cs | 17 ++++++++++++- ...CucumberMessages.CompatibilityTests.csproj | 5 +++- .../CucumberMessages.configuration.json | 2 +- .../partialConfiguration.json | 3 +++ 7 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 Tests/CucumberMessages.CompatibilityTests/partialConfiguration.json diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 8681eca79..2bb3ec12a 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -68,7 +68,7 @@ private ResolvedConfiguration ResolveConfiguration() private ConfigurationDTO ApplyHierarchicalConfiguration() { var defaultConfigurationProvider = new DefaultConfigurationSource(_environmentWrapper); - var fileBasedConfigurationProvider = new RCM_ConfigFile_ConfigurationSource(); + var fileBasedConfigurationProvider = new RCM_ConfigFile_ConfigurationSource(_environmentWrapper); ConfigurationDTO config = defaultConfigurationProvider.GetConfiguration(); config = AddConfig(config, fileBasedConfigurationProvider.GetConfiguration()); @@ -81,7 +81,10 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) var relativePathValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE); var fileNameValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE); var profileValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); - string profileName = profileValue is Success ? ((Success)profileValue).Result : "DEFAULT"; + string profileName = profileValue is Success ? + ((Success)profileValue).Result : + !string.IsNullOrEmpty(config.ActiveProfileName) ? config.ActiveProfileName : + "DEFAULT"; var idGenStyleValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE); var activeConfiguredDestination = config.Profiles.Where(d => d.ProfileName == profileName).FirstOrDefault(); @@ -127,12 +130,12 @@ private ConfigurationDTO AddConfig(ConfigurationDTO rootConfig, ConfigurationDTO { AddOrOverrideProfile(rootConfig.Profiles, overridingProfile); } - if (overridingConfig.ActiveProfileName != null && !rootConfig.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) + if (!String.IsNullOrEmpty(overridingConfig.ActiveProfileName) && !rootConfig.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) { // The incoming configuration DTO points to a profile that doesn't exist. _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {rootConfig.ActiveProfileName} instead."); } - else if (overridingConfig.ActiveProfileName != null) + else if (!String.IsNullOrEmpty(overridingConfig.ActiveProfileName)) rootConfig.ActiveProfileName = overridingConfig.ActiveProfileName; rootConfig.FileOutputEnabled = overridingConfig.FileOutputEnabled; diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs index 3a821e427..6cbccc73a 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -8,6 +8,7 @@ public static class CucumberConfigurationConstants public const string REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ACTIVE_OUTPUT_PROFILE"; public const string REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ENABLED"; public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ID_GENERATION_STYLE"; + public const string REQNROLL_CUCUMBER_MESSAGES_CONFIGURATION_FILE_OVERRIDE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__CONFIGURATION_FILE_OVERRIDE"; public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_INCREMENTING = "INCREMENTING"; public const string REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_UUID = "UUID"; diff --git a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs index 75200530a..a71c1b9fd 100644 --- a/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs +++ b/Reqnroll/CucumberMessages/Configuration/RCM_ConfigFile_ConfigurationSource.cs @@ -1,23 +1,33 @@ -using System.IO; +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using System.IO; using System.Text.Json; namespace Reqnroll.CucumberMessages.Configuration { - // TODO: add support for an environment variable override of the configuration filename. - internal class RCM_ConfigFile_ConfigurationSource : IConfigurationSource + public class RCM_ConfigFile_ConfigurationSource : IConfigurationSource { - private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "CucumberMessages.configuration.json"; + private const string CUCUMBERMESSAGESCONFIGURATIONFILENAME = "cucumberMessages.configuration.json"; + private IEnvironmentWrapper _environmentWrapper; + + public RCM_ConfigFile_ConfigurationSource(IEnvironmentWrapper environmentWrapper) + { + _environmentWrapper = environmentWrapper; + } public ConfigurationDTO GetConfiguration() { var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; jsonOptions.Converters.Add(new IdGenerationStyleEnumConverter()); + var fileNameOverridden = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_CONFIGURATION_FILE_OVERRIDE_ENVIRONMENT_VARIABLE); + + var fileName = fileNameOverridden is Success ? ((Success)fileNameOverridden).Result : CUCUMBERMESSAGESCONFIGURATIONFILENAME; + ConfigurationDTO configurationDTO = null; - if (File.Exists(CUCUMBERMESSAGESCONFIGURATIONFILENAME)) + if (File.Exists(fileName)) { - configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(CUCUMBERMESSAGESCONFIGURATIONFILENAME), - jsonOptions); + configurationDTO = JsonSerializer.Deserialize(File.ReadAllText(fileName), jsonOptions); } return configurationDTO; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 0cac5c5db..c6c920705 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -16,12 +16,27 @@ using SpecFlow.Internal.Json; using Microsoft.VisualBasic.FileIO; using Reqnroll.CucumberMessages.PayloadProcessing; +using Reqnroll.CommonModels; +using Reqnroll.CucumberMessages.Configuration; namespace CucumberMessages.CompatibilityTests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { + [TestMethod] + public void Unit_PartialConfiguration() + { + var envWrapper = new Moq.Mock(); + var envVariable = new Success("partialConfiguration.json"); + + envWrapper.Setup(x => x.GetEnvironmentVariable(It.IsAny())).Returns(envVariable); + + var configReader = new RCM_ConfigFile_ConfigurationSource(envWrapper.Object); + var config = configReader.GetConfiguration(); + + config.FileOutputEnabled.Should().BeTrue(); + } [TestMethod] public void NullTest() @@ -302,7 +317,7 @@ public void ThenTheBasketPriceShouldBe(decimal expectedPrice) // (located in the CucumberMessagesValidator class) public void CCKScenarios(string testName, string featureNameText) { - ResetCucumberMessages(featureNameText+".ndjson"); + ResetCucumberMessages(featureNameText + ".ndjson"); EnableCucumberMessages(); SetCucumberMessagesOutputFileName(featureNameText + ".ndjson"); CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index d4620485d..518ac29bd 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -178,7 +178,10 @@ Always - + + Always + + Always
diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 5e5933ec8..615a09738 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -5,7 +5,7 @@ { "profileName": "LOCAL", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", - "outputDirectory": "CucumberMessages", + "outputDirectory": "CucumberMessages", "IDGenerationStyle": "INCREMENTING" } ] diff --git a/Tests/CucumberMessages.CompatibilityTests/partialConfiguration.json b/Tests/CucumberMessages.CompatibilityTests/partialConfiguration.json new file mode 100644 index 000000000..2c53f4b55 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/partialConfiguration.json @@ -0,0 +1,3 @@ +{ + "fileOutputEnabled": true +} \ No newline at end of file From b508dac27c1f45aaad1d70b0454669508b854351 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:11:20 -0500 Subject: [PATCH 185/218] Correcting namespace. --- Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs | 7 +++---- Reqnroll/Infrastructure/DefaultDependencyProvider.cs | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 29eaab18a..7006f2301 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -17,12 +17,11 @@ using Reqnroll.EnvironmentAccess; using Reqnroll.CommonModels; using Reqnroll.CucumberMessages.Configuration; -using Reqnroll.CucumberMessages.PubSub; using Reqnroll.CucumberMessages.PayloadProcessing; using System.Runtime.InteropServices.ComTypes; -namespace Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin +namespace Reqnroll.CucumberMessages.PubSub { /// /// The FileOutputPlugin is the subscriber to the CucumberMessageBroker. @@ -95,7 +94,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) string baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); string fileName = SanitizeFileName(config.OutputFileName); - fileWritingTask = Task.Factory.StartNew( () => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); + fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } @@ -106,7 +105,7 @@ public void Publish(ReqnrollCucumberMessage message) private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) { - + // Consider refactoring this to a Using() block. var fileStream = File.CreateText(Path.Combine(baseDirectory, fileName)); diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index e2dca6b56..572d21f97 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,7 +16,6 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PubSub; From 6058a928c81ef37168117187ecb4670ce1741eaf Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:09:39 -0500 Subject: [PATCH 186/218] Simplified the File Sink to run on the main thread. The serializer was changed to write directly to the filestream (so no additional buffers allocated). --- .../PayloadProcessing/NdjsonSerializer.cs | 6 ++ .../PubSub/FileOutputPlugin.cs | 66 +++---------------- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index 50309709f..8fb0517cb 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -1,6 +1,7 @@ using Io.Cucumber.Messages.Types; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; using System; +using System.IO; using System.Text.Json; namespace Reqnroll.CucumberMessages.PayloadProcessing @@ -55,5 +56,10 @@ internal static T Deserialize(string json) { return JsonSerializer.Deserialize(json, JsonOptions)!; } + + public static void SerializeToStream(FileStream fs, Envelope message) + { + JsonSerializer.Serialize(fs, message, JsonOptions); + } } } diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 7006f2301..f325d83f1 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -1,24 +1,17 @@ #nullable enable -using Reqnroll.CucumberMessages; using Reqnroll.Plugins; using Reqnroll.UnitTestProvider; -using Io.Cucumber.Messages.Types; -using System.Reflection; using Reqnroll.Events; -using System.Collections.Concurrent; -using System.Text.Json; using Reqnroll.Tracing; using Reqnroll.BoDi; using System; using System.Threading.Tasks; using System.IO; using System.Linq; -using Reqnroll.EnvironmentAccess; -using Reqnroll.CommonModels; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PayloadProcessing; -using System.Runtime.InteropServices.ComTypes; +using System.Text; namespace Reqnroll.CucumberMessages.PubSub @@ -27,21 +20,18 @@ namespace Reqnroll.CucumberMessages.PubSub /// The FileOutputPlugin is the subscriber to the CucumberMessageBroker. /// It receives Cucumber Messages and writes them to a file. /// - /// File writing is done on a background thread. /// public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin { private Task? fileWritingTask; - //Thread safe collections to hold: - // 1. Inbound Cucumber Messages - BlockingCollection - private readonly BlockingCollection postedMessages = new(); private ICucumberConfiguration _configuration; private Lazy traceListener; private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; private IObjectContainer? globalObjectContainer; + private FileStream? _fileStream; public FileOutputPlugin(ICucumberConfiguration configuration) @@ -75,12 +65,12 @@ private void Close(TestRunFinishedEvent @event) } private void CloseFileSink() { + CloseStream(_fileStream!); if (disposedValue) return; - postedMessages.CompleteAdding(); fileWritingTask?.Wait(); fileWritingTask = null; } - + private const int TUNING_PARAM_FILE_WRITE_BUFFER_SIZE = 65536; private void LaunchFileSink(TestRunStartedEvent testRunStarted) { ICucumberConfiguration config = _configuration; @@ -93,55 +83,20 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) } string baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); string fileName = SanitizeFileName(config.OutputFileName); - - fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); + _fileStream = File.Create(Path.Combine(baseDirectory, fileName), TUNING_PARAM_FILE_WRITE_BUFFER_SIZE); globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } - + private static byte[] nl = Encoding.UTF8.GetBytes(Environment.NewLine); public void Publish(ReqnrollCucumberMessage message) { - postedMessages.Add(message); - } - - private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) - { - - // Consider refactoring this to a Using() block. - var fileStream = File.CreateText(Path.Combine(baseDirectory, fileName)); - - - foreach (var message in postedMessages.GetConsumingEnumerable()) - { - var featureName = message.CucumberMessageSource; - - if (message.Envelope != null) - { - var cm = Serialize(message.Envelope); - Write(fileStream, cm); - } - } - - CloseStream(fileStream); - } - - - private string Serialize(Envelope message) - { - return NdjsonSerializer.Serialize(message); - } - private void Write(StreamWriter fileStream, string cucumberMessage) - { - try - { - fileStream!.WriteLine(cucumberMessage); - } - catch (System.Exception ex) + if (message.Envelope != null) { - trace?.WriteTestOutput($"FileOutputPlugin Write. Exception: {ex.Message}"); + NdjsonSerializer.SerializeToStream(_fileStream!, message.Envelope); + _fileStream!.Write(nl, 0, nl.Length); } } - private void CloseStream(StreamWriter fileStream) + private void CloseStream(FileStream fileStream) { fileStream?.Flush(); fileStream?.Close(); @@ -156,7 +111,6 @@ protected virtual void Dispose(bool disposing) if (disposing) { CloseFileSink(); - postedMessages.Dispose(); } disposedValue = true; } From 772bbaf7ab85de03b1887a6edfce6a5a5440cbd2 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:34:41 -0500 Subject: [PATCH 187/218] Fix using in tests. --- .../CucumberCompatibilityTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index c6c920705..9941f7c7e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -9,7 +9,6 @@ using Reqnroll.TestProjectGenerator; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using Reqnroll.TestProjectGenerator.Driver; -using Reqnoll.CucumberMessage.FileSink.ReqnrollPlugin; using Moq; using Reqnroll.Tracing; using Reqnroll.EnvironmentAccess; From 4722564571d6d12a65f0d143166858b78972590b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:48:18 -0500 Subject: [PATCH 188/218] Fixed bug in overriding Enable flag in config files. --- .../Configuration/CucumberConfiguration.cs | 7 +++--- .../CucumberCompatibilityTestBase.cs | 23 ++++++++++++++++++- .../CucumberCompatibilityTests.cs | 19 +++++++++------ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 2bb3ec12a..47f6cae3f 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -3,6 +3,7 @@ using Reqnroll.Tracing; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; @@ -115,10 +116,8 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) result.IDGenerationStyle = IdGenerationStyleEnumConverter.ParseIdGenerationStyle(((Success)idGenStyleValue).Result); var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); - var enabled = enabledResult is Success ? ((Success)enabledResult).Result : "FALSE"; - - result.Enabled = Convert.ToBoolean(enabled); - + result.Enabled = enabledResult is Success ? Convert.ToBoolean(((Success)enabledResult).Result) : result.Enabled; + return result; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index c5066ce54..f49f81853 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -39,9 +39,11 @@ protected void DisableCucumberMessages() protected void ResetCucumberMessages(string? fileToDelete = null) { - DisableCucumberMessages(); DeletePreviousMessagesOutput(fileToDelete); ResetCucumberMessagesOutputFileName(); + ResetCucumberMessagesConfigurationFileName(); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE, null); + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE, null); Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE, null); } @@ -50,6 +52,15 @@ protected void ResetCucumberMessagesOutputFileName() Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE, null); } + protected void SetCucumberMessagesConfigurationFileName(string fileName) + { + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_CONFIGURATION_FILE_OVERRIDE_ENVIRONMENT_VARIABLE, fileName); + } + protected void ResetCucumberMessagesConfigurationFileName() + { + Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_CONFIGURATION_FILE_OVERRIDE_ENVIRONMENT_VARIABLE, null); + } + protected void DeletePreviousMessagesOutput(string? fileToDelete = null) { var directory = ActualsResultLocationDirectory(); @@ -99,6 +110,7 @@ protected void CucumberMessagesAddConfigurationFile(string configFileName) { var configFileContent = File.ReadAllText(configFileName); _projectsDriver.AddFile(configFileName, configFileContent); + SetCucumberMessagesConfigurationFileName(configFileName); } protected static string ActualsResultLocationDirectory() @@ -119,5 +131,14 @@ protected void SetEnvironmentVariableForGUIDIdGeneration() Environment.SetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE, CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_UUID); } + protected void FileShouldExist(string v) + { + var directory = ActualsResultLocationDirectory(); + + var file = Path.Combine(directory, v); + + File.Exists(file).Should().BeTrue(file, $"File {v} should exist"); + } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 9941f7c7e..1b36e6dca 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -24,17 +24,22 @@ namespace CucumberMessages.CompatibilityTests public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { [TestMethod] - public void Unit_PartialConfiguration() + public void PartialConfigurationIsCorrectlyHandled() { - var envWrapper = new Moq.Mock(); - var envVariable = new Success("partialConfiguration.json"); + ResetCucumberMessages("reqnoll_report.ndjson"); + CucumberMessagesAddConfigurationFile("partialConfiguration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); - envWrapper.Setup(x => x.GetEnvironmentVariable(It.IsAny())).Returns(envVariable); + AddPassingStepBinding("When"); - var configReader = new RCM_ConfigFile_ConfigurationSource(envWrapper.Object); - var config = configReader.GetConfiguration(); + ExecuteTests(); - config.FileOutputEnabled.Should().BeTrue(); + ShouldAllScenariosPass(); } [TestMethod] From eaace7377316818f6a81aff50a806b14b44b4df8 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:04:56 -0500 Subject: [PATCH 189/218] Rename CCK directory to Samples --- .../CucumberCompatibilityTests.cs | 8 +- ...CucumberMessages.CompatibilityTests.csproj | 198 +++++++++--------- .../{CCK => Samples}/ambiguous/ambiguous.cs | 0 .../ambiguous/ambiguous.feature | 0 .../ambiguous/ambiguous.feature.ndjson | 0 .../attachments/attachments.cs | 0 .../attachments/attachments.feature | 0 .../attachments/attachments.feature.ndjson | 0 .../attachments/cucumber.jpeg | Bin .../{CCK => Samples}/attachments/cucumber.png | Bin .../{CCK => Samples}/attachments/document.pdf | Bin .../{CCK => Samples}/background/background.cs | 0 .../background/background.feature | 0 .../background/background.feature.ndjson | 0 .../{CCK => Samples}/cdata/cdata.cs | 0 .../{CCK => Samples}/cdata/cdata.feature | 0 .../cdata/cdata.feature.ndjson | 0 .../data-tables/data_tables.cs | 0 .../data-tables/data_tables.feature | 0 .../data-tables/data_tables.feature.ndjson | 0 .../{CCK => Samples}/empty/empty.cs | 0 .../{CCK => Samples}/empty/empty.feature | 0 .../empty/empty.feature.ndjson | 0 .../examples-tables/examples_tables.cs | 0 .../examples-tables/examples_tables.feature | 0 .../examples_tables.feature.ndjson | 0 .../{CCK => Samples}/hooks/cucumber.svg | 0 .../{CCK => Samples}/hooks/hooks.cs | 2 +- .../{CCK => Samples}/hooks/hooks.feature | 0 .../hooks/hooks.feature.ndjson | 0 .../{CCK => Samples}/minimal/minimal.cs | 0 .../{CCK => Samples}/minimal/minimal.feature | 0 .../minimal/minimal.feature.ndjson | 0 .../parameter-types/parameter_types.cs | 0 .../parameter-types/parameter_types.feature | 0 .../parameter_types.feature.ndjson | 0 .../{CCK => Samples}/pending/pending.cs | 0 .../{CCK => Samples}/pending/pending.feature | 0 .../pending/pending.feature.ndjson | 0 .../{CCK => Samples}/rules/rules.cs | 0 .../{CCK => Samples}/rules/rules.feature | 0 .../rules/rules.feature.ndjson | 0 .../{CCK => Samples}/skipped/skipped.cs | 0 .../{CCK => Samples}/skipped/skipped.feature | 0 .../skipped/skipped.feature.ndjson | 0 .../stack-traces/stack_traces.cs | 0 .../stack-traces/stack_traces.feature | 0 .../stack-traces/stack_traces.feature.ndjson | 0 .../{CCK => Samples}/undefined/undefined.cs | 0 .../undefined/undefined.feature | 0 .../undefined/undefined.feature.ndjson | 0 .../unknown_parameter_type.cs | 0 .../unknown_parameter_type.feature | 0 .../unknown_parameter_type.feature.ndjson | 0 54 files changed, 104 insertions(+), 104 deletions(-) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/ambiguous/ambiguous.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/ambiguous/ambiguous.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/ambiguous/ambiguous.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/attachments.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/attachments.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/attachments.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/cucumber.jpeg (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/cucumber.png (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/attachments/document.pdf (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/background/background.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/background/background.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/background/background.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/cdata/cdata.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/cdata/cdata.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/cdata/cdata.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/data-tables/data_tables.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/data-tables/data_tables.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/data-tables/data_tables.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/empty/empty.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/empty/empty.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/empty/empty.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/examples-tables/examples_tables.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/examples-tables/examples_tables.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/examples-tables/examples_tables.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/hooks/cucumber.svg (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/hooks/hooks.cs (96%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/hooks/hooks.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/hooks/hooks.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/minimal/minimal.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/minimal/minimal.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/minimal/minimal.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/parameter-types/parameter_types.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/parameter-types/parameter_types.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/parameter-types/parameter_types.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/pending/pending.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/pending/pending.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/pending/pending.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/rules/rules.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/rules/rules.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/rules/rules.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/skipped/skipped.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/skipped/skipped.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/skipped/skipped.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/stack-traces/stack_traces.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/stack-traces/stack_traces.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/stack-traces/stack_traces.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/undefined/undefined.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/undefined/undefined.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/undefined/undefined.feature.ndjson (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/unknown-parameter-type/unknown_parameter_type.cs (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/unknown-parameter-type/unknown_parameter_type.feature (100%) rename Tests/CucumberMessages.CompatibilityTests/{CCK => Samples}/unknown-parameter-type/unknown_parameter_type.feature.ndjson (100%) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 1b36e6dca..d8fefc749 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -329,8 +329,8 @@ public void CCKScenarios(string testName, string featureNameText) var featureFileName = testName.Replace("-", "_"); - AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); + AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.CompatibilityTests.Samples", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.CompatibilityTests.Samples", Assembly.GetExecutingAssembly()); //AddBinaryFilesFromResource($"{testName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); @@ -369,13 +369,13 @@ private void AddUtilClassWithFileSystemPath() { string location = AppContext.BaseDirectory; AddBindingClass( - $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\CCK\"; }} }} "); + $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\Samples\"; }} }} "); } private IEnumerable GetExpectedResults(string testName, string featureFileName) { var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "CCK", $"{testName}\\{featureFileName}.feature.ndjson")); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "Samples", $"{testName}\\{featureFileName}.feature.ndjson")); foreach (var json in expectedJsonText) { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 518ac29bd..03a92e2a1 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -10,138 +10,138 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + Always - + Always - + Always - - - - - + + + + + Always - - - + + + Always - - - - - + + + + + Always - - - + + + Always - - - + + + Always - - - - + + + + Always - - + + Always - - - + + + Always - - - + + + Always - - - - - - + + + + + + Always - - + + Always - + Always @@ -169,13 +169,13 @@
- + Always - + Always - + Always diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/ambiguous/ambiguous.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/attachments.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/cucumber.jpeg similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.jpeg rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/cucumber.jpeg diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/cucumber.png similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/cucumber.png rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/cucumber.png diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/document.pdf similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/attachments/document.pdf rename to Tests/CucumberMessages.CompatibilityTests/Samples/attachments/document.pdf diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/background/background.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/background/background.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/background/background.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/cdata/cdata.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/data-tables/data_tables.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/empty/empty.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/examples-tables/examples_tables.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/cucumber.svg similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/hooks/cucumber.svg rename to Tests/CucumberMessages.CompatibilityTests/Samples/hooks/cucumber.svg diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.cs similarity index 96% rename from Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.cs index 5b6294857..fc84caf13 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.cs +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using System.Diagnostics; -namespace CucumberMessages.CompatibilityTests.CCK.hooks +namespace CucumberMessages.CompatibilityTests.CCK.hooksMulti { [Binding] internal class Hooks diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/hooks/hooks.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/minimal/minimal.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/parameter-types/parameter_types.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/pending/pending.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/rules/rules.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/skipped/skipped.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/stack-traces/stack_traces.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/undefined/undefined.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.cs similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.cs rename to Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature rename to Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature diff --git a/Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CCK/unknown-parameter-type/unknown_parameter_type.feature.ndjson rename to Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson From 5236e479dbb0ffad786630d6720aecba98fb869b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:56:20 -0500 Subject: [PATCH 190/218] Improved codegen to make all references to Messages types as global:: --- ...umberGherkinDocumentExpressionGenerator.cs | 104 ++++++++++-------- .../CucumberPicklesExpressionGenerator.cs | 47 ++++---- 2 files changed, 89 insertions(+), 62 deletions(-) diff --git a/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs index 41169d446..a8de9ca49 100644 --- a/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs +++ b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs @@ -35,6 +35,8 @@ internal class CucumberGherkinDocumentExpressionGenerator : CucumberMessage_Trav List _TableRowsList; List _TableCellsList; + private static readonly string GENERICLIST = typeof(List<>).FullName; + public CucumberGherkinDocumentExpressionGenerator(CodeDomHelper codeDomHelper) { _codeDomHelper = codeDomHelper; @@ -68,10 +70,11 @@ public CodeExpression GenerateGherkinDocumentExpression(GherkinDocument gherkinD Visit(gherkinDocument); - var commentsListExpr = new CodeTypeReference(typeof(List)); - var initializer = new CodeArrayCreateExpression(typeof(Comment), _CommentsList.ToArray()); + var commentTypeRef = new CodeTypeReference(typeof(Comment), CodeTypeReferenceOptions.GlobalReference); + var commentsListExpr = new CodeTypeReference(GENERICLIST, commentTypeRef); + var initializer = new CodeArrayCreateExpression(commentTypeRef, _CommentsList.ToArray()); - _gherkinDocumentExpression = new CodeObjectCreateExpression(typeof(GherkinDocument), + _gherkinDocumentExpression = new CodeObjectCreateExpression(new CodeTypeReference(typeof(GherkinDocument), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(_gherkinDocument.Uri), _feature, new CodeObjectCreateExpression(commentsListExpr, initializer)); @@ -90,13 +93,15 @@ public override void Visit(Feature feature) base.Visit(feature); - var tagsListExpr = new CodeTypeReference(typeof(List)); - var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); - var FClistExpr = new CodeTypeReference(typeof(List)); - var initializer = new CodeArrayCreateExpression(typeof(FeatureChild), _FeatureChildrenList.ToArray()); + var fcCodeDomTypeRef = new CodeTypeReference(typeof(FeatureChild), CodeTypeReferenceOptions.GlobalReference); + var FClistExpr = new CodeTypeReference(GENERICLIST, fcCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(fcCodeDomTypeRef, _FeatureChildrenList.ToArray()); - _feature = new CodeObjectCreateExpression(typeof(Feature), + _feature = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Feature), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), new CodePrimitiveExpression(feature.Language), @@ -116,7 +121,7 @@ public override void Visit(Comment comment) base.Visit(comment); - _CommentsList.Add(new CodeObjectCreateExpression(typeof(Comment), + _CommentsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Comment), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(comment.Text))); @@ -129,7 +134,7 @@ public override void Visit(Tag tag) base.Visit(tag); - _TagsList.Add(new CodeObjectCreateExpression(typeof(Tag), + _TagsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(tag.Name), new CodePrimitiveExpression(tag.Id))); @@ -144,7 +149,7 @@ public override void Visit(Location location) var columnExprTypeExpr = new CodeTypeReference(typeof(Nullable<>)); columnExprTypeExpr.TypeArguments.Add(typeof(long)); - _location = new CodeObjectCreateExpression(typeof(Location), + _location = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Location), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(location.Line), location.Column == null ? new CodeObjectCreateExpression(columnExprTypeExpr) :new CodeObjectCreateExpression(columnExprTypeExpr, new CodePrimitiveExpression(location.Column))); @@ -162,7 +167,7 @@ public override void Visit(FeatureChild featureChild) base.Visit(featureChild); - _FeatureChildrenList.Add(new CodeObjectCreateExpression(typeof(FeatureChild), + _FeatureChildrenList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(FeatureChild), CodeTypeReferenceOptions.GlobalReference), _rule ?? new CodePrimitiveExpression(null), _background ?? new CodePrimitiveExpression(null), _scenario ?? new CodePrimitiveExpression(null))); @@ -182,13 +187,15 @@ public override void Visit(Io.Cucumber.Messages.Types.Rule rule) base.Visit(rule); - var tagsListExpr = new CodeTypeReference(typeof(List)); - var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); - var ruleChildrenListExpr = new CodeTypeReference(typeof(List)); - var ruleChildrenInitializer = new CodeArrayCreateExpression(typeof(RuleChild), _RuleChildrenList.ToArray()); + var ruleChildCodeDomTypeRef = new CodeTypeReference(typeof(RuleChild), CodeTypeReferenceOptions.GlobalReference); + var ruleChildrenListExpr = new CodeTypeReference(GENERICLIST, ruleChildCodeDomTypeRef); + var ruleChildrenInitializer = new CodeArrayCreateExpression(ruleChildCodeDomTypeRef, _RuleChildrenList.ToArray()); - _rule = new CodeObjectCreateExpression(typeof(Io.Cucumber.Messages.Types.Rule), + _rule = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Rule), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), new CodePrimitiveExpression(rule.Keyword), @@ -212,7 +219,7 @@ public override void Visit(RuleChild ruleChild) base.Visit(ruleChild); - _RuleChildrenList.Add(new CodeObjectCreateExpression(typeof(RuleChild), + _RuleChildrenList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(RuleChild), CodeTypeReferenceOptions.GlobalReference), _background ?? new CodePrimitiveExpression(null), _scenario ?? new CodePrimitiveExpression(null))); @@ -232,16 +239,19 @@ public override void Visit(Scenario scenario) base.Visit(scenario); - var tagsListExpr = new CodeTypeReference(typeof(List)); - var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); - var stepsListExpr = new CodeTypeReference(typeof(List)); - var stepsinitializer = new CodeArrayCreateExpression(typeof(Step), _StepsList.ToArray()); + var stepCodeDomTypeRef = new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference); + var stepsListExpr = new CodeTypeReference(GENERICLIST, stepCodeDomTypeRef); + var stepsinitializer = new CodeArrayCreateExpression(stepCodeDomTypeRef, _StepsList.ToArray()); - var examplesListExpr = new CodeTypeReference(typeof(List)); - var examplesinitializer = new CodeArrayCreateExpression(typeof(Examples), _ExamplesList.ToArray()); + var examplesCodeDomTypeRef = new CodeTypeReference(typeof(Examples), CodeTypeReferenceOptions.GlobalReference); + var examplesListExpr = new CodeTypeReference(GENERICLIST, examplesCodeDomTypeRef); + var examplesinitializer = new CodeArrayCreateExpression(examplesCodeDomTypeRef, _ExamplesList.ToArray()); - _scenario = new CodeObjectCreateExpression(typeof(Scenario), + _scenario = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Scenario), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), new CodePrimitiveExpression(scenario.Keyword), @@ -271,13 +281,16 @@ public override void Visit(Examples examples) base.Visit(examples); - var tagsListExpr = new CodeTypeReference(typeof(List)); - var tagsinitializer = new CodeArrayCreateExpression(typeof(Tag), _TagsList.ToArray()); + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); var tableHeaderRow = _TableRowsList.First(); - var tableBodyListExpr = new CodeTypeReference(typeof(List)); - var tableBodyInitializer = new CodeArrayCreateExpression(typeof(TableRow), _TableRowsList.Skip(1).ToArray()); - _ExamplesList.Add(new CodeObjectCreateExpression(typeof(Examples), + var tableRowCodeDomTypeRef = new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference); + var tableBodyListExpr = new CodeTypeReference(GENERICLIST, tableRowCodeDomTypeRef); + var tableBodyInitializer = new CodeArrayCreateExpression(tableRowCodeDomTypeRef, _TableRowsList.Skip(1).ToArray()); + + _ExamplesList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Examples), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), new CodePrimitiveExpression(examples.Keyword), @@ -299,10 +312,13 @@ public override void Visit(Background background) _StepsList = new List(); base.Visit(background); - var stepListExpr = new CodeTypeReference(typeof(List)); - var initializer = new CodeArrayCreateExpression(typeof(Step), _StepsList.ToArray()); - _background = new CodeObjectCreateExpression(typeof(Background), + + var stepCodeDomTypeRef = new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference); + var stepListExpr = new CodeTypeReference(GENERICLIST, stepCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(stepCodeDomTypeRef, _StepsList.ToArray()); + + _background = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Background), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(background.Keyword), new CodePrimitiveExpression(background.Name), @@ -325,10 +341,10 @@ public override void Visit(Step step) base.Visit(step); - _StepsList.Add(new CodeObjectCreateExpression(typeof(Step), + _StepsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(step.Keyword), - new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(StepKeywordType)), step.KeywordType.ToString()), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(StepKeywordType), CodeTypeReferenceOptions.GlobalReference)), step.KeywordType.ToString()), new CodePrimitiveExpression(step.Text), _DocString ?? new CodePrimitiveExpression(null), _dataTable ?? new CodePrimitiveExpression(null), @@ -345,7 +361,7 @@ public override void Visit(DocString docString) base.Visit(docString); - _DocString = new CodeObjectCreateExpression(typeof(DocString), + _DocString = new CodeObjectCreateExpression(new CodeTypeReference(typeof(DocString), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(docString.MediaType), new CodePrimitiveExpression(docString.Content), @@ -362,10 +378,11 @@ public override void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) base.Visit(dataTable); - var listExpr = new CodeTypeReference(typeof(List)); - var initializer = new CodeArrayCreateExpression(typeof(TableRow), _TableRowsList.ToArray()); + var tableRowCodeDomTypeRef = new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference); + var listExpr = new CodeTypeReference(GENERICLIST, tableRowCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(tableRowCodeDomTypeRef, _TableRowsList.ToArray()); - _dataTable = new CodeObjectCreateExpression(typeof(Io.Cucumber.Messages.Types.DataTable), + _dataTable = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.DataTable), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(listExpr, initializer)); @@ -381,11 +398,12 @@ public override void Visit(TableRow row) base.Visit(row); - var CellListExpr = new CodeTypeReference(typeof(List)); + var tableCellCodeDomTypeRef = new CodeTypeReference(typeof(TableCell), CodeTypeReferenceOptions.GlobalReference); + var CellListExpr = new CodeTypeReference(GENERICLIST, tableCellCodeDomTypeRef); - var initializer = new CodeArrayCreateExpression(typeof(TableCell), _TableCellsList.ToArray()); + var initializer = new CodeArrayCreateExpression(tableCellCodeDomTypeRef, _TableCellsList.ToArray()); - _TableRowsList.Add(new CodeObjectCreateExpression(typeof(TableRow), + _TableRowsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference), _location, new CodeObjectCreateExpression(CellListExpr, initializer), new CodePrimitiveExpression(row.Id))); @@ -400,7 +418,7 @@ public override void Visit(TableCell cell) base.Visit(cell); - _TableCellsList.Add(new CodeObjectCreateExpression(typeof(TableCell), + _TableCellsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(TableCell), CodeTypeReferenceOptions.GlobalReference), _location, new CodePrimitiveExpression(cell.Value))); diff --git a/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs index e9f5c9018..797aa0111 100644 --- a/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs +++ b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs @@ -24,6 +24,9 @@ internal class CucumberPicklesExpressionGenerator : CucumberMessage_TraversalVis private List _TableRows; private List _PickleCells; + private static readonly string GENERICLIST = typeof(List<>).FullName; + + public CucumberPicklesExpressionGenerator(CodeDomHelper codeDomHelper) { _codeDomHelper = codeDomHelper; @@ -46,8 +49,10 @@ public CodeExpression GeneratePicklesExpression(IEnumerable pickles) { Visit(pickle); } - var commentsListExpr = new CodeTypeReference(typeof(List)); - var initializer = new CodeArrayCreateExpression(typeof(Pickle), _PickleList.ToArray()); + + var pickleCodeTypeRef = new CodeTypeReference(typeof(Pickle), CodeTypeReferenceOptions.GlobalReference); + var commentsListExpr = new CodeTypeReference(GENERICLIST, pickleCodeTypeRef); + var initializer = new CodeArrayCreateExpression(pickleCodeTypeRef, _PickleList.ToArray()); return new CodeObjectCreateExpression(commentsListExpr, initializer); } @@ -62,16 +67,18 @@ public override void Visit(Pickle pickle) base.Visit(pickle); - var stepsExpr = new CodeTypeReference(typeof(List)); - var stepsinitializer = new CodeArrayCreateExpression(typeof(PickleStep), _PickleSteps.ToArray()); + var pStepTypeRef = new CodeTypeReference(typeof(PickleStep), CodeTypeReferenceOptions.GlobalReference); + var stepsExpr = new CodeTypeReference(GENERICLIST, pStepTypeRef); + var stepsinitializer = new CodeArrayCreateExpression(pStepTypeRef, _PickleSteps.ToArray()); - var tagsExpr = new CodeTypeReference(typeof(List)); - var tagsinitializer = new CodeArrayCreateExpression(typeof(PickleTag), _PickleTags.ToArray()); + var tagsTypeRef = new CodeTypeReference(typeof(PickleTag), CodeTypeReferenceOptions.GlobalReference); + var tagsExpr = new CodeTypeReference(GENERICLIST, tagsTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagsTypeRef, _PickleTags.ToArray()); var astIdsExpr = new CodeTypeReference(typeof(List)); var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), pickle.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); - _PickleList.Add(new CodeObjectCreateExpression(typeof(Pickle), + _PickleList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Pickle), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(pickle.Id), new CodePrimitiveExpression(pickle.Uri), new CodePrimitiveExpression(pickle.Name), @@ -95,11 +102,11 @@ public override void Visit(PickleStep step) var astIdsExpr = new CodeTypeReference(typeof(List)); var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), step.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); - _PickleSteps.Add(new CodeObjectCreateExpression(typeof(PickleStep), + _PickleSteps.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleStep), CodeTypeReferenceOptions.GlobalReference), _PickleStepArgument ?? new CodePrimitiveExpression(null), new CodeObjectCreateExpression(astIdsExpr, astIdsInitializer), new CodePrimitiveExpression(step.Id), - new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(PickleStepType)), step.Type.ToString()), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(PickleStepType), CodeTypeReferenceOptions.GlobalReference)), step.Type.ToString()), new CodePrimitiveExpression(step.Text))); _PickleStepArgument = arg; @@ -107,7 +114,7 @@ public override void Visit(PickleStep step) public override void Visit(PickleDocString docString) { - _PickleDocString = new CodeObjectCreateExpression(typeof(PickleDocString), + _PickleDocString = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleDocString), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(docString.MediaType), new CodePrimitiveExpression(docString.Content)); } @@ -122,7 +129,7 @@ public override void Visit(PickleStepArgument argument) base.Visit(argument); - _PickleStepArgument = new CodeObjectCreateExpression(typeof(PickleStepArgument), + _PickleStepArgument = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleStepArgument), CodeTypeReferenceOptions.GlobalReference), _PickleDocString ?? new CodePrimitiveExpression(null), _PickleTable ?? new CodePrimitiveExpression(null)); @@ -137,10 +144,11 @@ public override void Visit(PickleTable pickleTable) base.Visit(pickleTable); - var rowsExpr = new CodeTypeReference(typeof(List)); - var rowsInitializer = new CodeArrayCreateExpression(typeof(PickleTableRow), _TableRows.ToArray()); + var pickleTableRowTypeRef = new CodeTypeReference(typeof(PickleTableRow), CodeTypeReferenceOptions.GlobalReference); + var rowsExpr = new CodeTypeReference(GENERICLIST, pickleTableRowTypeRef); + var rowsInitializer = new CodeArrayCreateExpression(pickleTableRowTypeRef, _TableRows.ToArray()); - _PickleTable = new CodeObjectCreateExpression(typeof(PickleTable), + _PickleTable = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTable), CodeTypeReferenceOptions.GlobalReference), new CodeObjectCreateExpression(rowsExpr, rowsInitializer)); _TableRows = rows; @@ -153,10 +161,11 @@ public override void Visit(PickleTableRow row) base.Visit(row); - var cellsExpr = new CodeTypeReference(typeof(List)); - var cellsInitializer = new CodeArrayCreateExpression(typeof(PickleTableCell), _PickleCells.ToArray()); + var pickleTableCellTypeRef = new CodeTypeReference(typeof(PickleTableCell), CodeTypeReferenceOptions.GlobalReference); + var cellsExpr = new CodeTypeReference(GENERICLIST, pickleTableCellTypeRef); + var cellsInitializer = new CodeArrayCreateExpression(pickleTableCellTypeRef, _PickleCells.ToArray()); - _TableRows.Add(new CodeObjectCreateExpression(typeof(PickleTableRow), + _TableRows.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTableRow), CodeTypeReferenceOptions.GlobalReference), new CodeObjectCreateExpression(cellsExpr, cellsInitializer))); _PickleCells = cells; @@ -164,13 +173,13 @@ public override void Visit(PickleTableRow row) public override void Visit(PickleTableCell cell) { - _PickleCells.Add(new CodeObjectCreateExpression(typeof(PickleTableCell), + _PickleCells.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTableCell), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(cell.Value))); } public override void Visit(PickleTag tag) { - _PickleTags.Add(new CodeObjectCreateExpression(typeof(PickleTag), + _PickleTags.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTag), CodeTypeReferenceOptions.GlobalReference), new CodePrimitiveExpression(tag.Name), new CodePrimitiveExpression(tag.AstNodeId))); } From 7dd43584feff80a801580b3c14ed989fc0942496 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:08:58 -0500 Subject: [PATCH 191/218] Building up the static message object trees are deferred until the Feature Tracker needs them. Eliminated the explicit use of the PickleStepSequence from the generated code; reducing the footprint of the changes required during generation. --- .../Generation/GeneratorConstants.cs | 2 - .../Generation/ScenarioPartHelper.cs | 37 ------------------- .../Generation/UnitTestMethodGenerator.cs | 14 +------ .../ExecutionTracking/FeatureTracker.cs | 15 ++++++-- .../FeatureLevelCucumberMessages.cs | 15 +++----- .../RuntimeSupport/PickleJar.cs | 29 --------------- .../RuntimeSupport/PickleStepSequence.cs | 33 +++++++++++++++++ Reqnroll/ScenarioInfo.cs | 5 +-- 8 files changed, 55 insertions(+), 95 deletions(-) create mode 100644 Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index ab60a6c06..e5005f87c 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -20,7 +20,5 @@ public class GeneratorConstants public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; public const string PICKLEINDEX_PARAMETER_NAME = "__pickleIndex"; public const string PICKLEINDEX_VARIABLE_NAME = "m_pickleIndex"; - public const string PICKLESTEPSEQUENCE_VARIABLE_NAME = "m_pickleStepSequence"; - public const string PICKLESTEPSEQUENCE_PARAMETER_NAME = "__pickleStepSequence"; } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index d567398df..522c4fea1 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -35,7 +35,6 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) backgroundMethod.Attributes = MemberAttributes.Public; backgroundMethod.Name = GeneratorConstants.BACKGROUND_NAME; - backgroundMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); _codeDomHelper.MarkCodeMemberMethodAsAsync(backgroundMethod); @@ -43,7 +42,6 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, background.Location)) { } - AddVariableForPickleStepSequenceFromMethodParameter(backgroundMethod); foreach (var step in background.Steps) { @@ -236,40 +234,5 @@ public void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleId new CodePrimitiveExpression(pickleIndex.ToString())); testMethod.Statements.Add(pickleIdVariable); } - - public void AddVariableForPickleStepSequenceFromMethodParameter(CodeMemberMethod testMethod) - { - var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, - new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_PARAMETER_NAME)); - - testMethod.Statements.Add(pickleStepSequence); - } - - public void AddVariableForPickleStepSequenceForPickleId(CodeMemberMethod testMethod) - { - // m_pickleStepSequence = testRunner.FeatureContext.FeatureInfo.FeatureCucumberMessages.PickleJar.PickleStepSequenceFor(m_pickleId); - var pickleStepSequence = new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(PickleStepSequence)), GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME, - // Right side of the assignment (property access chain) - - new CodeMethodInvokeExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodePropertyReferenceExpression( - new CodeVariableReferenceExpression(GeneratorConstants.TESTRUNNER_FIELD), - "FeatureContext" - ), - "FeatureInfo" - ), - "FeatureCucumberMessages" - ), - "PickleJar" - ), - "PickleStepSequenceFor", - new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME)) - ); - - testMethod.Statements.Add(pickleStepSequence); - } } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 398ad5ab9..6c55940c9 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -208,7 +208,6 @@ private void GenerateTestBody( // The pickleIndex tells the runtime which Pickle this test corresponds to. The pickleStepSequence is used to provide an index into the Pickle Step IDs when generating Step related Messages. // When Backgrounds and Rule Backgrounds are used, we don't know ahead of time how many Steps there are in the Pickle. AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, _pickleIndex); - AddVariableForPickleStepSequence(testMethod); testMethod.Statements.Add( new CodeVariableDeclarationStatement(_codeDomHelper.GetGlobalizedTypeName(typeof(ScenarioInfo)), "scenarioInfo", @@ -218,8 +217,7 @@ private void GenerateTestBody( new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), inheritedTagsExpression, - new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME), - new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)))); + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -233,10 +231,6 @@ internal void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickle _scenarioPartHelper.AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, pickleIndex); } - internal void AddVariableForPickleStepSequence(CodeMemberMethod testMethod) - { - _scenarioPartHelper.AddVariableForPickleStepSequenceForPickleId(testMethod); - } private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { var tagVariable = new CodeVariableDeclarationStatement(typeof(string[]), GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME, tagsExpression); @@ -291,11 +285,7 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex { var backgroundMethodCallExpression = new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), - generationContext.FeatureBackgroundMethod.Name, - // When invoking a Background, we pass the PickleStepSequence along so that we can track how many steps are generated by the Background. - // The test method will then emit steps with Pickle Step indexes from there. - new CodeVariableReferenceExpression(GeneratorConstants.PICKLESTEPSEQUENCE_VARIABLE_NAME)); - + generationContext.FeatureBackgroundMethod.Name); _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(backgroundMethodCallExpression); statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(backgroundMethodCallExpression)); } diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 37b5b0aaf..c2927fd7b 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -2,6 +2,7 @@ using Io.Cucumber.Messages.Types; using Reqnroll.Bindings; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Events; using System; using System.Collections.Concurrent; @@ -44,6 +45,7 @@ public class FeatureTracker // This dictionary maps from (string) PickleIDIndex to (string) PickleID public Dictionary PickleIds { get; } = new(); + public PickleJar PickleJar { get; set; } public bool FeatureExecutionSuccess { get; private set; } @@ -68,11 +70,11 @@ internal void ProcessEvent(FeatureStartedEvent featureStartedEvent) private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) { - yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source); + yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source()); - var gd = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument; + var gd = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument(); - var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles.ToList(); + var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles().ToList(); var idReWriter = new CucumberMessages.RuntimeSupport.IdReWriter(); idReWriter.ReWriteIds(gd, pickles, IDGenerator, out var reWrittenGherkinDocument, out var reWrittenPickles); @@ -83,6 +85,9 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature { PickleIds.Add(i.ToString(), pickles[i].Id); } + + PickleJar = new PickleJar(pickles); + yield return Envelope.Create(gd); foreach (var pickle in pickles) { @@ -171,6 +176,10 @@ public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { + // Fetch the PickleStepSequence for this Pickle and give to the ScenarioInfo + var pickleStepSequence = PickleJar.PickleStepSequenceFor(pickleIndex); + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleStepSequence = pickleStepSequence; ; + var tccmt = new TestCaseTracker(this, pickleId); tccmt.ProcessEvent(scenarioStartedEvent); testCaseTrackersById.TryAdd(pickleId, tccmt); diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index f49e8f6d5..2fae1e14b 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -17,19 +17,16 @@ public FeatureLevelCucumberMessages(Func source, Func g { if (CucumberConfiguration.Current.Enabled) { - Source = source(); - GherkinDocument = gherkinDocument(); - Pickles = pickles(); + Source = source; + GherkinDocument = gherkinDocument; + Pickles = pickles; Location = location; } - PickleJar = new PickleJar(Pickles); - } public string Location { get; } - public Io.Cucumber.Messages.Types.Source Source { get; } - public Io.Cucumber.Messages.Types.GherkinDocument GherkinDocument { get; } - public IEnumerable Pickles { get; } - public PickleJar PickleJar { get; } + public Func Source { get; } + public Func GherkinDocument { get; } + public Func> Pickles { get; } } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index 3d441fe64..e286da9a0 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -47,33 +47,4 @@ public void NextPickle() _PickleCounter++; } } - - - public class PickleStepSequence - { - public bool HasPickles { get; } - public Pickle CurrentPickle { get; } - - private int _PickleStepCounter; - - public PickleStepSequence(bool hasPickles, Pickle pickle) - { - HasPickles = hasPickles; - CurrentPickle = pickle; - _PickleStepCounter = 0; - } - public void NextStep() - { - _PickleStepCounter++; - } - public string CurrentPickleStepId - { - get - { - if (!HasPickles) return null; - return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; - } - } - - } } diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs new file mode 100644 index 000000000..20fb217a6 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs @@ -0,0 +1,33 @@ +using Io.Cucumber.Messages.Types; +using System.Linq; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + public class PickleStepSequence + { + public bool HasPickles { get; } + public Pickle CurrentPickle { get; } + + private int _PickleStepCounter; + + public PickleStepSequence(bool hasPickles, Pickle pickle) + { + HasPickles = hasPickles; + CurrentPickle = pickle; + _PickleStepCounter = 0; + } + public void NextStep() + { + _PickleStepCounter++; + } + public string CurrentPickleStepId + { + get + { + if (!HasPickles) return null; + return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; + } + } + + } +} diff --git a/Reqnroll/ScenarioInfo.cs b/Reqnroll/ScenarioInfo.cs index c4d8203d0..23d9b90d8 100644 --- a/Reqnroll/ScenarioInfo.cs +++ b/Reqnroll/ScenarioInfo.cs @@ -43,9 +43,9 @@ public class ScenarioInfo /// public string PickleIdIndex { get; } // The list of step PickleIds in the step sequence for this test case. - public PickleStepSequence PickleStepSequence { get; private set; } + public PickleStepSequence PickleStepSequence { get; set; } - public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleIndex = null, PickleStepSequence stepSequence = null) + public ScenarioInfo(string title, string description, string[] tags, IOrderedDictionary arguments, string[] inheritedTags = null, string pickleIndex = null) { Title = title; Description = description; @@ -53,7 +53,6 @@ public ScenarioInfo(string title, string description, string[] tags, IOrderedDic Arguments = arguments; CombinedTags = Tags.Concat(inheritedTags ?? Array.Empty()).ToArray(); PickleIdIndex = pickleIndex; - PickleStepSequence = stepSequence; } } } \ No newline at end of file From 1d08a7c82968a86c0130da05fa894871ca05443a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:39:58 -0500 Subject: [PATCH 192/218] ExternalBindings: initial set up of a test harness. --- ...CucumberMessages.CompatibilityTests.csproj | 6 ++++++ .../ExternalBinding.feature | 8 ++++++++ .../SampleExternalBindingClass.cs | 19 +++++++++++++++++++ .../SampleInternalBindingClass.cs | 19 +++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/ExternalBinding.feature create mode 100644 Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleExternalBindingClass.cs create mode 100644 Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleInternalBindingClass.cs diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj index 03a92e2a1..0f354db4e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj @@ -17,6 +17,8 @@ + + @@ -43,6 +45,7 @@ + @@ -98,6 +101,9 @@ Always + + + diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/ExternalBinding.feature b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/ExternalBinding.feature new file mode 100644 index 000000000..9af48da5f --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/ExternalBinding.feature @@ -0,0 +1,8 @@ +Feature: ExternalBinding + +This feature calls upon one step defined in an internal class +and one step defined in an external assembly + +Scenario: External_Binding_Assemblies_Work_With_Cucumber_Messages + Given I have 3 cukes in my belly + When the sample external binding class is called diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleExternalBindingClass.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleExternalBindingClass.cs new file mode 100644 index 000000000..984606d9c --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleExternalBindingClass.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.Samples.ExternalBindingAssemblies +{ + [Binding] + public class SampleExternalBindingClass + { + [When("the sample external binding class is called")] + public void WhenTheSampleExternalBindingClassIsCalled() + { + // nop + } + } +} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleInternalBindingClass.cs b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleInternalBindingClass.cs new file mode 100644 index 000000000..ccd4cc0ce --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/ExternalBindingAssemblies/SampleInternalBindingClass.cs @@ -0,0 +1,19 @@ +using Reqnroll; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CucumberMessages.CompatibilityTests.Samples.ExternalBindingAssemblies +{ + [Binding] + public class SampleInternalBindingClass + { + [Given("I have {int} cukes in my belly")] + public void GivenIHaveCukesInMyBelly(int cukes) + { + // nop + } + } +} From 05061ceabc3cd4cf15777a7b94c3ff094f626529 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:55:16 -0500 Subject: [PATCH 193/218] Modified Publisher to subscribed to the TestThreadEventPublisher as an EventListener rather than registering event handler delegates (should be marginally faster). --- .../PubSub/CucumberMessagePublisher.cs | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 9152a3e23..7807e7c7a 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -25,7 +25,7 @@ namespace Reqnroll.CucumberMessages.PubSub /// /// It uses the IRuntimePlugin interface to force the runtime to load it during startup (although it is not an external plugin per se). /// - public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin + public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin, IExecutionEventListener { private Lazy _brokerFactory; private ICucumberMessageBroker _broker; @@ -67,17 +67,46 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - testThreadEventPublisher.AddHandler(FeatureStartedEventHandler); - testThreadEventPublisher.AddHandler(FeatureFinishedEventHandler); - testThreadEventPublisher.AddHandler(ScenarioStartedEventHandler); - testThreadEventPublisher.AddHandler(ScenarioFinishedEventHandler); - testThreadEventPublisher.AddHandler(StepStartedEventHandler); - testThreadEventPublisher.AddHandler(StepFinishedEventHandler); - testThreadEventPublisher.AddHandler(HookBindingStartedEventHandler); - testThreadEventPublisher.AddHandler(HookBindingFinishedEventHandler); - testThreadEventPublisher.AddHandler(AttachmentAddedEventHandler); - testThreadEventPublisher.AddHandler(OutputAddedEventHandler); + testThreadEventPublisher.AddListener(this); + } + public void OnEvent(IExecutionEvent executionEvent) + { + switch (executionEvent) + { + case FeatureStartedEvent featureStartedEvent: + FeatureStartedEventHandler(featureStartedEvent); + break; + case FeatureFinishedEvent featureFinishedEvent: + FeatureFinishedEventHandler(featureFinishedEvent); + break; + case ScenarioStartedEvent scenarioStartedEvent: + ScenarioStartedEventHandler(scenarioStartedEvent); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + ScenarioFinishedEventHandler(scenarioFinishedEvent); + break; + case StepStartedEvent stepStartedEvent: + StepStartedEventHandler(stepStartedEvent); + break; + case StepFinishedEvent stepFinishedEvent: + StepFinishedEventHandler(stepFinishedEvent); + break; + case HookBindingStartedEvent hookBindingStartedEvent: + HookBindingStartedEventHandler(hookBindingStartedEvent); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + HookBindingFinishedEventHandler(hookBindingFinishedEvent); + break; + case AttachmentAddedEvent attachmentAddedEvent: + AttachmentAddedEventHandler(attachmentAddedEvent); + break; + case OutputAddedEvent outputAddedEvent: + OutputAddedEventHandler(outputAddedEvent); + break; + default: + break; + } } // This method will get called after TestRunStartedEvent has been published and after any BeforeTestRun hooks have been called From a17237aec75dd4f63537a0637f6b7287c0e97784 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:11:31 -0500 Subject: [PATCH 194/218] Added test to confirm that External Binding Assembly's step definitions get published as StepDefinition messages. Restructured test project to split CCK tests from basic integration tests. --- Reqnroll.sln | 2 +- .../CrossReferenceBuilder.cs | 2 +- .../CucumberCompatibilityTestBase.cs | 34 +- .../CucumberCompatibilityTests.cs | 315 +--------------- ...s.csproj => CucumberMessages.Tests.csproj} | 0 .../CucumberMessagesBasicTests.cs | 351 ++++++++++++++++++ .../CucumberMessagesValidator.cs | 12 +- ...FluentAssertionsCustomStringComparisons.cs | 2 +- ...ionCucumberMessagePropertySelectionRule.cs | 2 +- 9 files changed, 397 insertions(+), 323 deletions(-) rename Tests/CucumberMessages.CompatibilityTests/{CucumberMessages.CompatibilityTests.csproj => CucumberMessages.Tests.csproj} (100%) create mode 100644 Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs diff --git a/Reqnroll.sln b/Reqnroll.sln index fe8c30ca0..373c6de86 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,7 +116,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.CompatibilityTests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.CompatibilityTests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.Tests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.Tests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs index 6df495453..1a2eea796 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CrossReferenceBuilder.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { internal class CrossReferenceBuilder : CucumberMessage_TraversalVisitorBase { diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index f49f81853..ee84d0bc5 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,6 +1,8 @@ using FluentAssertions; +using Io.Cucumber.Messages.Types; using Moq; using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing; using Reqnroll.EnvironmentAccess; using Reqnroll.SystemTests; using Reqnroll.Tracing; @@ -12,7 +14,7 @@ using System.Text.Json; using System.Threading.Tasks; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { public class CucumberCompatibilityTestBase : SystemTestBase { @@ -140,5 +142,35 @@ protected void FileShouldExist(string v) File.Exists(file).Should().BeTrue(file, $"File {v} should exist"); } + protected void AddUtilClassWithFileSystemPath() + { + string location = AppContext.BaseDirectory; + AddBindingClass( + $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\Samples\"; }} }} "); + } + + protected IEnumerable GetExpectedResults(string testName, string featureFileName) + { + var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); + var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "Samples", $"{testName}\\{featureFileName}.feature.ndjson")); + + foreach (var json in expectedJsonText) + { + var e = NdjsonSerializer.Deserialize(json); + yield return e; + }; + } + + protected IEnumerable GetActualResults(string testName, string fileName) + { + string resultLocation = ActualsResultLocationDirectory(); + + // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario + + var actualJsonText = File.ReadAllLines(Path.Combine(resultLocation, $"{fileName}.ndjson")); + + foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); + } + } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index d8fefc749..0163f167e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -18,289 +18,11 @@ using Reqnroll.CommonModels; using Reqnroll.CucumberMessages.Configuration; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { [TestClass] public class CucumberCompatibilityTests : CucumberCompatibilityTestBase { - [TestMethod] - public void PartialConfigurationIsCorrectlyHandled() - { - ResetCucumberMessages("reqnoll_report.ndjson"); - CucumberMessagesAddConfigurationFile("partialConfiguration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Test - Scenario: Eating Cukes - When I eat 5 cukes - """); - - AddPassingStepBinding("When"); - - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - [TestMethod] - public void NullTest() - { - ResetCucumberMessages(); - // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else - AddFeatureFile(""" - Feature: Cucumber Messages Null Test - Scenario: Eating Cukes - When I eat 5 cukes - """); - - AddPassingStepBinding("When"); - - ExecuteTests(); - - ShouldAllScenariosPass(); - } - [TestMethod] - public void SmokeTest() - { - ResetCucumberMessages("reqnoll_report.ndjson"); - EnableCucumberMessages(); - //SetCucumberMessagesOutputFileName(); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Test - Scenario: Eating Cukes - When I eat 5 cukes - """); - - AddPassingStepBinding("When"); - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - [TestMethod] - public void CanGenerateGUIDIds_SmokeTest() - { - ResetCucumberMessages("CanGenerateGUIDIds_SmokeTest.ndjson"); - EnableCucumberMessages(); - SetCucumberMessagesOutputFileName("CanGenerateGUIDIds_SmokeTest.ndjson"); - SetEnvironmentVariableForGUIDIdGeneration(); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Test - Scenario: Eating Cukes - When I eat 5 cukes - """); - - AddPassingStepBinding("When"); - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - [TestMethod] - public void SmokeTestMultipleFeatures() - { - ResetCucumberMessages("SmokeTestMultipleFeatures.ndjson"); - EnableCucumberMessages(); - SetCucumberMessagesOutputFileName("SmokeTestMultipleFeatures.ndjson"); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Test - Scenario: Eating Cukes - When I eat 5 cukes - """); - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Test Second Smoke Test - Scenario: Eating Other Cukes - When I eat 6 cukes - """); - - AddPassingStepBinding("When"); - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - - [TestMethod] - public void SmokeOutlineTest() - { - ResetCucumberMessages("Cucumber Messages Smoke Outline Test.ndjson"); - EnableCucumberMessages(); - SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test.ndjson"); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Outline Test - @some-tag - Scenario Outline: Log JSON - When the following string is attached as : - Examples: - | color | - | "red" | - | "green" | - | "blue" | - """); - - AddPassingStepBinding("When"); - ExecuteTests(); - - ShouldAllScenariosPass(); - } - [TestMethod] - public void SmokeOutlineTestAsMethods() - { - ResetCucumberMessages("Cucumber Messages Smoke Outline Test As Methods.ndjson"); - var _configurationFileDriver = GetServiceSafe(); - _configurationFileDriver.SetIsRowTestsAllowed(false); - - EnableCucumberMessages(); - SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test As Methods.ndjson"); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - AddFeatureFile(""" - Feature: Cucumber Messages Smoke Outline Test As Methods - @some-tag - Scenario Outline: Log JSON - When the following string is attached as : - Examples: - | color | - | "red" | - | "green" | - | "blue" | - """); - - AddPassingStepBinding("When"); - ExecuteTests(); - - ShouldAllScenariosPass(); - } - - - [TestMethod] - public void CucumberMessagesInteropWithExternalData() - { - ResetCucumberMessages("External Data from CSV file.ndjson"); - // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages - EnableCucumberMessages(); - SetCucumberMessagesOutputFileName("External Data from CSV file.ndjson"); - _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); - CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); - - // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest - var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.CompatibilityTests", Assembly.GetExecutingAssembly()); - _projectsDriver.AddFile("products.csv", content); - AddFeatureFile(""" - Feature: External Data from CSV file - - @DataSource:products.csv - Scenario: Valid product prices are calculated - The scenario will be treated as a scenario outline with the examples from the CSV file. - Given the customer has put 1 pcs of to the basket - When the basket price is calculated - Then the basket price should be greater than zero - - """); - - AddBindingClass(""" - using System; - using System.Collections.Generic; - using System.Linq; - - namespace Reqnroll.ExternalData.ReqnrollPlugin.IntegrationTest.StepDefinitions - { - [Binding] - public class PricingStepDefinitions - { - class PriceCalculator - { - private readonly Dictionary _basket = new(); - private readonly Dictionary _itemPrices = new(); - - public void AddToBasket(string productName, int quantity) - { - if (!_basket.TryGetValue(productName, out var currentQuantity)) - currentQuantity = 0; - _basket[productName] = currentQuantity + quantity; - } - - public decimal CalculatePrice() - { - return _basket.Sum(bi => GetPrice(bi.Key) * bi.Value); - } - - private decimal GetPrice(string productName) - { - if (_itemPrices.TryGetValue(productName, out var itemPrice)) - return itemPrice; - return 1.5m; - } - - public void SetPrice(string productName, in decimal itemPrice) - { - _itemPrices[productName] = itemPrice; - } - } - - private readonly ScenarioContext _scenarioContext; - private readonly PriceCalculator _priceCalculator = new(); - private decimal _calculatedPrice; - - public PricingStepDefinitions(ScenarioContext scenarioContext) - { - _scenarioContext = scenarioContext; - } - - [Given(@"the price of (.*) is €(.*)")] - public void GivenThePriceOfProductIs(string productName, decimal itemPrice) - { - _priceCalculator.SetPrice(productName, itemPrice); - } - - [Given(@"the customer has put (.*) pcs of (.*) to the basket")] - public void GivenTheCustomerHasPutPcsOfProductToTheBasket(int quantity, string productName) - { - _priceCalculator.AddToBasket(productName, quantity); - } - - [Given(@"the customer has put a product to the basket")] - public void GivenTheCustomerHasPutAProductToTheBasket() - { - var productName = _scenarioContext.ScenarioInfo.Arguments["product"]?.ToString(); - _priceCalculator.AddToBasket(productName, 1); - } - - [When(@"the basket price is calculated")] - public void WhenTheBasketPriceIsCalculated() - { - _calculatedPrice = _priceCalculator.CalculatePrice(); - } - - [Then(@"the basket price should be greater than zero")] - public void ThenTheBasketPriceShouldBeGreaterThanZero() - { - if (_calculatedPrice <= 0) throw new Exception("Basket price is less than zero: " + _calculatedPrice ); - } - - [Then(@"the basket price should be €(.*)")] - public void ThenTheBasketPriceShouldBe(decimal expectedPrice) - { - if(expectedPrice != _calculatedPrice) throw new Exception("Basket price is not as expected: " + _calculatedPrice + " vs " + expectedPrice); - } - - } - } - - """); - ExecuteTests(); - - ShouldAllScenariosPass(3); - - } [TestMethod] [DataRow("minimal", "minimal")] @@ -329,8 +51,8 @@ public void CCKScenarios(string testName, string featureNameText) var featureFileName = testName.Replace("-", "_"); - AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.CompatibilityTests.Samples", Assembly.GetExecutingAssembly()); - AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.CompatibilityTests.Samples", Assembly.GetExecutingAssembly()); + AddFeatureFileFromResource($"{featureFileName}/{featureFileName}.feature", "CucumberMessages.Tests.Samples", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"{featureFileName}/{featureFileName}.cs", "CucumberMessages.Tests.Samples", Assembly.GetExecutingAssembly()); //AddBinaryFilesFromResource($"{testName}", "CucumberMessages.CompatibilityTests.CCK", Assembly.GetExecutingAssembly()); ExecuteTests(); @@ -364,37 +86,6 @@ public void NonCompliantCCKScenarios(string testName, string featureNameText) { CCKScenarios(testName, featureNameText); } - - private void AddUtilClassWithFileSystemPath() - { - string location = AppContext.BaseDirectory; - AddBindingClass( - $"public class FileSystemPath {{ public static string GetFilePathForAttachments() {{ return @\"{location}\\Samples\"; }} }} "); - } - - private IEnumerable GetExpectedResults(string testName, string featureFileName) - { - var workingDirectory = Path.Combine(AppContext.BaseDirectory, "..\\..\\.."); - var expectedJsonText = File.ReadAllLines(Path.Combine(workingDirectory!, "Samples", $"{testName}\\{featureFileName}.feature.ndjson")); - - foreach (var json in expectedJsonText) - { - var e = NdjsonSerializer.Deserialize(json); - yield return e; - }; - } - - private IEnumerable GetActualResults(string testName, string fileName) - { - string resultLocation = ActualsResultLocationDirectory(); - - // Hack: the file name is hard-coded in the test row data to match the name of the feature within the Feature file for the example scenario - - var actualJsonText = File.ReadAllLines(Path.Combine(resultLocation, $"{fileName}.ndjson")); - - foreach (var json in actualJsonText) yield return NdjsonSerializer.Deserialize(json); - } - } } \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj similarity index 100% rename from Tests/CucumberMessages.CompatibilityTests/CucumberMessages.CompatibilityTests.csproj rename to Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs new file mode 100644 index 000000000..eaac77bf7 --- /dev/null +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs @@ -0,0 +1,351 @@ +using Io.Cucumber.Messages.Types; +using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; +using Newtonsoft.Json.Bson; +using Reqnroll; +using System.Reflection; +using FluentAssertions; +using System.Text.Json; +using System.ComponentModel; +using Reqnroll.TestProjectGenerator; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Reqnroll.TestProjectGenerator.Driver; +using Moq; +using Reqnroll.Tracing; +using Reqnroll.EnvironmentAccess; +using SpecFlow.Internal.Json; +using Microsoft.VisualBasic.FileIO; +using Reqnroll.CucumberMessages.PayloadProcessing; +using Reqnroll.CommonModels; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.TestProjectGenerator.Data; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; + +namespace CucumberMessages.Tests +{ + [TestClass] + public class CucumberMessagesBasicTests : CucumberCompatibilityTestBase + { + [TestMethod] + public void PartialConfigurationIsCorrectlyHandled() + { + ResetCucumberMessages("reqnoll_report.ndjson"); + CucumberMessagesAddConfigurationFile("partialConfiguration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] + public void NullTest() + { + ResetCucumberMessages(); + // The purpose of this test is to confirm that when Cucumber Messages are turned off, the Cucumber Messages ecosystem does not cause any interference anywhere else + AddFeatureFile(""" + Feature: Cucumber Messages Null Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] + public void SmokeTest() + { + ResetCucumberMessages("reqnoll_report.ndjson"); + EnableCucumberMessages(); + //SetCucumberMessagesOutputFileName(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] + public void CanGenerateGUIDIds_SmokeTest() + { + ResetCucumberMessages("CanGenerateGUIDIds_SmokeTest.ndjson"); + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CanGenerateGUIDIds_SmokeTest.ndjson"); + SetEnvironmentVariableForGUIDIdGeneration(); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + [TestMethod] + public void SmokeTestMultipleFeatures() + { + ResetCucumberMessages("SmokeTestMultipleFeatures.ndjson"); + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("SmokeTestMultipleFeatures.ndjson"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test + Scenario: Eating Cukes + When I eat 5 cukes + """); + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Test Second Smoke Test + Scenario: Eating Other Cukes + When I eat 6 cukes + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + + [TestMethod] + public void SmokeOutlineTest() + { + ResetCucumberMessages("Cucumber Messages Smoke Outline Test.ndjson"); + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test.ndjson"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + [TestMethod] + public void SmokeOutlineTestAsMethods() + { + ResetCucumberMessages("Cucumber Messages Smoke Outline Test As Methods.ndjson"); + var _configurationFileDriver = GetServiceSafe(); + _configurationFileDriver.SetIsRowTestsAllowed(false); + + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("CucumberMessages Smoke Outline Test As Methods.ndjson"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + AddFeatureFile(""" + Feature: Cucumber Messages Smoke Outline Test As Methods + @some-tag + Scenario Outline: Log JSON + When the following string is attached as : + Examples: + | color | + | "red" | + | "green" | + | "blue" | + """); + + AddPassingStepBinding("When"); + ExecuteTests(); + + ShouldAllScenariosPass(); + } + + + [TestMethod] + public void CucumberMessagesInteropWithExternalData() + { + ResetCucumberMessages("External Data from CSV file.ndjson"); + // The purpose of this test is to prove that the ScenarioOutline tables generated by the ExternalData plugin can be used in Cucumber messages + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName("External Data from CSV file.ndjson"); + _projectsDriver.AddNuGetPackage("Reqnroll.ExternalData", "2.1.1-local"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest + var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.CompatibilityTests", Assembly.GetExecutingAssembly()); + _projectsDriver.AddFile("products.csv", content); + AddFeatureFile(""" + Feature: External Data from CSV file + + @DataSource:products.csv + Scenario: Valid product prices are calculated + The scenario will be treated as a scenario outline with the examples from the CSV file. + Given the customer has put 1 pcs of to the basket + When the basket price is calculated + Then the basket price should be greater than zero + + """); + + AddBindingClass(""" + using System; + using System.Collections.Generic; + using System.Linq; + + namespace Reqnroll.ExternalData.ReqnrollPlugin.IntegrationTest.StepDefinitions + { + [Binding] + public class PricingStepDefinitions + { + class PriceCalculator + { + private readonly Dictionary _basket = new(); + private readonly Dictionary _itemPrices = new(); + + public void AddToBasket(string productName, int quantity) + { + if (!_basket.TryGetValue(productName, out var currentQuantity)) + currentQuantity = 0; + _basket[productName] = currentQuantity + quantity; + } + + public decimal CalculatePrice() + { + return _basket.Sum(bi => GetPrice(bi.Key) * bi.Value); + } + + private decimal GetPrice(string productName) + { + if (_itemPrices.TryGetValue(productName, out var itemPrice)) + return itemPrice; + return 1.5m; + } + + public void SetPrice(string productName, in decimal itemPrice) + { + _itemPrices[productName] = itemPrice; + } + } + + private readonly ScenarioContext _scenarioContext; + private readonly PriceCalculator _priceCalculator = new(); + private decimal _calculatedPrice; + + public PricingStepDefinitions(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [Given(@"the price of (.*) is €(.*)")] + public void GivenThePriceOfProductIs(string productName, decimal itemPrice) + { + _priceCalculator.SetPrice(productName, itemPrice); + } + + [Given(@"the customer has put (.*) pcs of (.*) to the basket")] + public void GivenTheCustomerHasPutPcsOfProductToTheBasket(int quantity, string productName) + { + _priceCalculator.AddToBasket(productName, quantity); + } + + [Given(@"the customer has put a product to the basket")] + public void GivenTheCustomerHasPutAProductToTheBasket() + { + var productName = _scenarioContext.ScenarioInfo.Arguments["product"]?.ToString(); + _priceCalculator.AddToBasket(productName, 1); + } + + [When(@"the basket price is calculated")] + public void WhenTheBasketPriceIsCalculated() + { + _calculatedPrice = _priceCalculator.CalculatePrice(); + } + + [Then(@"the basket price should be greater than zero")] + public void ThenTheBasketPriceShouldBeGreaterThanZero() + { + if (_calculatedPrice <= 0) throw new Exception("Basket price is less than zero: " + _calculatedPrice ); + } + + [Then(@"the basket price should be €(.*)")] + public void ThenTheBasketPriceShouldBe(decimal expectedPrice) + { + if(expectedPrice != _calculatedPrice) throw new Exception("Basket price is not as expected: " + _calculatedPrice + " vs " + expectedPrice); + } + + } + } + + """); + ExecuteTests(); + + ShouldAllScenariosPass(3); + } + + [TestMethod] + public void ExternalAssembliesAreSupportedInStepDefinitionMessages() + { + var featureNameText = "ExternalBinding"; + ResetCucumberMessages(featureNameText + ".ndjson"); + EnableCucumberMessages(); + SetCucumberMessagesOutputFileName(featureNameText + ".ndjson"); + CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); + + //Set up the Default Project (main test assembly) + AddUtilClassWithFileSystemPath(); + + AddFeatureFileFromResource($"ExternalBindingAssemblies/{featureNameText}.feature", "CucumberMessages.Tests.Samples", Assembly.GetExecutingAssembly()); + AddBindingClassFromResource($"ExternalBindingAssemblies/SampleInternalBindingClass.cs", "CucumberMessages.Tests.Samples", Assembly.GetExecutingAssembly()); + + // fix problem with TestFolders data structure getting munged by adding a second project to the solution + var testPath = _testProjectFolders.ProjectFolder; + var compAssPath = _testProjectFolders.CompiledAssemblyPath; + var projectBinOutputPath = _testProjectFolders.ProjectBinOutputPath; + + //setup an external project; add a binding class to it; and add it to the solution + var externalProject = _projectsDriver.CreateProject("ExternalBindingsProject", "C#", ProjectType.Library); + externalProject.IsReqnrollFeatureProject = false; + var bindingCLassFileContent = _testFileManager.GetTestFileContent("SampleExternalBindingClass.cs", "CucumberMessages.Tests.Samples.ExternalBindingAssemblies", Assembly.GetExecutingAssembly()); + externalProject.AddBindingClass(bindingCLassFileContent); //add the binding + + _projectsDriver.AddProjectReference("ExternalBindingsProject"); + // set the Reqnroll configuration to include an external Assemblies setting + var _configurationFileDriver = GetServiceSafe(); + _configurationFileDriver.AddStepAssembly(new BindingAssembly("ExternalBindingsProject")); + // restoring values to the TestProjectFolders data structure + _testProjectFolders.ProjectFolder = testPath; + _testProjectFolders.ProjectBinOutputPath = projectBinOutputPath; + _testProjectFolders.CompiledAssemblyPath = compAssPath; + + ExecuteTests(); + ShouldAllScenariosPass(); + + var actualResults = GetActualResults("", featureNameText).ToList(); + var stepDefs = actualResults.Select(e => e.Content()).OfType(); + stepDefs.Any(sd => sd.SourceReference.JavaMethod.ClassName.Contains("SampleExternalBindingClass")).Should().BeTrue("StepDefinition for SampleExternalBindingClass should be found in the StepDefinitions"); + + } + } + +} \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 92d449f1a..73e0abf8f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -5,7 +5,7 @@ using System.Reflection; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { public class CucumberMessagesValidator { @@ -109,11 +109,11 @@ private void CompareMessageType() else actual = new List(); - expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; + expected = expecteds_elementsByType[typeof(T)].AsEnumerable().OfType().ToList(); ; if (!(typeof(T) == typeof(TestStepFinished))) { - actual.Should().BeEquivalentTo(expected, options => options.WithTracing(),"When comparing " + typeof(T).Name + "s"); + actual.Should().BeEquivalentTo(expected, options => options.WithTracing(), "When comparing " + typeof(T).Name + "s"); } else { @@ -200,7 +200,7 @@ private void TestExecutionStepsShouldProperlyReferenceTestCases() return; var testStepStarteds = actuals_elementsByType[typeof(TestStepStarted)].OfType().ToList(); var testStepFinisheds = actuals_elementsByType[typeof(TestStepFinished)].OfType().ToList(); - + testCaseStartedIds.Should().Contain(id => testStepStarteds.Any(tss => tss.TestCaseStartedId == id), "a test case started should be referenced by at least one test step started message"); testCaseStartedIds.Should().Contain(id => testStepFinisheds.Any(tsf => tsf.TestCaseStartedId == id), "a test case started should be referenced by at least one test step finished message"); } @@ -421,8 +421,8 @@ private void ArrangeGlobalFluentAssertionOptions() { string subject = subjects[i]; string expectation = expectations[i]; - if ((subject.Length > 0 && subject[0] == '^') || (expectation.Length > 0 && expectation[0] == '^') || - (subject.Length > 0 && subject[subject.Length - 1] == '$') || (expectation.Length > 0 && expectation[expectation.Length - 1] == '$')) + if (subject.Length > 0 && subject[0] == '^' || expectation.Length > 0 && expectation[0] == '^' || + subject.Length > 0 && subject[subject.Length - 1] == '$' || expectation.Length > 0 && expectation[expectation.Length - 1] == '$') { // If the first or last character is '^' or '$', remove it before comparing subject = subject.Length > 0 && subject[0] == '^' ? subject.Substring(1) : subject; diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs index 6b09f6b55..d30478f9e 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAssertionsCustomStringComparisons.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { public class FluentAssertionsCustomStringComparisons : IEqualityComparer { diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index 502eeefb2..f473843df 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -6,7 +6,7 @@ using FluentAssertions; using FluentAssertions.Equivalency; -namespace CucumberMessages.CompatibilityTests +namespace CucumberMessages.Tests { /// /// Fluent Asssertion Cucumber Message Property Selection Rule From c4cc0b73bade488b0af74d243ba0ac1ac6c7d2fe Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:57:37 -0500 Subject: [PATCH 195/218] Testing: configured Multi-Feature smoke test to be parallel (by Feature). --- Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs | 2 +- .../CucumberMessagesBasicTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index 8fb0517cb..c3b6f5799 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -57,7 +57,7 @@ internal static T Deserialize(string json) return JsonSerializer.Deserialize(json, JsonOptions)!; } - public static void SerializeToStream(FileStream fs, Envelope message) + public static void SerializeToStream(Stream fs, Envelope message) { JsonSerializer.Serialize(fs, message, JsonOptions); } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs index eaac77bf7..2e62d62fd 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs @@ -105,6 +105,7 @@ When I eat 5 cukes [TestMethod] public void SmokeTestMultipleFeatures() { + _projectsDriver.EnableTestParallelExecution(); ResetCucumberMessages("SmokeTestMultipleFeatures.ndjson"); EnableCucumberMessages(); SetCucumberMessagesOutputFileName("SmokeTestMultipleFeatures.ndjson"); @@ -120,7 +121,6 @@ When I eat 5 cukes Scenario: Eating Other Cukes When I eat 6 cukes """); - AddPassingStepBinding("When"); ExecuteTests(); From e1cace2a1cc0533179c919de68acd0c77e328d2e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:08:16 -0500 Subject: [PATCH 196/218] FileSinkPlugin refactored to again use a BlockingCollection as a queue. Found this to be necessary when Features are run in parallel in to order to keep the written messages from clobbering each other. --- .../PubSub/FileOutputPlugin.cs | 48 +++++++++++-------- .../CucumberMessagesBasicTests.cs | 2 +- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index f325d83f1..b905adec0 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -12,6 +12,7 @@ using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PayloadProcessing; using System.Text; +using System.Collections.Concurrent; namespace Reqnroll.CucumberMessages.PubSub @@ -25,14 +26,15 @@ public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugi { private Task? fileWritingTask; + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + private readonly BlockingCollection postedMessages = new(); private ICucumberConfiguration _configuration; private Lazy traceListener; private ITraceListener? trace => traceListener.Value; private IObjectContainer? testThreadObjectContainer; private IObjectContainer? globalObjectContainer; - private FileStream? _fileStream; - public FileOutputPlugin(ICucumberConfiguration configuration) { @@ -63,13 +65,7 @@ private void Close(TestRunFinishedEvent @event) // The latter will close down the file stream. Dispose(true); } - private void CloseFileSink() - { - CloseStream(_fileStream!); - if (disposedValue) return; - fileWritingTask?.Wait(); - fileWritingTask = null; - } + private const int TUNING_PARAM_FILE_WRITE_BUFFER_SIZE = 65536; private void LaunchFileSink(TestRunStartedEvent testRunStarted) { @@ -83,25 +79,34 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) } string baseDirectory = Path.Combine(config.BaseDirectory, config.OutputDirectory); string fileName = SanitizeFileName(config.OutputFileName); - _fileStream = File.Create(Path.Combine(baseDirectory, fileName), TUNING_PARAM_FILE_WRITE_BUFFER_SIZE); + fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFilesBackgroundTask(baseDirectory, fileName), TaskCreationOptions.LongRunning); + globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } private static byte[] nl = Encoding.UTF8.GetBytes(Environment.NewLine); public void Publish(ReqnrollCucumberMessage message) { - if (message.Envelope != null) - { - NdjsonSerializer.SerializeToStream(_fileStream!, message.Envelope); - _fileStream!.Write(nl, 0, nl.Length); - } + postedMessages.Add(message); } - private void CloseStream(FileStream fileStream) + private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) { - fileStream?.Flush(); - fileStream?.Close(); - fileStream?.Dispose(); + using var fileStream = File.Create(Path.Combine(baseDirectory, fileName), TUNING_PARAM_FILE_WRITE_BUFFER_SIZE); + + foreach (var message in postedMessages.GetConsumingEnumerable()) + { + if (message.Envelope != null) + { + NdjsonSerializer.SerializeToStream(fileStream!, message.Envelope); + + // Write a newline after each message, except for the last one + if(message.Envelope.TestRunFinished == null) + fileStream!.Write(nl, 0, nl.Length); + } + } + } + private bool disposedValue = false; protected virtual void Dispose(bool disposing) @@ -110,7 +115,10 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - CloseFileSink(); + postedMessages.CompleteAdding(); + fileWritingTask?.Wait(); + fileWritingTask = null; + postedMessages.Dispose(); } disposedValue = true; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs index 2e62d62fd..2a7de3b4b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs @@ -103,7 +103,7 @@ When I eat 5 cukes } [TestMethod] - public void SmokeTestMultipleFeatures() + public void SmokeTestMultipleFeaturesInParallel() { _projectsDriver.EnableTestParallelExecution(); ResetCucumberMessages("SmokeTestMultipleFeatures.ndjson"); From a78dca631d5c655be35fbacdf5ee0dd79f0196e0 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:50:15 -0500 Subject: [PATCH 197/218] Minor fix to the Factory that emits Meta messages to get the Reqnroll version without the build hash portion. --- Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs index f45ebab66..c2b2036cd 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -81,7 +81,7 @@ public bool IsRunningInDockerContainer() public string GetReqnrollVersion() { - return VersionInfo.AssemblyInformationalVersion; + return VersionInfo.NuGetVersion; } public string GetNetCoreVersion() { From 7237a40dd35d4f5ee0aeaa1dbbb3218efa5b646a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:22:20 -0500 Subject: [PATCH 198/218] Fix Cucumber Test problem caused by code reorg The External Data sample needed adjustment to properly fetch its csv file from Resources given the new project structure. --- .../CucumberMessagesBasicTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs index 2a7de3b4b..3ce0b576c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesBasicTests.cs @@ -194,7 +194,7 @@ public void CucumberMessagesInteropWithExternalData() CucumberMessagesAddConfigurationFile("CucumberMessages.configuration.json"); // this test borrows a subset of a feature, the binding class, and the data file from ExternalData.ReqnrollPlugin.IntegrationTest - var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.CompatibilityTests", Assembly.GetExecutingAssembly()); + var content = _testFileManager.GetTestFileContent("products.csv", "CucumberMessages.Tests", Assembly.GetExecutingAssembly()); _projectsDriver.AddFile("products.csv", content); AddFeatureFile(""" Feature: External Data from CSV file From a24a27791e6b30ce7401cf346f2717ce908815db Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:46:13 -0500 Subject: [PATCH 199/218] Async Await support added to TestThreadExecutionEventPublisher and (most) invocations of it. Made Publisher, Broker, and FileSink async/await. Made FeatureTracker async, but not Scenario and Step trackers (tbd). --- .../ExecutionTracking/FeatureTracker.cs | 54 ++++++++++----- .../PubSub/CucumberMessageBroker.cs | 6 +- .../PubSub/CucumberMessagePublisher.cs | 69 ++++++++++--------- .../PubSub/FileOutputPlugin.cs | 4 +- .../PubSub/ICucumberMessageSink.cs | 2 +- Reqnroll/Events/IExecutionEventListener.cs | 7 ++ .../ITestThreadExecutionEventPublisher.cs | 5 ++ .../TestThreadExecutionEventPublisher.cs | 23 +++++++ .../Infrastructure/TestExecutionEngine.cs | 28 ++++---- .../TestThreadExecutionEventPublisherTests.cs | 30 ++++---- 10 files changed, 140 insertions(+), 88 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index c2927fd7b..763da077f 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -10,6 +10,7 @@ using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace Reqnroll.CucumberMessages.ExecutionTracking { @@ -156,7 +157,7 @@ private string ExtractLastID(List pickles) } // When the FeatureFinished event fires, we calculate the Feature-level Execution Status - public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + public Task ProcessEvent(FeatureFinishedEvent featureFinishedEvent) { var testCases = testCaseTrackersById.Values.ToList(); @@ -166,13 +167,15 @@ public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) true => testCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK), _ => true }; + + return Task.CompletedTask; } - public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + public Task ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { // as in the Publisher, we're using defensive coding here b/c some test setups might not have complete info var pickleIndex = scenarioStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -184,12 +187,14 @@ public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) tccmt.ProcessEvent(scenarioStartedEvent); testCaseTrackersById.TryAdd(pickleId, tccmt); } + return Task.CompletedTask; + } - public IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + public Task> ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { var pickleIndex = scenarioFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Enumerable.Empty(); + if (String.IsNullOrEmpty(pickleIndex)) return Task.FromResult(Enumerable.Empty()); if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -197,17 +202,17 @@ public IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinished { tccmt.ProcessEvent(scenarioFinishedEvent); - return tccmt.TestCaseCucumberMessages(); + return Task.FromResult(tccmt.TestCaseCucumberMessages()); } } - return Enumerable.Empty(); + return Task.FromResult(Enumerable.Empty()); } - public void ProcessEvent(StepStartedEvent stepStartedEvent) + public Task ProcessEvent(StepStartedEvent stepStartedEvent) { var pickleIndex = stepStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -216,13 +221,15 @@ public void ProcessEvent(StepStartedEvent stepStartedEvent) tccmt.ProcessEvent(stepStartedEvent); } } + + return Task.CompletedTask; } - public void ProcessEvent(StepFinishedEvent stepFinishedEvent) + public Task ProcessEvent(StepFinishedEvent stepFinishedEvent) { var pickleIndex = stepFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) @@ -230,57 +237,66 @@ public void ProcessEvent(StepFinishedEvent stepFinishedEvent) tccmt.ProcessEvent(stepFinishedEvent); } } + + return Task.CompletedTask; } - public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + public Task ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { var pickleIndex = hookBindingStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) tccmt.ProcessEvent(hookBindingStartedEvent); } + + return Task.CompletedTask; } - public void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + public Task ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { var pickleIndex = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) tccmt.ProcessEvent(hookBindingFinishedEvent); } + + return Task.CompletedTask; } - public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + public Task ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var pickleId = attachmentAddedEvent.FeatureInfo?.CucumberMessages_PickleId; - if (String.IsNullOrEmpty(pickleId)) return; + if (String.IsNullOrEmpty(pickleId)) return Task.CompletedTask; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { tccmt.ProcessEvent(attachmentAddedEvent); } + + return Task.CompletedTask; } - public void ProcessEvent(OutputAddedEvent outputAddedEvent) + public Task ProcessEvent(OutputAddedEvent outputAddedEvent) { var pickleId = outputAddedEvent.FeatureInfo?.CucumberMessages_PickleId; - if (String.IsNullOrEmpty(pickleId)) return; + if (String.IsNullOrEmpty(pickleId)) return Task.CompletedTask; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { tccmt.ProcessEvent(outputAddedEvent); } + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index 548163016..e36426ca0 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -16,7 +16,7 @@ namespace Reqnroll.CucumberMessages.PubSub public interface ICucumberMessageBroker { bool Enabled { get; } - void Publish(ReqnrollCucumberMessage featureMessages); + Task Publish(ReqnrollCucumberMessage featureMessages); } /// @@ -38,11 +38,11 @@ public CucumberMessageBroker(IObjectContainer objectContainer) _objectContainer = objectContainer; RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); } - public void Publish(ReqnrollCucumberMessage message) + public async Task Publish(ReqnrollCucumberMessage message) { foreach (var sink in RegisteredSinks.Value) { - sink.Publish(message); + await sink.Publish(message); } } diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 7807e7c7a..1872e7eb3 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -14,6 +14,7 @@ using Reqnroll.CucumberMessages.Configuration; using Gherkin.CucumberMessages; using Reqnroll.Bindings; +using System.Threading.Tasks; namespace Reqnroll.CucumberMessages.PubSub { @@ -25,7 +26,7 @@ namespace Reqnroll.CucumberMessages.PubSub /// /// It uses the IRuntimePlugin interface to force the runtime to load it during startup (although it is not an external plugin per se). /// - public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin, IExecutionEventListener + public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugin, IAsyncExecutionEventListener { private Lazy _brokerFactory; private ICucumberMessageBroker _broker; @@ -67,42 +68,42 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar public void HookIntoTestThreadExecutionEventPublisher(ITestThreadExecutionEventPublisher testThreadEventPublisher) { - testThreadEventPublisher.AddListener(this); + testThreadEventPublisher.AddAsyncListener(this); } - public void OnEvent(IExecutionEvent executionEvent) + public async Task OnEventAsync(IExecutionEvent executionEvent) { switch (executionEvent) { case FeatureStartedEvent featureStartedEvent: - FeatureStartedEventHandler(featureStartedEvent); + await FeatureStartedEventHandler(featureStartedEvent); break; case FeatureFinishedEvent featureFinishedEvent: - FeatureFinishedEventHandler(featureFinishedEvent); + await FeatureFinishedEventHandler(featureFinishedEvent); break; case ScenarioStartedEvent scenarioStartedEvent: - ScenarioStartedEventHandler(scenarioStartedEvent); + await ScenarioStartedEventHandler(scenarioStartedEvent); break; case ScenarioFinishedEvent scenarioFinishedEvent: - ScenarioFinishedEventHandler(scenarioFinishedEvent); + await ScenarioFinishedEventHandler(scenarioFinishedEvent); break; case StepStartedEvent stepStartedEvent: - StepStartedEventHandler(stepStartedEvent); + await StepStartedEventHandler(stepStartedEvent); break; case StepFinishedEvent stepFinishedEvent: - StepFinishedEventHandler(stepFinishedEvent); + await StepFinishedEventHandler(stepFinishedEvent); break; case HookBindingStartedEvent hookBindingStartedEvent: - HookBindingStartedEventHandler(hookBindingStartedEvent); + await HookBindingStartedEventHandler(hookBindingStartedEvent); break; case HookBindingFinishedEvent hookBindingFinishedEvent: - HookBindingFinishedEventHandler(hookBindingFinishedEvent); + await HookBindingFinishedEventHandler(hookBindingFinishedEvent); break; case AttachmentAddedEvent attachmentAddedEvent: - AttachmentAddedEventHandler(attachmentAddedEvent); + await AttachmentAddedEventHandler(attachmentAddedEvent); break; case OutputAddedEvent outputAddedEvent: - OutputAddedEventHandler(outputAddedEvent); + await OutputAddedEventHandler(outputAddedEvent); break; default: break; @@ -137,7 +138,7 @@ private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEv } - private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { _broker = _brokerFactory.Value; var traceListener = objectContainer.Resolve(); @@ -163,12 +164,12 @@ private void FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { foreach (var msg in ft.StaticMessages) { - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + await _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } - private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) + private async Task FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { // For this and subsequent events, we pull up the FeatureTracker by feature name. // If the feature name is not avaiable (such as might be the case in certain test setups), we ignore the event. @@ -182,13 +183,13 @@ private void FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEve return; } var featureTracker = StartedFeatures[featureName]; - featureTracker.ProcessEvent(featureFinishedEvent); + await featureTracker.ProcessEvent(featureFinishedEvent); // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? } - private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + private async Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { var featureName = scenarioStartedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -198,7 +199,7 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve { if (featureTracker.Enabled) { - featureTracker.ProcessEvent(scenarioStartedEvent); + await featureTracker.ProcessEvent(scenarioStartedEvent); } else { @@ -212,7 +213,7 @@ private void ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve } } - private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + private async Task ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) { var featureName = scenarioFinishedEvent.FeatureContext?.FeatureInfo?.Title; @@ -220,14 +221,14 @@ private void ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinished return; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - foreach (var msg in featureTracker.ProcessEvent(scenarioFinishedEvent)) + foreach (var msg in await featureTracker.ProcessEvent(scenarioFinishedEvent)) { - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + await _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } - private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) + private async Task StepStartedEventHandler(StepStartedEvent stepStartedEvent) { var featureName = stepStartedEvent.FeatureContext?.FeatureInfo?.Title; @@ -236,11 +237,11 @@ private void StepStartedEventHandler(StepStartedEvent stepStartedEvent) if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(stepStartedEvent); + await featureTracker.ProcessEvent(stepStartedEvent); } } - private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) + private async Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { var featureName = stepFinishedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -248,11 +249,11 @@ private void StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(stepFinishedEvent); + await featureTracker.ProcessEvent(stepFinishedEvent); } } - private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + private async Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -260,11 +261,11 @@ private void HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingS if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(hookBindingStartedEvent); + await featureTracker.ProcessEvent(hookBindingStartedEvent); } } - private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) + private async Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { var featureName = hookBindingFinishedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -272,11 +273,11 @@ private void HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindin if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(hookBindingFinishedEvent); + await featureTracker.ProcessEvent(hookBindingFinishedEvent); } } - private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) + private async Task AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -284,11 +285,11 @@ private void AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEve if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(attachmentAddedEvent); + await featureTracker.ProcessEvent(attachmentAddedEvent); } } - private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) + private async Task OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) { var featureName = outputAddedEvent.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) @@ -296,7 +297,7 @@ private void OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - featureTracker.ProcessEvent(outputAddedEvent); + await featureTracker.ProcessEvent(outputAddedEvent); } } } diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index b905adec0..35b76052c 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -84,9 +84,9 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } private static byte[] nl = Encoding.UTF8.GetBytes(Environment.NewLine); - public void Publish(ReqnrollCucumberMessage message) + public async Task Publish(ReqnrollCucumberMessage message) { - postedMessages.Add(message); + await Task.Run( () => postedMessages.Add(message)); } private void ConsumeAndWriteToFilesBackgroundTask(string baseDirectory, string fileName) diff --git a/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs index 9a6987968..2cb2323c1 100644 --- a/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs @@ -5,6 +5,6 @@ namespace Reqnroll.CucumberMessages.PubSub { public interface ICucumberMessageSink { - void Publish(ReqnrollCucumberMessage message); + Task Publish(ReqnrollCucumberMessage message); } } diff --git a/Reqnroll/Events/IExecutionEventListener.cs b/Reqnroll/Events/IExecutionEventListener.cs index ec87491ba..8c7c12cf5 100644 --- a/Reqnroll/Events/IExecutionEventListener.cs +++ b/Reqnroll/Events/IExecutionEventListener.cs @@ -1,7 +1,14 @@ +using System.Threading.Tasks; + namespace Reqnroll.Events { public interface IExecutionEventListener { void OnEvent(IExecutionEvent executionEvent); } + + public interface IAsyncExecutionEventListener + { + Task OnEventAsync(IExecutionEvent executionEvent); + } } diff --git a/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs b/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs index b00b20753..e05fe3b12 100644 --- a/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs +++ b/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Reqnroll.Events { @@ -6,8 +7,12 @@ public interface ITestThreadExecutionEventPublisher { void PublishEvent(IExecutionEvent executionEvent); + Task PublishEventAsync(IExecutionEvent executionEvent); + void AddListener(IExecutionEventListener listener); + void AddAsyncListener(IAsyncExecutionEventListener listener); + void AddHandler(Action handler) where TEvent: IExecutionEvent; } } diff --git a/Reqnroll/Events/TestThreadExecutionEventPublisher.cs b/Reqnroll/Events/TestThreadExecutionEventPublisher.cs index 081263fc5..dddacc640 100644 --- a/Reqnroll/Events/TestThreadExecutionEventPublisher.cs +++ b/Reqnroll/Events/TestThreadExecutionEventPublisher.cs @@ -1,14 +1,22 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; namespace Reqnroll.Events { public class TestThreadExecutionEventPublisher : ITestThreadExecutionEventPublisher { private readonly List _listeners = new(); + private readonly List _asyncListeners = new(); private readonly Dictionary> _handlersDictionary = new(); public void PublishEvent(IExecutionEvent executionEvent) + { + Task.Run(async () => await PublishEventAsync(executionEvent)).Wait(); + } + + private void PublishSync(IExecutionEvent executionEvent) { foreach (var listener in _listeners) { @@ -24,11 +32,26 @@ public void PublishEvent(IExecutionEvent executionEvent) } } + public async Task PublishEventAsync(IExecutionEvent executionEvent) + { + PublishSync(executionEvent); + + foreach (var listener in _asyncListeners) + { + await listener.OnEventAsync(executionEvent); + } + } + public void AddListener(IExecutionEventListener listener) { _listeners.Add(listener); } + public void AddAsyncListener(IAsyncExecutionEventListener listener) + { + _asyncListeners.Add(listener); + } + public void AddHandler(Action handler) where TEvent : IExecutionEvent { if (!_handlersDictionary.TryGetValue(typeof(TEvent), out var handlers)) diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index 5ff414ca2..478495c95 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -117,7 +117,7 @@ public virtual async Task OnTestRunStartAsync() _testRunnerStartExecuted = true; - _testThreadExecutionEventPublisher.PublishEvent(new TestRunStartedEvent()); + await _testThreadExecutionEventPublisher.PublishEventAsync(new TestRunStartedEvent()); await FireEventsAsync(HookType.BeforeTestRun); } @@ -136,14 +136,14 @@ public virtual async Task OnTestRunEndAsync() await FireEventsAsync(HookType.AfterTestRun); - _testThreadExecutionEventPublisher.PublishEvent(new TestRunFinishedEvent()); + await _testThreadExecutionEventPublisher.PublishEventAsync(new TestRunFinishedEvent()); } public virtual async Task OnFeatureStartAsync(FeatureInfo featureInfo) { _contextManager.InitializeFeatureContext(featureInfo); - _testThreadExecutionEventPublisher.PublishEvent(new FeatureStartedEvent(FeatureContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureStartedEvent(FeatureContext)); await FireEventsAsync(HookType.BeforeFeature); } @@ -159,7 +159,7 @@ public virtual async Task OnFeatureEndAsync() _testTracer.TraceDuration(duration, "Feature: " + FeatureContext.FeatureInfo.Title); } - _testThreadExecutionEventPublisher.PublishEvent(new FeatureFinishedEvent(FeatureContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureFinishedEvent(FeatureContext)); _contextManager.CleanupFeatureContext(); } @@ -171,7 +171,7 @@ public virtual void OnScenarioInitialize(ScenarioInfo scenarioInfo) public virtual async Task OnScenarioStartAsync() { - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioStartedEvent(FeatureContext, ScenarioContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new ScenarioStartedEvent(FeatureContext, ScenarioContext)); try { @@ -234,7 +234,7 @@ public virtual async Task OnScenarioEndAsync() } finally { - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); _contextManager.CleanupScenarioContext(); } @@ -304,7 +304,7 @@ protected virtual async Task FireScenarioEventsAsync(HookType bindingEvent) private async Task FireEventsAsync(HookType hookType) { - _testThreadExecutionEventPublisher.PublishEvent(new HookStartedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookStartedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext)); var stepContext = _contextManager.GetStepContext(); var matchingHooks = _bindingRegistry.GetHooks(hookType) @@ -336,7 +336,7 @@ private async Task FireEventsAsync(HookType hookType) //A plugin-hook should not throw an exception under normal circumstances, exceptions are not handled/caught here FireRuntimePluginTestExecutionLifecycleEvents(hookType); - _testThreadExecutionEventPublisher.PublishEvent(new HookFinishedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext, hookException)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookFinishedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext, hookException)); //Note: the (user-)hook exception (if any) will be thrown after the plugin hooks executed to fail the test with the right error if (hookException != null) ExceptionDispatchInfo.Capture(hookException).Throw(); @@ -354,7 +354,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin var currentContainer = GetHookContainer(hookType); var arguments = ResolveArguments(hookBinding, currentContainer); - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding, _contextManager)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); Exception exceptionthrown = null; try @@ -371,7 +371,7 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager, exceptionthrown)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager, exceptionthrown)); } } @@ -558,7 +558,7 @@ protected virtual BindingMatch GetStepMatch(StepInstance stepInstance) protected virtual async Task ExecuteStepMatchAsync(BindingMatch match, object[] arguments, DurationHolder durationHolder) { - _testThreadExecutionEventPublisher.PublishEvent(new StepBindingStartedEvent(match.StepBinding)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepBindingStartedEvent(match.StepBinding)); try { @@ -566,7 +566,7 @@ protected virtual async Task ExecuteStepMatchAsync(BindingMatch match, object[] } finally { - _testThreadExecutionEventPublisher.PublishEvent(new StepBindingFinishedEvent(match.StepBinding, durationHolder.Duration)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepBindingFinishedEvent(match.StepBinding, durationHolder.Duration)); } } @@ -631,7 +631,7 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, var pickleStepId = stepSequenceIdentifiers?.CurrentPickleStepId ?? ""; _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); - _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); try { @@ -640,7 +640,7 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, } finally { - _testThreadExecutionEventPublisher.PublishEvent(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); stepSequenceIdentifiers?.NextStep(); _contextManager.CleanupStepContext(); } diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs index d2950c4a3..ce2ee756a 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/TestThreadExecutionEventPublisherTests.cs @@ -20,7 +20,7 @@ public async Task Should_publish_step_started_event() await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "foo", null, null); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && + te.PublishEventAsync(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && e.FeatureContext.Equals(featureContainer.Resolve()) && e.StepContext.Equals(contextManagerStub.Object.StepContext))), Times.Once); @@ -35,7 +35,7 @@ public async Task Should_publish_step_binding_started_event() await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "foo", null, null); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.StepDefinitionBinding.Equals(stepDef))), Times.Once); } @@ -54,7 +54,7 @@ public async Task Should_publish_step_binding_finished_event() await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "foo", null, null); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.StepDefinitionBinding.Equals(stepDef) && e.Duration.Equals(expectedDuration))), Times.Once); @@ -68,7 +68,7 @@ public async Task Should_publish_step_finished_event() await testExecutionEngine.StepAsync(StepDefinitionKeyword.Given, null, "foo", null, null); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && e.FeatureContext.Equals(featureContainer.Resolve()) && e.StepContext.Equals(contextManagerStub.Object.StepContext))), @@ -104,11 +104,11 @@ public async Task Should_publish_hook_binding_events() await testExecutionEngine.InvokeHookAsync(methodBindingInvokerMock.Object, expectedHookBinding, hookType); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.HookBinding.Equals(expectedHookBinding))), Times.Once); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.HookBinding.Equals(expectedHookBinding) && e.Duration.Equals(expectedDuration))), Times.Once); @@ -123,7 +123,7 @@ public async Task Should_publish_scenario_started_event() await testExecutionEngine.OnScenarioStartAsync(); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && e.FeatureContext.Equals(featureContainer.Resolve()))), Times.Once); @@ -140,7 +140,7 @@ public async Task Should_publish_scenario_finished_event() await testExecutionEngine.OnScenarioEndAsync(); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && e.FeatureContext.Equals(featureContainer.Resolve()))), Times.Once); @@ -192,7 +192,7 @@ public async Task Should_publish_scenario_skipped_event() _testThreadExecutionEventPublisher.Verify(te => te.PublishEvent(It.IsAny()), Times.Once); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.ScenarioContext.Equals(scenarioContext) && e.FeatureContext.Equals(featureContainer.Resolve()))), Times.Once); @@ -206,7 +206,7 @@ public async Task Should_publish_feature_started_event() await testExecutionEngine.OnFeatureStartAsync(featureInfo); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.FeatureContext.Equals(featureContainer.Resolve()))), Times.Once); } @@ -219,7 +219,7 @@ public async Task Should_publish_feature_finished_event() await testExecutionEngine.OnFeatureEndAsync(); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.Is(e => + te.PublishEventAsync(It.Is(e => e.FeatureContext.Equals(featureContainer.Resolve()))), Times.Once); } @@ -232,7 +232,7 @@ public async Task Should_publish_testrun_started_event() await testExecutionEngine.OnTestRunStartAsync(); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.IsAny()), Times.Once); + te.PublishEventAsync(It.IsAny()), Times.Once); } [Fact] @@ -243,18 +243,18 @@ public async Task Should_publish_testrun_finished_event() await testExecutionEngine.OnTestRunEndAsync(); _testThreadExecutionEventPublisher.Verify(te => - te.PublishEvent(It.IsAny()), Times.Once); + te.PublishEventAsync(It.IsAny()), Times.Once); } private void AssertHookEventsForHookType(HookType hookType) { _testThreadExecutionEventPublisher.Verify( te => - te.PublishEvent(It.Is(e => e.HookType == hookType)), + te.PublishEventAsync(It.Is(e => e.HookType == hookType)), Times.Once); _testThreadExecutionEventPublisher.Verify( te => - te.PublishEvent(It.Is(e => e.HookType == hookType)), + te.PublishEventAsync(It.Is(e => e.HookType == hookType)), Times.Once); } } From 92b43e414c2aaf0868191faf3c8b3895e855a70b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:38:38 -0500 Subject: [PATCH 200/218] Revised async implementation so that only the outer API is async (Publisher, Broker, and Sink). Returned FeatureTracker back to normal, sync methods. --- .../ExecutionTracking/FeatureTracker.cs | 54 ++++------- .../PubSub/CucumberMessageBroker.cs | 6 +- .../PubSub/CucumberMessagePublisher.cs | 90 ++++++++++++------- .../PubSub/FileOutputPlugin.cs | 4 +- .../PubSub/ICucumberMessageSink.cs | 2 +- 5 files changed, 81 insertions(+), 75 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 763da077f..b9ed58239 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -157,7 +157,7 @@ private string ExtractLastID(List pickles) } // When the FeatureFinished event fires, we calculate the Feature-level Execution Status - public Task ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) { var testCases = testCaseTrackersById.Values.ToList(); @@ -167,15 +167,13 @@ public Task ProcessEvent(FeatureFinishedEvent featureFinishedEvent) true => testCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK), _ => true }; - - return Task.CompletedTask; } - public Task ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { // as in the Publisher, we're using defensive coding here b/c some test setups might not have complete info var pickleIndex = scenarioStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleIndex)) return; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -187,14 +185,12 @@ public Task ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) tccmt.ProcessEvent(scenarioStartedEvent); testCaseTrackersById.TryAdd(pickleId, tccmt); } - return Task.CompletedTask; - } - public Task> ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + public IEnumerable ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) { var pickleIndex = scenarioFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.FromResult(Enumerable.Empty()); + if (String.IsNullOrEmpty(pickleIndex)) return Enumerable.Empty(); if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -202,17 +198,17 @@ public Task> ProcessEvent(ScenarioFinishedEvent scenarioFi { tccmt.ProcessEvent(scenarioFinishedEvent); - return Task.FromResult(tccmt.TestCaseCucumberMessages()); + return tccmt.TestCaseCucumberMessages(); } } - return Task.FromResult(Enumerable.Empty()); + return Enumerable.Empty(); } - public Task ProcessEvent(StepStartedEvent stepStartedEvent) + public void ProcessEvent(StepStartedEvent stepStartedEvent) { var pickleIndex = stepStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleIndex)) return; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { @@ -221,15 +217,13 @@ public Task ProcessEvent(StepStartedEvent stepStartedEvent) tccmt.ProcessEvent(stepStartedEvent); } } - - return Task.CompletedTask; } - public Task ProcessEvent(StepFinishedEvent stepFinishedEvent) + public void ProcessEvent(StepFinishedEvent stepFinishedEvent) { var pickleIndex = stepFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleIndex)) return; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) @@ -237,66 +231,56 @@ public Task ProcessEvent(StepFinishedEvent stepFinishedEvent) tccmt.ProcessEvent(stepFinishedEvent); } } - - return Task.CompletedTask; } - public Task ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { var pickleIndex = hookBindingStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleIndex)) return; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) tccmt.ProcessEvent(hookBindingStartedEvent); } - - return Task.CompletedTask; } - public Task ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + public void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { var pickleIndex = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleIndex)) return; if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) tccmt.ProcessEvent(hookBindingFinishedEvent); } - - return Task.CompletedTask; } - public Task ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) { var pickleId = attachmentAddedEvent.FeatureInfo?.CucumberMessages_PickleId; - if (String.IsNullOrEmpty(pickleId)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleId)) return; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { tccmt.ProcessEvent(attachmentAddedEvent); } - - return Task.CompletedTask; } - public Task ProcessEvent(OutputAddedEvent outputAddedEvent) + public void ProcessEvent(OutputAddedEvent outputAddedEvent) { var pickleId = outputAddedEvent.FeatureInfo?.CucumberMessages_PickleId; - if (String.IsNullOrEmpty(pickleId)) return Task.CompletedTask; + if (String.IsNullOrEmpty(pickleId)) return; if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) { tccmt.ProcessEvent(outputAddedEvent); } - - return Task.CompletedTask; } } } \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs index e36426ca0..8b8b86cd4 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -16,7 +16,7 @@ namespace Reqnroll.CucumberMessages.PubSub public interface ICucumberMessageBroker { bool Enabled { get; } - Task Publish(ReqnrollCucumberMessage featureMessages); + Task PublishAsync(ReqnrollCucumberMessage featureMessages); } /// @@ -38,11 +38,11 @@ public CucumberMessageBroker(IObjectContainer objectContainer) _objectContainer = objectContainer; RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); } - public async Task Publish(ReqnrollCucumberMessage message) + public async Task PublishAsync(ReqnrollCucumberMessage message) { foreach (var sink in RegisteredSinks.Value) { - await sink.Publish(message); + await sink.PublishAsync(message); } } diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 1872e7eb3..95222df0d 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -126,18 +126,25 @@ private void PublisherStartup(object sender, RuntimePluginBeforeTestRunEventArgs SharedIDGenerator = IdGeneratorFactory.Create(CucumberConfiguration.Current.IDGenerationStyle); - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now)) }); - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = CucumberMessageFactory.ToMeta(args.ObjectContainer) }); + Task.Run(async () => + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now)) }); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = CucumberMessageFactory.ToMeta(args.ObjectContainer) }); + }).Wait(); } private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEventArgs e) { if (!Enabled) return; var status = StartedFeatures.Values.All(f => f.FeatureExecutionSuccess); - _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) }); + + Task.Run(async () => + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) })).Wait(); } - + #region TestThreadExecutionEventPublisher Event Handling Methods + // The following methods handle the events published by the TestThreadExecutionEventPublisher + // When one of these calls the Broker, that method is async; otherwise these are sync methods that return a completed Task (to allow them to be called async from the TestThreadExecutionEventPublisher) private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { _broker = _brokerFactory.Value; @@ -164,46 +171,46 @@ private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStarted { foreach (var msg in ft.StaticMessages) { - await _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } - private async Task FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) + private Task FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) { // For this and subsequent events, we pull up the FeatureTracker by feature name. // If the feature name is not avaiable (such as might be the case in certain test setups), we ignore the event. var featureName = featureFinishedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) { - return; + return Task.CompletedTask; } if (!StartedFeatures.ContainsKey(featureName) || !StartedFeatures[featureName].Enabled) { - return; + return Task.CompletedTask; } var featureTracker = StartedFeatures[featureName]; - await featureTracker.ProcessEvent(featureFinishedEvent); - + featureTracker.ProcessEvent(featureFinishedEvent); + return Task.CompletedTask; // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? } - private async Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + private Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) { var featureName = scenarioStartedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; var traceListener = objectContainer.Resolve(); if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { if (featureTracker.Enabled) { - await featureTracker.ProcessEvent(scenarioStartedEvent); + featureTracker.ProcessEvent(scenarioStartedEvent); } else { - return; + return Task.CompletedTask; } } else @@ -211,6 +218,8 @@ private async Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStar traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker not available"); throw new ApplicationException("FeatureTracker not available"); } + + return Task.CompletedTask; } private async Task ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) @@ -221,84 +230,97 @@ private async Task ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFi return; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - foreach (var msg in await featureTracker.ProcessEvent(scenarioFinishedEvent)) + foreach (var msg in featureTracker.ProcessEvent(scenarioFinishedEvent)) { - await _broker.Publish(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); } } } - private async Task StepStartedEventHandler(StepStartedEvent stepStartedEvent) + private Task StepStartedEventHandler(StepStartedEvent stepStartedEvent) { var featureName = stepStartedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(stepStartedEvent); + featureTracker.ProcessEvent(stepStartedEvent); } + + return Task.CompletedTask; } - private async Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) + private Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) { var featureName = stepFinishedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(stepFinishedEvent); + featureTracker.ProcessEvent(stepFinishedEvent); } + + return Task.CompletedTask; } - private async Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + private Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(hookBindingStartedEvent); + featureTracker.ProcessEvent(hookBindingStartedEvent); } + + return Task.CompletedTask; } - private async Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) + private Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { var featureName = hookBindingFinishedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(hookBindingFinishedEvent); + featureTracker.ProcessEvent(hookBindingFinishedEvent); } + + return Task.CompletedTask; } - private async Task AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) + private Task AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) { var featureName = attachmentAddedEvent.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(attachmentAddedEvent); + featureTracker.ProcessEvent(attachmentAddedEvent); } + + return Task.CompletedTask; } - private async Task OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) + private Task OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) { var featureName = outputAddedEvent.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return; + return Task.CompletedTask; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { - await featureTracker.ProcessEvent(outputAddedEvent); + featureTracker.ProcessEvent(outputAddedEvent); } + + return Task.CompletedTask; } + #endregion } } diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs index 35b76052c..a8bef8185 100644 --- a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -84,7 +84,7 @@ private void LaunchFileSink(TestRunStartedEvent testRunStarted) globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); } private static byte[] nl = Encoding.UTF8.GetBytes(Environment.NewLine); - public async Task Publish(ReqnrollCucumberMessage message) + public async Task PublishAsync(ReqnrollCucumberMessage message) { await Task.Run( () => postedMessages.Add(message)); } @@ -130,7 +130,7 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - public static string SanitizeFileName(string input) + private static string SanitizeFileName(string input) { if (string.IsNullOrEmpty(input)) return string.Empty; diff --git a/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs index 2cb2323c1..df04e6243 100644 --- a/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs +++ b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs @@ -5,6 +5,6 @@ namespace Reqnroll.CucumberMessages.PubSub { public interface ICucumberMessageSink { - Task Publish(ReqnrollCucumberMessage message); + Task PublishAsync(ReqnrollCucumberMessage message); } } From 7feaa082abf1ab19e7ee34220c2b0048ea4f9c8f Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:52:21 -0500 Subject: [PATCH 201/218] Fix to the NDJSONSerializer so that '+' sign in Messages enums is correctly serialized. --- Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index c3b6f5799..5afbadde3 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -24,7 +24,7 @@ public class NdjsonSerializer options.Converters.Add(new CucumberMessageEnumConverter()); options.Converters.Add(new CucumberMessageEnumConverter()); options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; - options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default; + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; return options; }); From f96fde9455efcd3c6d0bc0d3fc7fc93e9480928c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:57:24 -0500 Subject: [PATCH 202/218] Fix to FeatureFinished in the FeatureTracker so that if it happens that a FeatureFinishedEvent is received but not all Scenarios are yet marked as Finished, then the entire Feature success status should be set to False. --- Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index b9ed58239..1e5d5a5ae 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -165,7 +165,7 @@ public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) FeatureExecutionSuccess = testCases.All(tc => tc.Finished) switch { true => testCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK), - _ => true + _ => false }; } From f42771ecef13e4b323747dc41eb36b40347169b3 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:20:15 -0500 Subject: [PATCH 203/218] Update to Microsoft.Extensions.DependencyModel version To match Main --- Reqnroll/Reqnroll.csproj | 2 +- Reqnroll/Reqnroll.nuspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index ab13e29f0..0336382b9 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -27,7 +27,7 @@ - + diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec index 742ab84f2..410be6d92 100644 --- a/Reqnroll/Reqnroll.nuspec +++ b/Reqnroll/Reqnroll.nuspec @@ -22,7 +22,7 @@ - + From 186ab24a457f0d9cf2b44100f001ede43e2d054a Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:45:21 -0500 Subject: [PATCH 204/218] Updated code comments to reflect design considerations for running scenarios in parallel --- .../CucumberMessages/ExecutionTracking/FeatureTracker.cs | 2 ++ Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index 1e5d5a5ae..b0f8115eb 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -157,6 +157,8 @@ private string ExtractLastID(List pickles) } // When the FeatureFinished event fires, we calculate the Feature-level Execution Status + // If Scenarios are running in parallel, this event will fire multiple times (once per each instance of the test class). + // Running this method multiple times is harmless. The FeatureExecutionSuccess property is only consumed upon the TestRunComplete event (ie, only once). public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) { var testCases = testCaseTrackersById.Values.ToList(); diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 95222df0d..5b1be8327 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -158,12 +158,17 @@ private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStarted return; } + // The following should be thread safe when multiple instances of the Test Class are running in parallel. + // If StartedFeatures.ContainsKey returns true, then we know another instance of this Feature class has already started. We don't need a second instance of the + // FeatureTracker, and we don't want multiple copies of the static messages to be published. if (StartedFeatures.ContainsKey(featureName)) { // Already started, don't repeat the following steps return; } + // Creating multiple copies of the same FeatureTracker is safe as it causes no side-effects. + // If two or more threads are running this code simultaneously, all but one of them will get created but then will be ignored. var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. From 919b81902a5e96a7f6aa94abd0287c3040f9ebf5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:26:16 -0500 Subject: [PATCH 205/218] Link Cucumber docs to main docs --- docs/installation/index.md | 1 + docs/integrations/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/installation/index.md b/docs/installation/index.md index acb150010..08e87aa5b 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -9,6 +9,7 @@ setup-project setup-ide configuration compatibility +../cucumber-messages/configuration ``` diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 1df6e41fd..4ef756cad 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -14,4 +14,5 @@ mstest nunit windsor xunit +../cucumber-messages/introduction ``` From 88facf1b59aa6bc7386f5a08b3786b428215df75 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:57:19 -0500 Subject: [PATCH 206/218] Fix multithreading bug in Gherkin Document rewriter that would occur when two Features were rewriting simultaneously. --- .../Gherkin/GherkinDocumentIDReWriter.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs index 087b87874..f449e130d 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs @@ -25,6 +25,8 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin /// internal class GherkinDocumentIDReWriter : CucumberMessage_TraversalVisitorBase { + internal static object _sharedLockObject = new(); + private IIdGenerator _idGenerator; public Dictionary IdMap = new(); public GherkinDocumentIDReWriter(IIdGenerator idGenerator) @@ -46,16 +48,24 @@ public GherkinDocument ReWriteIds(GherkinDocument document, IDGenerationStyle ta case (false, IDGenerationStyle.Incrementing): var lastId = ProbeForLastUsedId(document); - ((SeedableIncrementingIdGenerator)_idGenerator).SetSeed(lastId); + var anotherThreadSetTheSeed = false; + lock (_sharedLockObject) + { + if (((SeedableIncrementingIdGenerator)_idGenerator).HasBeenUsed) + anotherThreadSetTheSeed = true; + else + { + ((SeedableIncrementingIdGenerator)_idGenerator).SetSeed(lastId); + } + } + + if (anotherThreadSetTheSeed) + return ReWrite(document); return document; } } // else targetStyle is IDGenerationStyle.UUID - if (existingIdStyle == IDGenerationStyle.UUID) - return document; - - // else existingIdStyle is IDGenerationStyle.Incrementing - return ReWrite(document); + return document; } private GherkinDocument ReWrite(GherkinDocument document) From 0fb0ddc4267ca3d57d65b06e5fc62855c33563d5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:35:51 -0500 Subject: [PATCH 207/218] Code Adapted to Messages v27; support for TestRunStartedId, HookType Enums, and TestHookStarted/Finished messages. CCK compliance not yet available. --- .../ExecutionTracking/FeatureTracker.cs | 54 +--------- .../ExecutionTracking/TestCaseTracker.cs | 12 +-- .../ExecutionTracking/TestRunHookTracker.cs | 22 ++++ .../Cucumber/CucumberMessageFactory.cs | 49 +++++++-- .../Gherkin/GherkinDocumentIDReWriter.cs | 2 +- .../PubSub/CucumberMessagePublisher.cs | 102 ++++++++++++++++-- Reqnroll/Reqnroll.csproj | 2 +- Reqnroll/Reqnroll.nuspec | 2 +- .../CucumberMessages.Tests.csproj | 2 +- 9 files changed, 167 insertions(+), 80 deletions(-) create mode 100644 Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index b0f8115eb..f84d62485 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -29,11 +29,12 @@ public class FeatureTracker // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. public IIdGenerator IDGenerator { get; set; } + public string TestRunStartedId { get; } // This dictionary tracks the StepDefintions(ID) by their method signature // used during TestCase creation to map from a Step Definition binding to its ID // This dictionary is shared across all Features (via the Publisher) - // The same is true of hte StepTransformations and StepDefinitionBindings used for Undefined Parameter Types + // The same is true of the StepTransformations and StepDefinitionBindings used for Undefined Parameter Types internal ConcurrentDictionary StepDefinitionsByPattern = new(); private ConcurrentBag StepTransformRegistry; private ConcurrentBag UndefinedParameterTypes; @@ -51,8 +52,9 @@ public class FeatureTracker public bool FeatureExecutionSuccess { get; private set; } // This constructor is used by the Publisher when it sees a Feature (by name) for the first time - public FeatureTracker(FeatureStartedEvent featureStartedEvent, IIdGenerator idGenerator, ConcurrentDictionary stepDefinitionPatterns, ConcurrentBag stepTransformRegistry, ConcurrentBag undefinedParameterTypes) + public FeatureTracker(FeatureStartedEvent featureStartedEvent, string testRunStartedId, IIdGenerator idGenerator, ConcurrentDictionary stepDefinitionPatterns, ConcurrentBag stepTransformRegistry, ConcurrentBag undefinedParameterTypes) { + TestRunStartedId = testRunStartedId; StepDefinitionsByPattern = stepDefinitionPatterns; StepTransformRegistry = stepTransformRegistry; UndefinedParameterTypes = undefinedParameterTypes; @@ -95,54 +97,6 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature yield return Envelope.Create(pickle); } - var bindingRegistry = featureStartedEvent.FeatureContext.FeatureContainer.Resolve(); - - foreach (var stepTransform in bindingRegistry.GetStepTransformations()) - { - if (StepTransformRegistry.Contains(stepTransform)) - continue; - StepTransformRegistry.Add(stepTransform); - var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, IDGenerator); - yield return Envelope.Create(parameterType); - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) - { - var errmsg = binding.ErrorMessage; - if (errmsg.Contains("Undefined parameter type")) - { - var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; - if (UndefinedParameterTypes.Contains(binding)) - continue; - UndefinedParameterTypes.Add(binding); - var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, IDGenerator); - yield return Envelope.Create(undefinedParameterType); - } - } - - foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) - { - var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); - if (StepDefinitionsByPattern.ContainsKey(pattern)) - continue; - var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, IDGenerator); - if (StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id)) - { - yield return Envelope.Create(stepDefinition); - } - } - - foreach (var hookBinding in bindingRegistry.GetHooks()) - { - var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); - if (StepDefinitionsByPattern.ContainsKey(hookId)) - continue; - var hook = CucumberMessageFactory.ToHook(hookBinding, IDGenerator); - if (StepDefinitionsByPattern.TryAdd(hookId, hook.Id)) - { - yield return Envelope.Create(hook); - }; - } } // This method is used to identify the last ID generated from the set generated during code gen. diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs index 4ed9fee5e..4cee3fcc3 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs @@ -20,6 +20,7 @@ public class TestCaseTracker { public TestCaseTracker(FeatureTracker featureTracker, string pickleId) { + TestRunStartedId = featureTracker.TestRunStartedId; PickleId = pickleId; FeatureName = featureTracker.FeatureName; Enabled = featureTracker.Enabled; @@ -30,6 +31,7 @@ public TestCaseTracker(FeatureTracker featureTracker, string pickleId) // Feature FeatureName and Pickle ID make up a unique identifier for tracking execution of Test Cases public string FeatureName { get; set; } + public string TestRunStartedId { get; } public string PickleId { get; set; } = string.Empty; public string TestCaseId { get; set; } public string TestCaseStartedId { get; private set; } @@ -218,9 +220,8 @@ internal IEnumerable PostProcessEvent(StepFinishedEvent stepFinishedEv internal void PreProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingStartedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingStartedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == HookType.AfterTestRun) + // At this point we only care about hooks that wrap scenarios or steps; Before/AfterTestRun hooks were processed earlier by the Publisher + if (hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.AfterFeature || hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.BeforeFeature) return; var step = new HookStepTracker(this); step.ProcessEvent(hookBindingStartedEvent); @@ -237,9 +238,8 @@ internal IEnumerable PostProcessEvent(HookBindingStartedEvent hookBind internal void PreProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) { - // At this point we only care about hooks that wrap scenarios or steps - if (hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeFeature - || hookBindingFinishedEvent.HookBinding.HookType == HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == HookType.AfterTestRun) + // At this point we only care about hooks that wrap scenarios or steps; TestRunHooks were processed earlier by the Publisher + if (hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.AfterFeature || hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.BeforeFeature) return; var step = FindMatchingHookStartedEvent(hookBindingFinishedEvent); diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs new file mode 100644 index 000000000..3876f1a62 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs @@ -0,0 +1,22 @@ +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + internal class TestRunHookTracker + { + public TestRunHookTracker(string id, HookBindingStartedEvent hookBindingStartedEvent, string testRunID) + { + TestRunHookId = id; + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); + TestRunID = testRunID; + } + + public string TestRunHookId { get; } + public string HookBindingSignature { get; } + public string TestRunID { get; } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs index 520829d8a..29f838452 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -1,12 +1,10 @@ using Cucumber.Messages; using Gherkin.CucumberMessages; using Io.Cucumber.Messages.Types; -using Reqnroll.Analytics; using Reqnroll.Bindings; using Reqnroll.BoDi; using Reqnroll.CommonModels; using Reqnroll.CucumberMessages.ExecutionTracking; -using Reqnroll.CucumberMessages.PayloadProcessing; using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.EnvironmentAccess; using Reqnroll.Events; @@ -26,15 +24,26 @@ namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber /// internal class CucumberMessageFactory { - public static TestRunStarted ToTestRunStarted(DateTime timestamp) + public static TestRunStarted ToTestRunStarted(DateTime timestamp, string id) { - return new TestRunStarted(Converters.ToTimestamp(timestamp)); + return new TestRunStarted(Converters.ToTimestamp(timestamp), id); } - public static TestRunFinished ToTestRunFinished(bool testRunStatus, DateTime timestamp) + public static TestRunFinished ToTestRunFinished(bool testRunStatus, DateTime timestamp, string testRunStartedId) { - return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(timestamp), null); + return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(timestamp), null, testRunStartedId); } + + internal static Envelope ToTestRunHookStarted(TestRunHookTracker hookTracker) + { + throw new NotImplementedException(); + } + + internal static Envelope ToTestRunHookFinished(TestRunHookTracker hookTracker) + { + throw new NotImplementedException(); + } + internal static TestCase ToTestCase(TestCaseTracker testCaseTracker, ScenarioStartedEvent scenarioStartedEvent) { var testSteps = new List(); @@ -59,7 +68,8 @@ internal static TestCase ToTestCase(TestCaseTracker testCaseTracker, ScenarioSta ( testCaseTracker.TestCaseId, testCaseTracker.PickleId, - testSteps + testSteps, + testCaseTracker.TestRunStartedId ); return testCase; } @@ -184,11 +194,28 @@ internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) iDGenerator.GetNewId(), null, sourceRef, - hookBinding.IsScoped ? $"@{hookBinding.BindingScope.Tag}" : null + hookBinding.IsScoped ? $"@{hookBinding.BindingScope.Tag}" : null, + ToHookType(hookBinding) ); return result; } + internal static Io.Cucumber.Messages.Types.HookType ToHookType(IHookBinding hookBinding) + { + return hookBinding.HookType switch + { + Bindings.HookType.BeforeTestRun => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_RUN, + Bindings.HookType.AfterTestRun => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_RUN, + Bindings.HookType.BeforeScenario => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_CASE, + Bindings.HookType.AfterScenario => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_CASE, + Bindings.HookType.BeforeStep => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_STEP, + Bindings.HookType.AfterStep => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_STEP, + + // Note: The following isn't strictly correct, but about all that can be done given that Cucumber doesn't support any other types of Hooks + _ => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_RUN + }; + } + internal static TestStep ToHookTestStep(HookStepTracker hookStepState) { // find the Hook message at the Feature level @@ -222,7 +249,8 @@ internal static Attachment ToAttachment(TestCaseTracker tracker, AttachmentAdded null, attachmentAddedEventWrapper.TestCaseStartedID, attachmentAddedEventWrapper.TestCaseStepID, - null); + null, + tracker.TestRunStartedId); } internal static Attachment ToAttachment(TestCaseTracker tracker, OutputAddedEventWrapper outputAddedEventWrapper) { @@ -234,7 +262,8 @@ internal static Attachment ToAttachment(TestCaseTracker tracker, OutputAddedEven null, outputAddedEventWrapper.TestCaseStartedID, outputAddedEventWrapper.TestCaseStepID, - null); + null, + tracker.TestRunStartedId); } private static TestStepResult ToTestStepResult(StepExecutionTrackerBase stepState) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs index f449e130d..d48e6b6dd 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinDocumentIDReWriter.cs @@ -47,10 +47,10 @@ public GherkinDocument ReWriteIds(GherkinDocument document, IDGenerationStyle ta return ReWrite(document); case (false, IDGenerationStyle.Incrementing): - var lastId = ProbeForLastUsedId(document); var anotherThreadSetTheSeed = false; lock (_sharedLockObject) { + var lastId = ProbeForLastUsedId(document); if (((SeedableIncrementingIdGenerator)_idGenerator).HasBeenUsed) anotherThreadSetTheSeed = true; else diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 5b1be8327..f557d5b0e 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -15,6 +15,8 @@ using Gherkin.CucumberMessages; using Reqnroll.Bindings; using System.Threading.Tasks; +using Reqnroll.EnvironmentAccess; +using System.Text.RegularExpressions; namespace Reqnroll.CucumberMessages.PubSub { @@ -43,9 +45,12 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi private ConcurrentBag UndefinedParameterTypeBindings = new(); public IIdGenerator SharedIDGenerator { get; private set; } - + private string _testRunStartedId; bool Enabled = false; + // This tracks the set of BeforeTestRun and AfterTestRun hooks that were called during the test run + private readonly ConcurrentDictionary TestRunHookTrackers = new(); + public CucumberMessagePublisher() { } @@ -125,13 +130,70 @@ private void PublisherStartup(object sender, RuntimePluginBeforeTestRunEventArgs } SharedIDGenerator = IdGeneratorFactory.Create(CucumberConfiguration.Current.IDGenerationStyle); + _testRunStartedId = SharedIDGenerator.GetNewId(); Task.Run(async () => { - await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now)) }); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now, _testRunStartedId)) }); await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = CucumberMessageFactory.ToMeta(args.ObjectContainer) }); + foreach (var msg in PopulateBindingCachesAndGenerateBindingMessages(args.ObjectContainer)) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = msg }); + } }).Wait(); } + private IEnumerable PopulateBindingCachesAndGenerateBindingMessages(IObjectContainer objectContainer) + { + var bindingRegistry = objectContainer.Resolve(); + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + if (StepArgumentTransforms.Contains(stepTransform)) + continue; + StepArgumentTransforms.Add(stepTransform); + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, SharedIDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) + { + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) + { + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + if (UndefinedParameterTypeBindings.Contains(binding)) + continue; + UndefinedParameterTypeBindings.Add(binding); + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, SharedIDGenerator); + yield return Envelope.Create(undefinedParameterType); + } + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + if (StepDefinitionsByPattern.ContainsKey(pattern)) + continue; + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, SharedIDGenerator); + if (StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id)) + { + yield return Envelope.Create(stepDefinition); + } + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + if (StepDefinitionsByPattern.ContainsKey(hookId)) + continue; + var hook = CucumberMessageFactory.ToHook(hookBinding, SharedIDGenerator); + if (StepDefinitionsByPattern.TryAdd(hookId, hook.Id)) + { + yield return Envelope.Create(hook); + }; + } + + } private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEventArgs e) { if (!Enabled) @@ -139,7 +201,8 @@ private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEv var status = StartedFeatures.Values.All(f => f.FeatureExecutionSuccess); Task.Run(async () => - await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) })).Wait(); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now, _testRunStartedId)) }) + ).Wait(); } #region TestThreadExecutionEventPublisher Event Handling Methods @@ -169,7 +232,7 @@ private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStarted // Creating multiple copies of the same FeatureTracker is safe as it causes no side-effects. // If two or more threads are running this code simultaneously, all but one of them will get created but then will be ignored. - var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); + var ft = new FeatureTracker(featureStartedEvent, _testRunStartedId, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) @@ -271,32 +334,51 @@ private Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) return Task.CompletedTask; } - private Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + private async Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { + if (hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun) + { + string hookId = SharedIDGenerator.GetNewId(); + var hookTracker = new TestRunHookTracker(hookId, hookBindingStartedEvent, _testRunStartedId); + TestRunHookTrackers.TryAdd(hookTracker.HookBindingSignature, hookTracker); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "testRunHook", Envelope = CucumberMessageFactory.ToTestRunHookStarted(hookTracker) }); + return; + } + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return Task.CompletedTask; + return; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(hookBindingStartedEvent); } - return Task.CompletedTask; + return; } - private Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) + private async Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { + if (hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun) + { + var signature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingFinishedEvent.HookBinding); + if (!TestRunHookTrackers.TryGetValue(signature, out var hookTracker)) + return; + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "testRunHook", Envelope = CucumberMessageFactory.ToTestRunHookFinished(hookTracker) }); + return; + } + + var featureName = hookBindingFinishedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) - return Task.CompletedTask; + return; if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { featureTracker.ProcessEvent(hookBindingFinishedEvent); } - return Task.CompletedTask; + return; } private Task AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 8d650ff52..7c8b4dcc1 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -26,7 +26,7 @@ - + diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec index 410be6d92..55e649792 100644 --- a/Reqnroll/Reqnroll.nuspec +++ b/Reqnroll/Reqnroll.nuspec @@ -20,7 +20,7 @@ - + diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj index 0f354db4e..7ba711bf8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj @@ -154,7 +154,7 @@ - + From cc5b70929af2a32079bf8ac3f7f63356666c6af5 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:22:20 -0500 Subject: [PATCH 208/218] Marked the older constructors for HookBinding, OutputAdded, and AttachmentAdded events as Obsolete. --- Reqnroll/Events/ExecutionEvent.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index e9b591e2d..b3abaf4c5 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -177,13 +177,15 @@ public class HookBindingStartedEvent : ExecutionEvent public IHookBinding HookBinding { get; } public IContextManager ContextManager { get; private set; } + [Obsolete("Use HookBindingStartedEvent(IHookBinding, IContextManager) instead")] public HookBindingStartedEvent(IHookBinding hookBinding) { HookBinding = hookBinding; } - public HookBindingStartedEvent(IHookBinding hookBinding, IContextManager contextManager) : this(hookBinding) + public HookBindingStartedEvent(IHookBinding hookBinding, IContextManager contextManager) { + HookBinding = hookBinding; ContextManager = contextManager; } } @@ -196,14 +198,17 @@ public class HookBindingFinishedEvent : ExecutionEvent public IContextManager ContextManager { get; private set; } public Exception HookException { get; private set; } + [Obsolete("Use HookBindingFinishedEvent(IHookBinding, TimeSpan, IContextManager) instead")] public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { HookBinding = hookBinding; Duration = duration; } - public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager, Exception hookException = null) : this(hookBinding, duration) + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager, Exception hookException = null) { + HookBinding = hookBinding; + Duration = duration; ContextManager = contextManager; HookException = hookException; } @@ -219,13 +224,15 @@ public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent public string ScenarioName { get; } public string StepText { get; } + [Obsolete("Use OutputAddedEvent(string, FeatureInfo) instead")] public OutputAddedEvent(string text) { Text = text; } - public OutputAddedEvent(string text, FeatureInfo featureInfo) : this(text) + public OutputAddedEvent(string text, FeatureInfo featureInfo) { + Text = text; FeatureInfo = featureInfo; } } @@ -234,13 +241,16 @@ public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } public FeatureInfo FeatureInfo { get; } + + [Obsolete("Use AttachmentAddedEvent(string, FeatureInfo) instead")] public AttachmentAddedEvent(string filePath) { FilePath = filePath; } - public AttachmentAddedEvent(string filePath, FeatureInfo featureInfo) : this(filePath) + public AttachmentAddedEvent(string filePath, FeatureInfo featureInfo) { + FilePath = filePath; FeatureInfo = featureInfo; } } From 452ac85c812587919bc68d3cbd11218b467e8f83 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:55:12 -0600 Subject: [PATCH 209/218] Refactored Test Class generation to match Reqnroll v2.2 changes in Feature class start-up. Modified FeatureInfo constructor to accept the FeatureMessages object as optional parameter. Had to modify a Runtime test to accomodate that change. Also changed Generation such that we're more thoroughly emitting the 'global::' prefix on Reqnroll types (for Messages related types). Marked the Non-Compliant CCK test scenarios as [Ignore]. --- .../Generation/UnitTestFeatureGenerator.cs | 117 ++++++++---------- Reqnroll/FeatureInfo.cs | 5 +- .../CucumberCompatibilityTests.cs | 1 + .../Bindings/StepContextTests.cs | 2 +- 4 files changed, 54 insertions(+), 71 deletions(-) diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index abe6cd770..cac70e790 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -29,6 +29,7 @@ public class UnitTestFeatureGenerator : IFeatureGenerator private readonly UnitTestMethodGenerator _unitTestMethodGenerator; private readonly LinePragmaHandler _linePragmaHandler; private readonly ICucumberConfiguration _cucumberConfiguration; + private CodeMemberMethod _cucumberMessagesInitializeMethod; public UnitTestFeatureGenerator( IUnitTestGeneratorProvider testGeneratorProvider, @@ -197,6 +198,7 @@ private void DeclareFeatureInfoMember(TestClassGenerationContext generationConte new CodeFieldReferenceExpression( new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(ProgrammingLanguage))), _codeDomHelper.TargetLanguage.ToString()), + new CodeMethodInvokeExpression(null, _cucumberMessagesInitializeMethod.Name), new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)); generationContext.TestClass.Members.Add(featureInfoField); @@ -213,32 +215,53 @@ private void DeclareFeatureMessagesFactoryMembers(TestClassGenerationContext gen CodeDelegateCreateExpression picklesFunc; string sourceFileLocation; + sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); // Adding three static methods to the class: one each as Factory methods for source, gherkinDocument, and pickles Messages // Bodies of these methods are added later inside the try/catch block - sourceFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "SourceFunc"); - var sourceFuncBody = new CodeMemberMethod(); - sourceFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; - sourceFuncBody.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Source)); - sourceFuncBody.Name = sourceFunc.MethodName; - generationContext.TestClass.Members.Add(sourceFuncBody); - - gherkinDocumentFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "GherkinDocumentFunc"); - var gherkinDocumentFuncBody = new CodeMemberMethod(); - gherkinDocumentFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; - gherkinDocumentFuncBody.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.GherkinDocument)); - gherkinDocumentFuncBody.Name = gherkinDocumentFunc.MethodName; - generationContext.TestClass.Members.Add(gherkinDocumentFuncBody); - - picklesFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func>)), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "PicklesFunc"); - var picklesFuncBody = new CodeMemberMethod(); - picklesFuncBody.Attributes = MemberAttributes.Private | MemberAttributes.Static; - picklesFuncBody.ReturnType = new CodeTypeReference(typeof(System.Collections.Generic.IEnumerable)); - picklesFuncBody.Name = picklesFunc.MethodName; - generationContext.TestClass.Members.Add(picklesFuncBody); + sourceFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "SourceFunc"); + var sourceFactoryMethod = new CodeMemberMethod(); + sourceFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + sourceFactoryMethod.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Source), CodeTypeReferenceOptions.GlobalReference); + sourceFactoryMethod.Name = sourceFunc.MethodName; + generationContext.TestClass.Members.Add(sourceFactoryMethod); + + gherkinDocumentFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "GherkinDocumentFunc"); + var gherkinDocumentFactoryMethod = new CodeMemberMethod(); + gherkinDocumentFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + gherkinDocumentFactoryMethod.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.GherkinDocument), CodeTypeReferenceOptions.GlobalReference); + gherkinDocumentFactoryMethod.Name = gherkinDocumentFunc.MethodName; + generationContext.TestClass.Members.Add(gherkinDocumentFactoryMethod); + + picklesFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func>), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "PicklesFunc"); + var picklesFactoryMethod = new CodeMemberMethod(); + picklesFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + picklesFactoryMethod.ReturnType = new CodeTypeReference(typeof(System.Collections.Generic.IEnumerable), CodeTypeReferenceOptions.GlobalReference); + picklesFactoryMethod.Name = picklesFunc.MethodName; + generationContext.TestClass.Members.Add(picklesFactoryMethod); + + // Create a new method that will be added to the test class. + // It will be called to provide the FeatureCucumberMessages property value of the FeatureInfo object when that object is constructed + var CucumberMessagesInitializeMethod = new CodeMemberMethod(); + CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; + CucumberMessagesInitializeMethod.ReturnType = new CodeTypeReference(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages))); + generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); + _cucumberMessagesInitializeMethod = CucumberMessagesInitializeMethod; + + // Create a FeatureLevelCucumberMessages object and add it to featureInfo + var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), + sourceFunc, + gherkinDocumentFunc, + picklesFunc, + new CodePrimitiveExpression(sourceFileLocation)); + + CucumberMessagesInitializeMethod.Statements.Add( + new CodeMethodReturnStatement( + featureLevelCucumberMessagesExpression)); + try { - sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); // Cucumber IDs can be UUIDs or stringified integers. This is configurable by the user. var IDGenStyle = _cucumberConfiguration.IDGenerationStyle; @@ -253,7 +276,7 @@ private void DeclareFeatureMessagesFactoryMembers(TestClassGenerationContext gen sourceExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(Io.Cucumber.Messages.Types.Source)), new CodePrimitiveExpression(featureSource.Uri), new CodePrimitiveExpression(featureSource.Data), - new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(Io.Cucumber.Messages.Types.SourceMediaType)), featureSource.MediaType.ToString())); + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.SourceMediaType), CodeTypeReferenceOptions.GlobalReference)), featureSource.MediaType.ToString())); // generate a CodeDom expression to create the GherkinDocument object from the featureGherkinDocumentMessage var gherkinDocumentExpressionGenerator = new CucumberGherkinDocumentExpressionGenerator(_codeDomHelper); @@ -265,11 +288,11 @@ private void DeclareFeatureMessagesFactoryMembers(TestClassGenerationContext gen // wrap these expressions in Func - sourceFuncBody.Statements.Add(new CodeMethodReturnStatement(sourceExpression)); + sourceFactoryMethod.Statements.Add(new CodeMethodReturnStatement(sourceExpression)); - gherkinDocumentFuncBody.Statements.Add(new CodeMethodReturnStatement(gherkinDocumentExpression)); + gherkinDocumentFactoryMethod.Statements.Add(new CodeMethodReturnStatement(gherkinDocumentExpression)); - picklesFuncBody.Statements.Add(new CodeMethodReturnStatement(picklesExpression)); + picklesFactoryMethod.Statements.Add(new CodeMethodReturnStatement(picklesExpression)); } catch (Exception e) @@ -295,48 +318,6 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio _testGeneratorProvider.SetTestClassInitializeMethod(generationContext); } - // Generation of Cucumber Messages relies on access to the parsed AST. - private void PersistStaticCucumberMessagesToFeatureInfo(TestClassGenerationContext generationContext, CodeMemberMethod testClassInitializeMethod) - { - CodeObjectCreateExpression sourceExpression; - CodeExpression gherkinDocumentExpression; - CodeExpression picklesExpression; - CodeDelegateCreateExpression sourceFunc; - CodeDelegateCreateExpression gherkinDocumentFunc; - CodeDelegateCreateExpression picklesFunc; - - string sourceFileLocation; - - // Create a new method that will be added to the test class. It will be called to initialize the FeatureCucumberMessages property of the FeatureInfo object - var CucumberMessagesInitializeMethod = new CodeMemberMethod(); - CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; - CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; - CucumberMessagesInitializeMethod.Parameters.Add(new CodeParameterDeclarationExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureInfo)), "featureInfo")); - generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); - - // Create a FeatureLevelCucumberMessages object and add it to featureInfo - var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(FeatureLevelCucumberMessages)), - sourceFunc, - gherkinDocumentFunc, - picklesFunc, - new CodePrimitiveExpression(sourceFileLocation)); - - CucumberMessagesInitializeMethod.Statements.Add( - new CodeAssignStatement( - new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("featureInfo"), "FeatureCucumberMessages"), - featureLevelCucumberMessagesExpression)); - - // Create a CodeMethodInvokeExpression to invoke the CucumberMessagesInitializeMethod - var invokeCucumberMessagesInitializeMethod = new CodeMethodInvokeExpression( - null, - CucumberMessagesInitializeMethod.Name, - new CodeVariableReferenceExpression("featureInfo")); - - // Add the CodeMethodInvokeExpression to the testClassInitializeMethod statements - testClassInitializeMethod.Statements.Add(invokeCucumberMessagesInitializeMethod); - - } - private void SetupTestClassCleanupMethod(TestClassGenerationContext generationContext) { var testClassCleanupMethod = generationContext.TestClassCleanupMethod; @@ -368,7 +349,7 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont var getTestRunnerExpression = new CodeMethodInvokeExpression( new CodeTypeReferenceExpression(_codeDomHelper.GetGlobalizedTypeName(typeof(TestRunnerManager))), nameof(TestRunnerManager.GetTestRunnerForAssembly), - _codeDomHelper.CreateOptionalArgumentExpression("featureHint", + _codeDomHelper.CreateOptionalArgumentExpression("featureHint", new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD))); testInitializeMethod.Statements.Add( diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index 152933d8d..11c7c87f8 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -25,11 +25,11 @@ public class FeatureInfo public string CucumberMessages_PickleId { get; set; } public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) - : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) + : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, null, tags) { } - public FeatureInfo(CultureInfo language, string folderPath, string title, string description, ProgrammingLanguage programmingLanguage, params string[] tags) + public FeatureInfo(CultureInfo language, string folderPath, string title, string description, ProgrammingLanguage programmingLanguage, FeatureLevelCucumberMessages featureLevelCucumberMessages = null, params string[] tags) { if (language.IsNeutralCulture) { @@ -43,6 +43,7 @@ public FeatureInfo(CultureInfo language, string folderPath, string title, string Title = title; Description = description; GenerationTargetLanguage = programmingLanguage; + FeatureCucumberMessages = featureLevelCucumberMessages; Tags = tags ?? Array.Empty(); } } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs index 0163f167e..965adaf4b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTests.cs @@ -76,6 +76,7 @@ public void NonCCKScenarios(string testName, string featureNameText) CCKScenarios(testName, featureNameText); } + [Ignore] [TestMethod] [DataRow("attachments", "Attachments")] [DataRow("skipped", "Skipping scenarios")] diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/StepContextTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/StepContextTests.cs index 519f8bd88..7fc0d3072 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/StepContextTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/StepContextTests.cs @@ -41,5 +41,5 @@ private ScenarioInfo CreateScenarioInfo(string[] directScenarioTags = null, stri => new("Sample scenario", null, directScenarioTags ?? Array.Empty(), new OrderedDictionary(), inheritedScenarioTags ?? Array.Empty()); private FeatureInfo CreateFeatureInfo(string[] featureTags = null) => - new(new CultureInfo("en-US"), @"C:\MyProject", "Sample feature", null, ProgrammingLanguage.CSharp, featureTags ?? Array.Empty()); + new(new CultureInfo("en-US"), @"C:\MyProject", "Sample feature", null, ProgrammingLanguage.CSharp, null, featureTags ?? Array.Empty()); } \ No newline at end of file From 41897a74d5582e0f89176398176e56d308a4ead4 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:24:47 -0600 Subject: [PATCH 210/218] Fixed an issue in which the FeatureLevelCucumberMessages constructor was failing now that is called well before the Messages configuration system is spun up. --- .../RuntimeSupport/FeatureLevelCucumberMessages.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs index 2fae1e14b..aaf52c4db 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -15,13 +15,10 @@ public class FeatureLevelCucumberMessages { public FeatureLevelCucumberMessages(Func source, Func gherkinDocument, Func> pickles, string location) { - if (CucumberConfiguration.Current.Enabled) - { - Source = source; - GherkinDocument = gherkinDocument; - Pickles = pickles; - Location = location; - } + Source = source; + GherkinDocument = gherkinDocument; + Pickles = pickles; + Location = location; } public string Location { get; } From 7bceb8064dfa594c179a2c6fac50ff079410dc31 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:13:20 -0600 Subject: [PATCH 211/218] Experimental commit - enhance BoDi to include the name of the Interface when throwing an 'already resolved' registration exception. --- Reqnroll/BoDi/ObjectContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reqnroll/BoDi/ObjectContainer.cs b/Reqnroll/BoDi/ObjectContainer.cs index 9f7df16c6..00ad7e13b 100644 --- a/Reqnroll/BoDi/ObjectContainer.cs +++ b/Reqnroll/BoDi/ObjectContainer.cs @@ -500,7 +500,7 @@ public bool IsRegistered(Type type, string name = null) private void AssertNotResolved(RegistrationKey interfaceType) { if (_resolvedKeys.Contains(interfaceType)) - throw new ObjectContainerException("An object has been resolved for this interface already.", null); + throw new ObjectContainerException($"An object has been resolved for this interface({interfaceType.ToString()}) already.", null); } private void ClearRegistrations(RegistrationKey registrationKey) From 2b8ef3d96a48784f619baf7742fca67662bfa9b8 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 9 Nov 2024 09:01:20 -0600 Subject: [PATCH 212/218] Fix threading bug that allowed a second Scenario to start before the Static Messages for a Feature were processed (which resulted in the PickleJar not being properly initialized). --- .../ExecutionTracking/FeatureTracker.cs | 17 ++++++- .../PubSub/CucumberMessagePublisher.cs | 51 ++++++++++++------- .../RuntimeSupport/PickleJar.cs | 29 +++-------- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index b0f8115eb..beea19c63 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -173,12 +173,25 @@ public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) { - // as in the Publisher, we're using defensive coding here b/c some test setups might not have complete info var pickleIndex = scenarioStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; - if (String.IsNullOrEmpty(pickleIndex)) return; + + // The following validations and ANE throws are in place to help identify threading bugs when Scenarios are run in parallel. + // TODO: consider removing these or placing them within #IFDEBUG + + if (String.IsNullOrEmpty(pickleIndex)) + { + // Should never happen + if (scenarioStartedEvent.ScenarioContext == null) + throw new ArgumentNullException("ScenarioContext", "ScenarioContext is not properly initialized for Cucumber Messages."); + if (scenarioStartedEvent.ScenarioContext.ScenarioInfo == null) + throw new ArgumentNullException("ScenarioInfo", "ScenarioContext/ScenarioInfo is not properly initialized for Cucumber Messages."); + throw new ArgumentNullException("PickleIdIndex", "ScenarioContext/ScenarioInfo does not have a properly initialized PickleIdIndex."); + } if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) { + if (PickleJar == null) + throw new ArgumentNullException("PickleJar", "PickleJar is not properly initialized for Cucumber Messages."); // Fetch the PickleStepSequence for this Pickle and give to the ScenarioInfo var pickleStepSequence = PickleJar.PickleStepSequenceFor(pickleIndex); scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleStepSequence = pickleStepSequence; ; diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 5b1be8327..4b9144b51 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -5,7 +5,6 @@ using Reqnroll.UnitTestProvider; using System.Collections.Concurrent; using System; -using System.Collections.Generic; using System.Linq; using Reqnroll.CucumberMessages.ExecutionTracking; using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; @@ -30,7 +29,9 @@ public class CucumberMessagePublisher : ICucumberMessagePublisher, IRuntimePlugi { private Lazy _brokerFactory; private ICucumberMessageBroker _broker; - private IObjectContainer objectContainer; + private IObjectContainer testThreadObjectContainer; + + public static object _lock = new object(); // Started Features by name private ConcurrentDictionary StartedFeatures = new(); @@ -59,8 +60,8 @@ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginPar }; runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => { - objectContainer = args.ObjectContainer; - _brokerFactory = new Lazy(() => objectContainer.Resolve()); + testThreadObjectContainer = args.ObjectContainer; + _brokerFactory = new Lazy(() => testThreadObjectContainer.Resolve()); var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); HookIntoTestThreadExecutionEventPublisher(testThreadExecutionEventPublisher); }; @@ -137,18 +138,21 @@ private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEv if (!Enabled) return; var status = StartedFeatures.Values.All(f => f.FeatureExecutionSuccess); - - Task.Run(async () => - await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) })).Wait(); + StartedFeatures.Clear(); + + Task.Run(async () => + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now)) }) + ).Wait(); } #region TestThreadExecutionEventPublisher Event Handling Methods + // The following methods handle the events published by the TestThreadExecutionEventPublisher // When one of these calls the Broker, that method is async; otherwise these are sync methods that return a completed Task (to allow them to be called async from the TestThreadExecutionEventPublisher) private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) { _broker = _brokerFactory.Value; - var traceListener = objectContainer.Resolve(); + var traceListener = testThreadObjectContainer.Resolve(); var featureName = featureStartedEvent.FeatureContext?.FeatureInfo?.Title; @@ -167,18 +171,29 @@ private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStarted return; } - // Creating multiple copies of the same FeatureTracker is safe as it causes no side-effects. - // If two or more threads are running this code simultaneously, all but one of them will get created but then will be ignored. - var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); - - // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. - if (StartedFeatures.TryAdd(featureName, ft) && ft.Enabled) + await Task.Run( () => { - foreach (var msg in ft.StaticMessages) + lock (_lock) { - await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); + + // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. + + // We publish the messages before adding the featureTracker to the StartedFeatures dictionary b/c other parallel scenario threads might be running. + // We don't want them to run until after the static messages have been published (and the PickleJar has been populated as a result). + if (!StartedFeatures.ContainsKey(featureName) && ft.Enabled) + { + Task.Run(async () => + { + foreach (var msg in ft.StaticMessages) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + }).Wait(); + } + StartedFeatures.TryAdd(featureName, ft); } - } + }); } private Task FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) @@ -206,7 +221,7 @@ private Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEve var featureName = scenarioStartedEvent.FeatureContext?.FeatureInfo?.Title; if (!Enabled || String.IsNullOrEmpty(featureName)) return Task.CompletedTask; - var traceListener = objectContainer.Resolve(); + var traceListener = testThreadObjectContainer.Resolve(); if (StartedFeatures.TryGetValue(featureName, out var featureTracker)) { if (featureTracker.Enabled) diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs index e286da9a0..4db28945a 100644 --- a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -14,37 +14,22 @@ public class PickleJar { public int _PickleCounter = 0; - public bool HasPickles { get; } + public bool HasPickles => Pickles != null && Pickles.Count() > 0; public IEnumerable Pickles { get; set; } - public PickleJar(string picklesJSON) : this(System.Text.Json.JsonSerializer.Deserialize>(picklesJSON)) { } - public PickleJar(IEnumerable pickles) : this(pickles, 0, 0) { } - - public PickleJar(IEnumerable pickles, int pickleCounter, int pickleStepCounter) + public PickleJar(IEnumerable pickles) { Pickles = pickles; - _PickleCounter = pickleCounter; - HasPickles = pickles != null && pickles.Count() > 0; - } - - public string CurrentPickleId - { - get - { - if (!HasPickles) return null; - return Pickles.ElementAt(_PickleCounter).Id; - } + _PickleCounter = 0; } - public Pickle CurrentPickle { get { return Pickles.ElementAt(_PickleCounter); } } public PickleStepSequence PickleStepSequenceFor(string pickleIndex) { - return new PickleStepSequence(HasPickles, HasPickles ? Pickles.ElementAt(int.Parse(pickleIndex)): null); - } + var pickleIndexInt = int.Parse(pickleIndex); + if (HasPickles && (pickleIndexInt < 0 || pickleIndexInt >= Pickles.Count())) + throw new ArgumentException("Invalid pickle index: " + pickleIndex); - public void NextPickle() - { - _PickleCounter++; + return new PickleStepSequence(HasPickles, HasPickles ? Pickles.ElementAt(pickleIndexInt): null); } } } From 23ad53756a55e759aeefebf8c19e903b43d47b63 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Sat, 9 Nov 2024 14:13:33 -0600 Subject: [PATCH 213/218] Minor refactor to Publisher Moving the FeatureTracker creation to within the double-check of the lock section (so that it is called only one time). --- .../PubSub/CucumberMessagePublisher.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 4b9144b51..09b0eb430 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -171,27 +171,27 @@ private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStarted return; } - await Task.Run( () => + await Task.Run(() => { lock (_lock) { - var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); - // This will add a FeatureTracker to the StartedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. // We publish the messages before adding the featureTracker to the StartedFeatures dictionary b/c other parallel scenario threads might be running. // We don't want them to run until after the static messages have been published (and the PickleJar has been populated as a result). - if (!StartedFeatures.ContainsKey(featureName) && ft.Enabled) + if (!StartedFeatures.ContainsKey(featureName)) { - Task.Run(async () => - { - foreach (var msg in ft.StaticMessages) + var ft = new FeatureTracker(featureStartedEvent, SharedIDGenerator, StepDefinitionsByPattern, StepArgumentTransforms, UndefinedParameterTypeBindings); + if (ft.Enabled) + Task.Run(async () => { - await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); - } - }).Wait(); + foreach (var msg in ft.StaticMessages) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + }).Wait(); + StartedFeatures.TryAdd(featureName, ft); } - StartedFeatures.TryAdd(featureName, ft); } }); } From 5d06ba2b4312d3327ab0e79f222903b66f96768c Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:16:28 -0600 Subject: [PATCH 214/218] Clarified comments on FeatureTracker regarding interdependencies for thread safety. --- .../CucumberMessages/ExecutionTracking/FeatureTracker.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs index beea19c63..c233c0f70 100644 --- a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -68,11 +68,11 @@ internal void ProcessEvent(FeatureStartedEvent featureStartedEvent) // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow _staticMessages = new Lazy>(() => GenerateStaticMessages(featureStartedEvent)); } + + // This method is used to generate the static messages (Source, GherkinDocument & Pickles) and the StepTransformations, StepDefinitions and Hook messages which are global to the entire Solution + // This should be called only once per Feature. As such, it relies on the use of a lock section within the Publisher to ensure that only a single instance of the FeatureTracker is created per Feature private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) { - - yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source()); - var gd = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument(); var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles().ToList(); @@ -89,6 +89,8 @@ private IEnumerable GenerateStaticMessages(FeatureStartedEvent feature PickleJar = new PickleJar(pickles); + yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source()); + yield return Envelope.Create(gd); foreach (var pickle in pickles) { From 95860cfa05e0a54d066dfa53166dd5eeb466daaa Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:41:56 -0600 Subject: [PATCH 215/218] Updated ndjson expected results from CCK From CCK --- .../attachments/attachments.feature.ndjson | 138 +++++++------- .../Samples/cdata/cdata.feature.ndjson | 16 +- .../data-tables/data_tables.feature.ndjson | 20 +-- .../Samples/empty/empty.feature.ndjson | 12 +- .../examples_tables.feature.ndjson | 168 +++++++++--------- .../Samples/hooks/hooks.feature.ndjson | 136 +++++++------- .../Samples/minimal/minimal.feature.ndjson | 16 +- .../parameter_types.feature.ndjson | 16 +- .../Samples/pending/pending.feature.ndjson | 44 ++--- .../Samples/rules/rules.feature.ndjson | 72 ++++---- .../Samples/skipped/skipped.feature.ndjson | 50 +++--- .../stack-traces/stack_traces.feature.ndjson | 16 +- .../undefined/undefined.feature.ndjson | 44 ++--- .../unknown_parameter_type.feature.ndjson | 18 +- 14 files changed, 383 insertions(+), 383 deletions(-) diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson index 1e42d17af..bb4b39511 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/attachments/attachments.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching JPEG images\n When a JPEG image is attached\n\n Scenario: Attaching PNG images\n When a PNG image is attached\n\n Scenario Outline: Attaching images in an examples table\n When a image is attached\n\n Examples:\n | type |\n | JPEG |\n | PNG |\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/attachments/attachments.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":12},"name":"Strings can be attached with a media type","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":16},"text":"the string \"hello\" is attached as \"application/octet-stream\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":18},"name":"Log text","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":19},"text":"the string \"hello\" is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"14","keyword":"Scenario","location":{"column":3,"line":21},"name":"Log ANSI coloured text","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":22},"text":"text with ANSI escapes is logged"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"16","keyword":"Scenario","location":{"column":3,"line":24},"name":"Log JSON","steps":[{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```","location":{"column":8,"line":26}},"id":"15","keyword":"When ","keywordType":"Action","location":{"column":6,"line":25},"text":"the following string is attached as \"application/json\":"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":30},"name":"Byte arrays are base64-encoded regardless of media type","steps":[{"id":"17","keyword":"When ","keywordType":"Action","location":{"column":5,"line":31},"text":"an array with 10 bytes is attached as \"text/plain\""}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"20","keyword":"Scenario","location":{"column":3,"line":33},"name":"Attaching JPEG images","steps":[{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":5,"line":34},"text":"a JPEG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"22","keyword":"Scenario","location":{"column":3,"line":36},"name":"Attaching PNG images","steps":[{"id":"21","keyword":"When ","keywordType":"Action","location":{"column":5,"line":37},"text":"a PNG image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"27","keyword":"Examples","location":{"column":5,"line":42},"name":"","tableBody":[{"cells":[{"location":{"column":9,"line":44},"value":"JPEG"}],"id":"25","location":{"column":7,"line":44}},{"cells":[{"location":{"column":9,"line":45},"value":"PNG"}],"id":"26","location":{"column":7,"line":45}}],"tableHeader":{"cells":[{"location":{"column":9,"line":43},"value":"type"}],"id":"24","location":{"column":7,"line":43}},"tags":[]}],"id":"28","keyword":"Scenario Outline","location":{"column":3,"line":39},"name":"Attaching images in an examples table","steps":[{"id":"23","keyword":"When ","keywordType":"Action","location":{"column":5,"line":40},"text":"a image is attached"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"30","keyword":"Scenario","location":{"column":3,"line":47},"name":"Attaching PDFs with a different filename","steps":[{"id":"29","keyword":"When ","keywordType":"Action","location":{"column":5,"line":48},"text":"a PDF document is attached and renamed"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"32","keyword":"Scenario","location":{"column":3,"line":50},"name":"Attaching URIs","steps":[{"id":"31","keyword":"When ","keywordType":"Action","location":{"column":5,"line":51},"text":"a link to \"https://cucumber.io\" is attached"}],"tags":[]}}],"description":" It is sometimes useful to take a screenshot while a scenario runs.\n Or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Attachments","tags":[]},"uri":"samples/attachments/attachments.feature"}} {"pickle":{"astNodeIds":["10"],"id":"34","language":"en","name":"Strings can be attached with a media type","steps":[{"astNodeIds":["9"],"id":"33","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action"}],"tags":[],"uri":"samples/attachments/attachments.feature"}} @@ -21,71 +21,71 @@ {"stepDefinition":{"id":"6","pattern":{"source":"a PNG image is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":35},"uri":"samples/attachments/attachments.feature.ts"}}} {"stepDefinition":{"id":"7","pattern":{"source":"a PDF document is attached and renamed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":39},"uri":"samples/attachments/attachments.feature.ts"}}} {"stepDefinition":{"id":"8","pattern":{"source":"a link to {string} is attached","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":43},"uri":"samples/attachments/attachments.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"56","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"},{"group":{"children":[{"children":[{"children":[]}],"start":35,"value":"application/octet-stream"},{"children":[{"children":[]}]}],"start":34,"value":"\"application/octet-stream\""},"parameterTypeName":"string"}]}]}]}} -{"testCase":{"id":"58","pickleId":"36","testSteps":[{"id":"57","pickleStepId":"35","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"}]}]}]}} -{"testCase":{"id":"60","pickleId":"38","testSteps":[{"id":"59","pickleStepId":"37","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"62","pickleId":"40","testSteps":[{"id":"61","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":37,"value":"application/json"},{"children":[{"children":[]}]}],"start":36,"value":"\"application/json\""},"parameterTypeName":"string"}]}]}]}} -{"testCase":{"id":"64","pickleId":"42","testSteps":[{"id":"63","pickleStepId":"41","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"10"},"parameterTypeName":"int"},{"group":{"children":[{"children":[{"children":[]}],"start":39,"value":"text/plain"},{"children":[{"children":[]}]}],"start":38,"value":"\"text/plain\""},"parameterTypeName":"string"}]}]}]}} -{"testCase":{"id":"66","pickleId":"44","testSteps":[{"id":"65","pickleStepId":"43","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"68","pickleId":"46","testSteps":[{"id":"67","pickleStepId":"45","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"70","pickleId":"48","testSteps":[{"id":"69","pickleStepId":"47","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"72","pickleId":"50","testSteps":[{"id":"71","pickleStepId":"49","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"74","pickleId":"52","testSteps":[{"id":"73","pickleStepId":"51","stepDefinitionIds":["7"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"76","pickleId":"54","testSteps":[{"id":"75","pickleStepId":"53","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":11,"value":"https://cucumber.io"},{"children":[{"children":[]}]}],"start":10,"value":"\"https://cucumber.io\""},"parameterTypeName":"string"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"77","testCaseId":"56","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"77","testStepId":"55","timestamp":{"nanos":2000000,"seconds":0}}} -{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","testCaseStartedId":"77","testStepId":"55"}} -{"testStepFinished":{"testCaseStartedId":"77","testStepId":"55","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"77","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"78","testCaseId":"58","timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"78","testStepId":"57","timestamp":{"nanos":6000000,"seconds":0}}} -{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"78","testStepId":"57"}} -{"testStepFinished":{"testCaseStartedId":"78","testStepId":"57","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"78","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"79","testCaseId":"60","timestamp":{"nanos":9000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"79","testStepId":"59","timestamp":{"nanos":10000000,"seconds":0}}} -{"attachment":{"body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"79","testStepId":"59"}} -{"testStepFinished":{"testCaseStartedId":"79","testStepId":"59","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"79","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"80","testCaseId":"62","timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"80","testStepId":"61","timestamp":{"nanos":14000000,"seconds":0}}} -{"attachment":{"body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","testCaseStartedId":"80","testStepId":"61"}} -{"testStepFinished":{"testCaseStartedId":"80","testStepId":"61","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"80","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"81","testCaseId":"64","timestamp":{"nanos":17000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"81","testStepId":"63","timestamp":{"nanos":18000000,"seconds":0}}} -{"attachment":{"body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","testCaseStartedId":"81","testStepId":"63"}} -{"testStepFinished":{"testCaseStartedId":"81","testStepId":"63","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"81","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"82","testCaseId":"66","timestamp":{"nanos":21000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"82","testStepId":"65","timestamp":{"nanos":22000000,"seconds":0}}} -{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"82","testStepId":"65"}} -{"testStepFinished":{"testCaseStartedId":"82","testStepId":"65","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"82","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"83","testCaseId":"68","timestamp":{"nanos":25000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"83","testStepId":"67","timestamp":{"nanos":26000000,"seconds":0}}} -{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"83","testStepId":"67"}} -{"testStepFinished":{"testCaseStartedId":"83","testStepId":"67","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"83","timestamp":{"nanos":28000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"84","testCaseId":"70","timestamp":{"nanos":29000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"84","testStepId":"69","timestamp":{"nanos":30000000,"seconds":0}}} -{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"84","testStepId":"69"}} -{"testStepFinished":{"testCaseStartedId":"84","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":31000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"84","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"85","testCaseId":"72","timestamp":{"nanos":33000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"85","testStepId":"71","timestamp":{"nanos":34000000,"seconds":0}}} -{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"85","testStepId":"71"}} -{"testStepFinished":{"testCaseStartedId":"85","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"nanos":36000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"86","testCaseId":"74","timestamp":{"nanos":37000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"86","testStepId":"73","timestamp":{"nanos":38000000,"seconds":0}}} -{"attachment":{"body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","fileName":"renamed.pdf","mediaType":"application/pdf","testCaseStartedId":"86","testStepId":"73"}} -{"testStepFinished":{"testCaseStartedId":"86","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":39000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"87","testCaseId":"76","timestamp":{"nanos":41000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"87","testStepId":"75","timestamp":{"nanos":42000000,"seconds":0}}} -{"attachment":{"body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","testCaseStartedId":"87","testStepId":"75"}} -{"testStepFinished":{"testCaseStartedId":"87","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"nanos":44000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":45000000,"seconds":0}}} +{"testRunStarted":{"id":"55","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"57","pickleId":"34","testRunStartedId":"55","testSteps":[{"id":"56","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"},{"group":{"children":[{"children":[{"children":[]}],"start":35,"value":"application/octet-stream"},{"children":[{"children":[]}]}],"start":34,"value":"\"application/octet-stream\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"59","pickleId":"36","testRunStartedId":"55","testSteps":[{"id":"58","pickleStepId":"35","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":12,"value":"hello"},{"children":[{"children":[]}]}],"start":11,"value":"\"hello\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"61","pickleId":"38","testRunStartedId":"55","testSteps":[{"id":"60","pickleStepId":"37","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"63","pickleId":"40","testRunStartedId":"55","testSteps":[{"id":"62","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":37,"value":"application/json"},{"children":[{"children":[]}]}],"start":36,"value":"\"application/json\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"65","pickleId":"42","testRunStartedId":"55","testSteps":[{"id":"64","pickleStepId":"41","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"10"},"parameterTypeName":"int"},{"group":{"children":[{"children":[{"children":[]}],"start":39,"value":"text/plain"},{"children":[{"children":[]}]}],"start":38,"value":"\"text/plain\""},"parameterTypeName":"string"}]}]}]}} +{"testCase":{"id":"67","pickleId":"44","testRunStartedId":"55","testSteps":[{"id":"66","pickleStepId":"43","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"69","pickleId":"46","testRunStartedId":"55","testSteps":[{"id":"68","pickleStepId":"45","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"71","pickleId":"48","testRunStartedId":"55","testSteps":[{"id":"70","pickleStepId":"47","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"73","pickleId":"50","testRunStartedId":"55","testSteps":[{"id":"72","pickleStepId":"49","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"75","pickleId":"52","testRunStartedId":"55","testSteps":[{"id":"74","pickleStepId":"51","stepDefinitionIds":["7"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"77","pickleId":"54","testRunStartedId":"55","testSteps":[{"id":"76","pickleStepId":"53","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[{"children":[]}],"start":11,"value":"https://cucumber.io"},{"children":[{"children":[]}]}],"start":10,"value":"\"https://cucumber.io\""},"parameterTypeName":"string"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"78","testCaseId":"57","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"78","testStepId":"56","timestamp":{"nanos":2000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","testCaseStartedId":"78","testStepId":"56"}} +{"testStepFinished":{"testCaseStartedId":"78","testStepId":"56","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"78","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"79","testCaseId":"59","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"79","testStepId":"58","timestamp":{"nanos":6000000,"seconds":0}}} +{"attachment":{"body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"79","testStepId":"58"}} +{"testStepFinished":{"testCaseStartedId":"79","testStepId":"58","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"79","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"80","testCaseId":"61","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"80","testStepId":"60","timestamp":{"nanos":10000000,"seconds":0}}} +{"attachment":{"body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"80","testStepId":"60"}} +{"testStepFinished":{"testCaseStartedId":"80","testStepId":"60","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"80","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"81","testCaseId":"63","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"81","testStepId":"62","timestamp":{"nanos":14000000,"seconds":0}}} +{"attachment":{"body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","testCaseStartedId":"81","testStepId":"62"}} +{"testStepFinished":{"testCaseStartedId":"81","testStepId":"62","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"81","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"82","testCaseId":"65","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"82","testStepId":"64","timestamp":{"nanos":18000000,"seconds":0}}} +{"attachment":{"body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","testCaseStartedId":"82","testStepId":"64"}} +{"testStepFinished":{"testCaseStartedId":"82","testStepId":"64","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"82","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"83","testCaseId":"67","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"83","testStepId":"66","timestamp":{"nanos":22000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"83","testStepId":"66"}} +{"testStepFinished":{"testCaseStartedId":"83","testStepId":"66","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"83","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"84","testCaseId":"69","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"84","testStepId":"68","timestamp":{"nanos":26000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"84","testStepId":"68"}} +{"testStepFinished":{"testCaseStartedId":"84","testStepId":"68","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"84","timestamp":{"nanos":28000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"85","testCaseId":"71","timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"70","timestamp":{"nanos":30000000,"seconds":0}}} +{"attachment":{"body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","testCaseStartedId":"85","testStepId":"70"}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"70","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"86","testCaseId":"73","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"72","timestamp":{"nanos":34000000,"seconds":0}}} +{"attachment":{"body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","testCaseStartedId":"86","testStepId":"72"}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"72","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"nanos":36000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"87","testCaseId":"75","timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"74","timestamp":{"nanos":38000000,"seconds":0}}} +{"attachment":{"body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","fileName":"renamed.pdf","mediaType":"application/pdf","testCaseStartedId":"87","testStepId":"74"}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"74","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"88","testCaseId":"77","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"76","timestamp":{"nanos":42000000,"seconds":0}}} +{"attachment":{"body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","testCaseStartedId":"88","testStepId":"76"}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"76","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"88","timestamp":{"nanos":44000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"55","timestamp":{"nanos":45000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson index 51aa3abcb..f0cb0b6d8 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/cdata/cdata.feature.ndjson @@ -1,12 +1,12 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: cdata\n Cucumber xml formatters should be able to handle xml cdata elements\n\n Scenario: cdata\n Given I have 42 in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/cdata/cdata.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":4},"name":"cdata","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":5},"text":"I have 42 in my belly"}],"tags":[]}}],"description":" Cucumber xml formatters should be able to handle xml cdata elements","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"cdata","tags":[]},"uri":"samples/cdata/cdata.feature"}} {"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cdata","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 in my belly","type":"Context"}],"tags":[],"uri":"samples/cdata/cdata.feature"}} {"stepDefinition":{"id":"0","pattern":{"source":"I have {int} in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/cdata/cdata.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} +{"testRunStarted":{"id":"5","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"7","pickleId":"4","testRunStartedId":"5","testSteps":[{"id":"6","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"5","timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson index a8a9c2bd7..d530c1c70 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/data-tables/data_tables.feature.ndjson @@ -1,15 +1,15 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Data Tables\n Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.\n\n Scenario: transposed table\n When the following table is transposed:\n | a | b |\n | 1 | 2 |\n Then it should be:\n | a | 1 |\n | b | 2 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/data-tables/data-tables.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":7},"name":"transposed table","steps":[{"dataTable":{"location":{"column":7,"line":9},"rows":[{"cells":[{"location":{"column":9,"line":9},"value":"a"},{"location":{"column":13,"line":9},"value":"b"}],"id":"2","location":{"column":7,"line":9}},{"cells":[{"location":{"column":9,"line":10},"value":"1"},{"location":{"column":13,"line":10},"value":"2"}],"id":"3","location":{"column":7,"line":10}}]},"id":"4","keyword":"When ","keywordType":"Action","location":{"column":5,"line":8},"text":"the following table is transposed:"},{"dataTable":{"location":{"column":7,"line":12},"rows":[{"cells":[{"location":{"column":9,"line":12},"value":"a"},{"location":{"column":13,"line":12},"value":"1"}],"id":"5","location":{"column":7,"line":12}},{"cells":[{"location":{"column":9,"line":13},"value":"b"},{"location":{"column":13,"line":13},"value":"2"}],"id":"6","location":{"column":7,"line":13}}]},"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":11},"text":"it should be:"}],"tags":[]}}],"description":" Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Data Tables","tags":[]},"uri":"samples/data-tables/data-tables.feature"}} {"pickle":{"astNodeIds":["8"],"id":"11","language":"en","name":"transposed table","steps":[{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"b"}]},{"cells":[{"value":"1"},{"value":"2"}]}]}},"astNodeIds":["4"],"id":"9","text":"the following table is transposed:","type":"Action"},{"argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"1"}]},{"cells":[{"value":"b"},{"value":"2"}]}]}},"astNodeIds":["7"],"id":"10","text":"it should be:","type":"Outcome"}],"tags":[],"uri":"samples/data-tables/data-tables.feature"}} {"stepDefinition":{"id":"0","pattern":{"source":"the following table is transposed:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":5},"uri":"samples/data-tables/data-tables.feature.ts"}}} {"stepDefinition":{"id":"1","pattern":{"source":"it should be:","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":9},"uri":"samples/data-tables/data-tables.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"14","pickleId":"11","testSteps":[{"id":"12","pickleStepId":"9","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"13","pickleStepId":"10","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"15","testCaseId":"14","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"15","testStepId":"12","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"15","testStepId":"12","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"15","testStepId":"13","timestamp":{"nanos":4000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"15","testStepId":"13","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"15","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":7000000,"seconds":0}}} +{"testRunStarted":{"id":"12","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"15","pickleId":"11","testRunStartedId":"12","testSteps":[{"id":"13","pickleStepId":"9","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"14","pickleStepId":"10","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"16","testCaseId":"15","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"13","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"13","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"14","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"14","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"16","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"12","timestamp":{"nanos":7000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson index 4d171d6dd..2e429bb5b 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/empty/empty.feature.ndjson @@ -1,9 +1,9 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"17.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"22.7.0"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is\n\n Scenario: Blank Scenario\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/empty/empty.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"0","keyword":"Scenario","location":{"column":3,"line":7},"name":"Blank Scenario","steps":[],"tags":[]}}],"description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Empty Scenarios","tags":[]},"uri":"samples/empty/empty.feature"}} {"pickle":{"astNodeIds":["0"],"id":"1","language":"en","name":"Blank Scenario","steps":[],"tags":[],"uri":"samples/empty/empty.feature"}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"2","pickleId":"1","testSteps":[]}} -{"testCaseStarted":{"attempt":0,"id":"3","testCaseId":"2","timestamp":{"nanos":1000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"3","timestamp":{"nanos":2000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":3000000,"seconds":0}}} +{"testRunStarted":{"id":"2","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"3","pickleId":"1","testRunStartedId":"2","testSteps":[]}} +{"testCaseStarted":{"attempt":0,"id":"4","testCaseId":"3","timestamp":{"nanos":1000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"4","timestamp":{"nanos":2000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"2","timestamp":{"nanos":3000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson index b169fd03d..c60e75c29 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/examples-tables/examples_tables.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n @undefined\n Examples: These are undefined because the value is not an {int}\n | start | eat | left |\n | 12 | banana | 12 |\n | 0 | 1 | apple |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/examples-tables/examples-tables.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[{"description":"","id":"12","keyword":"Examples","location":{"column":5,"line":17},"name":"These are passing","tableBody":[{"cells":[{"location":{"column":12,"line":19},"value":"12"},{"location":{"column":19,"line":19},"value":"5"},{"location":{"column":26,"line":19},"value":"7"}],"id":"9","location":{"column":7,"line":19}},{"cells":[{"location":{"column":12,"line":20},"value":"20"},{"location":{"column":19,"line":20},"value":"5"},{"location":{"column":25,"line":20},"value":"15"}],"id":"10","location":{"column":7,"line":20}}],"tableHeader":{"cells":[{"location":{"column":9,"line":18},"value":"start"},{"location":{"column":17,"line":18},"value":"eat"},{"location":{"column":23,"line":18},"value":"left"}],"id":"8","location":{"column":7,"line":18}},"tags":[{"id":"11","location":{"column":5,"line":16},"name":"@passing"}]},{"description":"","id":"17","keyword":"Examples","location":{"column":5,"line":23},"name":"These are failing","tableBody":[{"cells":[{"location":{"column":12,"line":25},"value":"12"},{"location":{"column":18,"line":25},"value":"20"},{"location":{"column":26,"line":25},"value":"0"}],"id":"14","location":{"column":7,"line":25}},{"cells":[{"location":{"column":13,"line":26},"value":"0"},{"location":{"column":19,"line":26},"value":"1"},{"location":{"column":26,"line":26},"value":"0"}],"id":"15","location":{"column":7,"line":26}}],"tableHeader":{"cells":[{"location":{"column":9,"line":24},"value":"start"},{"location":{"column":17,"line":24},"value":"eat"},{"location":{"column":23,"line":24},"value":"left"}],"id":"13","location":{"column":7,"line":24}},"tags":[{"id":"16","location":{"column":5,"line":22},"name":"@failing"}]},{"description":"","id":"22","keyword":"Examples","location":{"column":5,"line":29},"name":"These are undefined because the value is not an {int}","tableBody":[{"cells":[{"location":{"column":12,"line":31},"value":"12"},{"location":{"column":17,"line":31},"value":"banana"},{"location":{"column":29,"line":31},"value":"12"}],"id":"19","location":{"column":7,"line":31}},{"cells":[{"location":{"column":13,"line":32},"value":"0"},{"location":{"column":22,"line":32},"value":"1"},{"location":{"column":26,"line":32},"value":"apple"}],"id":"20","location":{"column":7,"line":32}}],"tableHeader":{"cells":[{"location":{"column":9,"line":30},"value":"start"},{"location":{"column":17,"line":30},"value":"eat"},{"location":{"column":26,"line":30},"value":"left"}],"id":"18","location":{"column":7,"line":30}},"tags":[{"id":"21","location":{"column":5,"line":28},"name":"@undefined"}]}],"id":"23","keyword":"Scenario Outline","location":{"column":3,"line":11},"name":"Eating cucumbers","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":12},"text":"there are cucumbers"},{"id":"6","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"I eat cucumbers"},{"id":"7","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":14},"text":"I should have cucumbers"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"31","keyword":"Examples","location":{"column":5,"line":39},"name":"","tableBody":[{"cells":[{"location":{"column":14,"line":41},"value":"11"},{"location":{"column":22,"line":41},"value":"12"},{"location":{"column":31,"line":41},"value":"1"}],"id":"28","location":{"column":7,"line":41}},{"cells":[{"location":{"column":15,"line":42},"value":"1"},{"location":{"column":23,"line":42},"value":"4"},{"location":{"column":31,"line":42},"value":"2"}],"id":"29","location":{"column":7,"line":42}},{"cells":[{"location":{"column":15,"line":43},"value":"0"},{"location":{"column":23,"line":43},"value":"4"},{"location":{"column":31,"line":43},"value":"4"}],"id":"30","location":{"column":7,"line":43}}],"tableHeader":{"cells":[{"location":{"column":9,"line":40},"value":"friends"},{"location":{"column":19,"line":40},"value":"start"},{"location":{"column":27,"line":40},"value":"share"}],"id":"27","location":{"column":7,"line":40}},"tags":[]}],"id":"32","keyword":"Scenario Outline","location":{"column":3,"line":34},"name":"Eating cucumbers with friends","steps":[{"id":"24","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":35},"text":"there are friends"},{"id":"25","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":36},"text":"there are cucumbers"},{"id":"26","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":37},"text":"each person can eat cucumbers"}],"tags":[]}}],"description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Examples Tables","tags":[]},"uri":"samples/examples-tables/examples-tables.feature"}} {"pickle":{"astNodeIds":["23","9"],"id":"36","language":"en","name":"Eating cucumbers","steps":[{"astNodeIds":["5","9"],"id":"33","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["6","9"],"id":"34","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["7","9"],"id":"35","text":"I should have 7 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@passing"}],"uri":"samples/examples-tables/examples-tables.feature"}} @@ -15,86 +15,86 @@ {"stepDefinition":{"id":"2","pattern":{"source":"I eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} {"stepDefinition":{"id":"3","pattern":{"source":"I should have {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} {"stepDefinition":{"id":"4","pattern":{"source":"each person can eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":20},"uri":"samples/examples-tables/examples-tables.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"72","pickleId":"36","testSteps":[{"id":"69","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"70","pickleStepId":"34","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"35","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"76","pickleId":"40","testSteps":[{"id":"73","pickleStepId":"37","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"74","pickleStepId":"38","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"80","pickleId":"44","testSteps":[{"id":"77","pickleStepId":"41","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"78","pickleStepId":"42","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"43","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"84","pickleId":"48","testSteps":[{"id":"81","pickleStepId":"45","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"82","pickleStepId":"46","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"47","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"88","pickleId":"52","testSteps":[{"id":"85","pickleStepId":"49","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"86","pickleStepId":"50","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"87","pickleStepId":"51","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"12"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"92","pickleId":"56","testSteps":[{"id":"89","pickleStepId":"53","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"90","pickleStepId":"54","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"91","pickleStepId":"55","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} -{"testCase":{"id":"96","pickleId":"60","testSteps":[{"id":"93","pickleStepId":"57","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"11"},"parameterTypeName":"int"}]}]},{"id":"94","pickleStepId":"58","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"95","pickleStepId":"59","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"1"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"100","pickleId":"64","testSteps":[{"id":"97","pickleStepId":"61","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"98","pickleStepId":"62","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"99","pickleStepId":"63","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"2"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"104","pickleId":"68","testSteps":[{"id":"101","pickleStepId":"65","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"102","pickleStepId":"66","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"103","pickleStepId":"67","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"4"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"105","testCaseId":"72","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"105","testStepId":"69","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"105","testStepId":"69","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"105","testStepId":"70","timestamp":{"nanos":4000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"105","testStepId":"70","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"105","testStepId":"71","timestamp":{"nanos":6000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"105","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"105","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"106","testCaseId":"76","timestamp":{"nanos":9000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"106","testStepId":"73","timestamp":{"nanos":10000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"106","testStepId":"73","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"106","testStepId":"74","timestamp":{"nanos":12000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"106","testStepId":"74","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"106","testStepId":"75","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"106","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"106","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"107","testCaseId":"80","timestamp":{"nanos":17000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"107","testStepId":"77","timestamp":{"nanos":18000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"107","testStepId":"77","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"107","testStepId":"78","timestamp":{"nanos":20000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"107","testStepId":"78","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":21000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"107","testStepId":"79","timestamp":{"nanos":22000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"107","testStepId":"79","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:25","status":"FAILED"},"timestamp":{"nanos":23000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"107","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"108","testCaseId":"84","timestamp":{"nanos":25000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"108","testStepId":"81","timestamp":{"nanos":26000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"108","testStepId":"81","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"108","testStepId":"82","timestamp":{"nanos":28000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"108","testStepId":"82","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"108","testStepId":"83","timestamp":{"nanos":30000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"108","testStepId":"83","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:26","status":"FAILED"},"timestamp":{"nanos":31000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"108","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"109","testCaseId":"88","timestamp":{"nanos":33000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"109","testStepId":"85","timestamp":{"nanos":34000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"109","testStepId":"85","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"109","testStepId":"86","timestamp":{"nanos":36000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"109","testStepId":"86","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":37000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"109","testStepId":"87","timestamp":{"nanos":38000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"109","testStepId":"87","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":39000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"109","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"110","testCaseId":"92","timestamp":{"nanos":41000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"110","testStepId":"89","timestamp":{"nanos":42000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"110","testStepId":"89","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"110","testStepId":"90","timestamp":{"nanos":44000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"110","testStepId":"90","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"110","testStepId":"91","timestamp":{"nanos":46000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"110","testStepId":"91","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":47000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"110","timestamp":{"nanos":48000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"111","testCaseId":"96","timestamp":{"nanos":49000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"111","testStepId":"93","timestamp":{"nanos":50000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"111","testStepId":"93","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"111","testStepId":"94","timestamp":{"nanos":52000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"111","testStepId":"94","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"111","testStepId":"95","timestamp":{"nanos":54000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"111","testStepId":"95","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":55000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"111","timestamp":{"nanos":56000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"112","testCaseId":"100","timestamp":{"nanos":57000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"112","testStepId":"97","timestamp":{"nanos":58000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"112","testStepId":"97","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":59000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"112","testStepId":"98","timestamp":{"nanos":60000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"112","testStepId":"98","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":61000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"112","testStepId":"99","timestamp":{"nanos":62000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"112","testStepId":"99","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":63000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"112","timestamp":{"nanos":64000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"113","testCaseId":"104","timestamp":{"nanos":65000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"113","testStepId":"101","timestamp":{"nanos":66000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"113","testStepId":"101","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":67000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"113","testStepId":"102","timestamp":{"nanos":68000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"113","testStepId":"102","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":69000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"113","testStepId":"103","timestamp":{"nanos":70000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"113","testStepId":"103","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":71000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"113","timestamp":{"nanos":72000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":73000000,"seconds":0}}} +{"testRunStarted":{"id":"69","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"73","pickleId":"36","testRunStartedId":"69","testSteps":[{"id":"70","pickleStepId":"33","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"34","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"72","pickleStepId":"35","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"77","pickleId":"40","testRunStartedId":"69","testSteps":[{"id":"74","pickleStepId":"37","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"38","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"76","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"81","pickleId":"44","testRunStartedId":"69","testSteps":[{"id":"78","pickleStepId":"41","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"42","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"80","pickleStepId":"43","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"85","pickleId":"48","testRunStartedId":"69","testSteps":[{"id":"82","pickleStepId":"45","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"46","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"84","pickleStepId":"47","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"0"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"89","pickleId":"52","testRunStartedId":"69","testSteps":[{"id":"86","pickleStepId":"49","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"87","pickleStepId":"50","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"88","pickleStepId":"51","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"12"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"93","pickleId":"56","testRunStartedId":"69","testSteps":[{"id":"90","pickleStepId":"53","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"91","pickleStepId":"54","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"92","pickleStepId":"55","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"97","pickleId":"60","testRunStartedId":"69","testSteps":[{"id":"94","pickleStepId":"57","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"11"},"parameterTypeName":"int"}]}]},{"id":"95","pickleStepId":"58","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"96","pickleStepId":"59","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"1"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"101","pickleId":"64","testRunStartedId":"69","testSteps":[{"id":"98","pickleStepId":"61","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"99","pickleStepId":"62","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"100","pickleStepId":"63","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"2"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"105","pickleId":"68","testRunStartedId":"69","testSteps":[{"id":"102","pickleStepId":"65","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"0"},"parameterTypeName":"int"}]}]},{"id":"103","pickleStepId":"66","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"4"},"parameterTypeName":"int"}]}]},{"id":"104","pickleStepId":"67","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":20,"value":"4"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"106","testCaseId":"73","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"70","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"70","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"71","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"71","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"106","testStepId":"72","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"106","testStepId":"72","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"106","timestamp":{"nanos":8000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"107","testCaseId":"77","timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"74","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"74","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"75","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"75","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"107","testStepId":"76","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"107","testStepId":"76","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"107","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"108","testCaseId":"81","timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"78","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"78","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"79","timestamp":{"nanos":20000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"79","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"108","testStepId":"80","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"108","testStepId":"80","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:25","status":"FAILED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"108","timestamp":{"nanos":24000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"109","testCaseId":"85","timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"82","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"82","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"83","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"83","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"109","testStepId":"84","timestamp":{"nanos":30000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"109","testStepId":"84","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","type":"AssertionError"},"message":"Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14\nsamples/examples-tables/examples-tables.feature:26","status":"FAILED"},"timestamp":{"nanos":31000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"109","timestamp":{"nanos":32000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"110","testCaseId":"89","timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"86","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"86","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"87","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"87","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"110","testStepId":"88","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"110","testStepId":"88","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"110","timestamp":{"nanos":40000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"111","testCaseId":"93","timestamp":{"nanos":41000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"90","timestamp":{"nanos":42000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"90","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"91","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"91","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"111","testStepId":"92","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"111","testStepId":"92","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"111","timestamp":{"nanos":48000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"112","testCaseId":"97","timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"94","timestamp":{"nanos":50000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"94","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"95","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"95","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"112","testStepId":"96","timestamp":{"nanos":54000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"112","testStepId":"96","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":55000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"112","timestamp":{"nanos":56000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"113","testCaseId":"101","timestamp":{"nanos":57000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"98","timestamp":{"nanos":58000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"98","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":59000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"99","timestamp":{"nanos":60000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"99","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":61000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"113","testStepId":"100","timestamp":{"nanos":62000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"113","testStepId":"100","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":63000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"113","timestamp":{"nanos":64000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"114","testCaseId":"105","timestamp":{"nanos":65000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"114","testStepId":"102","timestamp":{"nanos":66000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"114","testStepId":"102","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":67000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"114","testStepId":"103","timestamp":{"nanos":68000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"114","testStepId":"103","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":69000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"114","testStepId":"104","timestamp":{"nanos":70000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"114","testStepId":"104","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":71000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"114","timestamp":{"nanos":72000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"69","timestamp":{"nanos":73000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson index f3946e576..513173052 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/hooks/hooks.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n\n Scenario: No tags and a undefined step\n When a step does not exist\n\n @some-tag\n Scenario: With a tag, a failure in the hook and a passed step\n When a step passes\n\n @with-attachment\n Scenario: With an tag, an valid attachment in the hook and a passed step\n When a step passes\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/hooks/hooks.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"8","keyword":"Scenario","location":{"column":3,"line":6},"name":"No tags and a passed step","steps":[{"id":"7","keyword":"When ","keywordType":"Action","location":{"column":5,"line":7},"text":"a step passes"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":9},"name":"No tags and a failed step","steps":[{"id":"9","keyword":"When ","keywordType":"Action","location":{"column":5,"line":10},"text":"a step fails"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":12},"name":"No tags and a undefined step","steps":[{"id":"11","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"a step does not exist"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Scenario","location":{"column":3,"line":16},"name":"With a tag, a failure in the hook and a passed step","steps":[{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":5,"line":17},"text":"a step passes"}],"tags":[{"id":"14","location":{"column":3,"line":15},"name":"@some-tag"}]}},{"scenario":{"description":"","examples":[],"id":"18","keyword":"Scenario","location":{"column":3,"line":20},"name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"id":"16","keyword":"When ","keywordType":"Action","location":{"column":5,"line":21},"text":"a step passes"}],"tags":[{"id":"17","location":{"column":3,"line":19},"name":"@with-attachment"}]}}],"description":" Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Hooks","tags":[]},"uri":"samples/hooks/hooks.feature"}} {"pickle":{"astNodeIds":["8"],"id":"20","language":"en","name":"No tags and a passed step","steps":[{"astNodeIds":["7"],"id":"19","text":"a step passes","type":"Action"}],"tags":[],"uri":"samples/hooks/hooks.feature"}} @@ -8,70 +8,70 @@ {"pickle":{"astNodeIds":["18"],"id":"28","language":"en","name":"With an tag, an valid attachment in the hook and a passed step","steps":[{"astNodeIds":["16"],"id":"27","text":"a step passes","type":"Action"}],"tags":[{"astNodeId":"17","name":"@with-attachment"}],"uri":"samples/hooks/hooks.feature"}} {"stepDefinition":{"id":"2","pattern":{"source":"a step passes","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"samples/hooks/hooks.feature.ts"}}} {"stepDefinition":{"id":"3","pattern":{"source":"a step fails","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/hooks/hooks.feature.ts"}}} -{"hook":{"id":"0","sourceReference":{"location":{"line":4},"uri":"samples/hooks/hooks.feature.ts"}}} -{"hook":{"id":"1","name":"A named hook","sourceReference":{"location":{"line":8},"uri":"samples/hooks/hooks.feature.ts"}}} -{"hook":{"id":"4","sourceReference":{"location":{"line":20},"uri":"samples/hooks/hooks.feature.ts"}}} -{"hook":{"id":"5","sourceReference":{"location":{"line":24},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@some-tag or @some-other-tag"}} -{"hook":{"id":"6","sourceReference":{"location":{"line":28},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@with-attachment"}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"33","pickleId":"20","testSteps":[{"hookId":"0","id":"29"},{"hookId":"1","id":"30"},{"id":"31","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"32"}]}} -{"testCase":{"id":"38","pickleId":"22","testSteps":[{"hookId":"0","id":"34"},{"hookId":"1","id":"35"},{"id":"36","pickleStepId":"21","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"37"}]}} -{"testCase":{"id":"43","pickleId":"24","testSteps":[{"hookId":"0","id":"39"},{"hookId":"1","id":"40"},{"id":"41","pickleStepId":"23","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"hookId":"4","id":"42"}]}} -{"testCase":{"id":"49","pickleId":"26","testSteps":[{"hookId":"0","id":"44"},{"hookId":"1","id":"45"},{"id":"46","pickleStepId":"25","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"5","id":"47"},{"hookId":"4","id":"48"}]}} -{"testCase":{"id":"55","pickleId":"28","testSteps":[{"hookId":"0","id":"50"},{"hookId":"1","id":"51"},{"id":"52","pickleStepId":"27","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"6","id":"53"},{"hookId":"4","id":"54"}]}} -{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"33","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"29","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"29","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"30","timestamp":{"nanos":4000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"30","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"31","timestamp":{"nanos":6000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"31","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"32","timestamp":{"nanos":8000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"32","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"57","testCaseId":"38","timestamp":{"nanos":11000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"57","testStepId":"34","timestamp":{"nanos":12000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"57","testStepId":"34","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"57","testStepId":"35","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"57","testStepId":"35","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"57","testStepId":"36","timestamp":{"nanos":16000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"57","testStepId":"36","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in step","type":"Error"},"message":"Exception in step\nsamples/hooks/hooks.feature:10","status":"FAILED"},"timestamp":{"nanos":17000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"57","testStepId":"37","timestamp":{"nanos":18000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"57","testStepId":"37","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"58","testCaseId":"43","timestamp":{"nanos":21000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"58","testStepId":"39","timestamp":{"nanos":22000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"58","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"58","testStepId":"40","timestamp":{"nanos":24000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"58","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"58","testStepId":"41","timestamp":{"nanos":26000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"58","testStepId":"41","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":27000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"58","testStepId":"42","timestamp":{"nanos":28000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"58","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"59","testCaseId":"49","timestamp":{"nanos":31000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"59","testStepId":"44","timestamp":{"nanos":32000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"59","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":33000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"nanos":34000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"59","testStepId":"46","timestamp":{"nanos":36000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"59","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":37000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"59","testStepId":"47","timestamp":{"nanos":38000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"59","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in conditional hook","type":"Error"},"message":"Exception in conditional hook\nsamples/hooks/hooks.feature:16","status":"FAILED"},"timestamp":{"nanos":39000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"59","testStepId":"48","timestamp":{"nanos":40000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"59","testStepId":"48","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":41000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"nanos":42000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"60","testCaseId":"55","timestamp":{"nanos":43000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"60","testStepId":"50","timestamp":{"nanos":44000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"60","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"60","testStepId":"51","timestamp":{"nanos":46000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"60","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":47000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"60","testStepId":"52","timestamp":{"nanos":48000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"60","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":49000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"60","testStepId":"53","timestamp":{"nanos":50000000,"seconds":0}}} -{"attachment":{"body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","testCaseStartedId":"60","testStepId":"53"}} -{"testStepFinished":{"testCaseStartedId":"60","testStepId":"53","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"60","testStepId":"54","timestamp":{"nanos":52000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"60","testStepId":"54","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"nanos":54000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":55000000,"seconds":0}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":4},"uri":"samples/hooks/hooks.feature.ts"},"type":"BEFORE_TEST_CASE"}} +{"hook":{"id":"1","name":"A named hook","sourceReference":{"location":{"line":8},"uri":"samples/hooks/hooks.feature.ts"},"type":"BEFORE_TEST_CASE"}} +{"hook":{"id":"4","sourceReference":{"location":{"line":20},"uri":"samples/hooks/hooks.feature.ts"},"type":"AFTER_TEST_CASE"}} +{"hook":{"id":"5","sourceReference":{"location":{"line":24},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@some-tag or @some-other-tag","type":"AFTER_TEST_CASE"}} +{"hook":{"id":"6","sourceReference":{"location":{"line":28},"uri":"samples/hooks/hooks.feature.ts"},"tagExpression":"@with-attachment","type":"AFTER_TEST_CASE"}} +{"testRunStarted":{"id":"29","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"34","pickleId":"20","testRunStartedId":"29","testSteps":[{"hookId":"0","id":"30"},{"hookId":"1","id":"31"},{"id":"32","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"33"}]}} +{"testCase":{"id":"39","pickleId":"22","testRunStartedId":"29","testSteps":[{"hookId":"0","id":"35"},{"hookId":"1","id":"36"},{"id":"37","pickleStepId":"21","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"4","id":"38"}]}} +{"testCase":{"id":"44","pickleId":"24","testRunStartedId":"29","testSteps":[{"hookId":"0","id":"40"},{"hookId":"1","id":"41"},{"id":"42","pickleStepId":"23","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"hookId":"4","id":"43"}]}} +{"testCase":{"id":"50","pickleId":"26","testRunStartedId":"29","testSteps":[{"hookId":"0","id":"45"},{"hookId":"1","id":"46"},{"id":"47","pickleStepId":"25","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"5","id":"48"},{"hookId":"4","id":"49"}]}} +{"testCase":{"id":"56","pickleId":"28","testRunStartedId":"29","testSteps":[{"hookId":"0","id":"51"},{"hookId":"1","id":"52"},{"id":"53","pickleStepId":"27","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"hookId":"6","id":"54"},{"hookId":"4","id":"55"}]}} +{"testCaseStarted":{"attempt":0,"id":"57","testCaseId":"34","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"30","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"30","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"31","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"31","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"32","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"32","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"33","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"33","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"58","testCaseId":"39","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"35","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"35","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"36","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"36","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"37","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"37","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in step","type":"Error"},"message":"Exception in step\nsamples/hooks/hooks.feature:10","status":"FAILED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"38","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"38","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"59","testCaseId":"44","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"40","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"41","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"41","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"42","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"42","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"43","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"43","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"60","testCaseId":"50","timestamp":{"nanos":31000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"45","timestamp":{"nanos":32000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":33000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"46","timestamp":{"nanos":34000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":35000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"47","timestamp":{"nanos":36000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":37000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"48","timestamp":{"nanos":38000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"48","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"Exception in conditional hook","type":"Error"},"message":"Exception in conditional hook\nsamples/hooks/hooks.feature:16","status":"FAILED"},"timestamp":{"nanos":39000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"49","timestamp":{"nanos":40000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"49","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":41000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"nanos":42000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"61","testCaseId":"56","timestamp":{"nanos":43000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"51","timestamp":{"nanos":44000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":45000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"52","timestamp":{"nanos":46000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":47000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"53","timestamp":{"nanos":48000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"53","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":49000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"54","timestamp":{"nanos":50000000,"seconds":0}}} +{"attachment":{"body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","testCaseStartedId":"61","testStepId":"54"}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"54","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":51000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"55","timestamp":{"nanos":52000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"55","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":53000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"61","timestamp":{"nanos":54000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"29","timestamp":{"nanos":55000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson index d10bc4bb3..6d974e093 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/minimal/minimal.feature.ndjson @@ -1,12 +1,12 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/minimal/minimal.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":9},"name":"cukes","steps":[{"id":"1","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"I have 42 cukes in my belly"}],"tags":[]}}],"description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it\n \n * This is\n * a bullet\n * list","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"minimal","tags":[]},"uri":"samples/minimal/minimal.feature"}} {"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"cukes","steps":[{"astNodeIds":["1"],"id":"3","text":"I have 42 cukes in my belly","type":"Context"}],"tags":[],"uri":"samples/minimal/minimal.feature"}} {"stepDefinition":{"id":"0","pattern":{"source":"I have {int} cukes in my belly","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/minimal/minimal.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} +{"testRunStarted":{"id":"5","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"7","pickleId":"4","testRunStartedId":"5","testSteps":[{"id":"6","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":7,"value":"42"},"parameterTypeName":"int"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"5","timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson index b2eddd562..f538d6f4f 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/parameter-types/parameter_types.feature.ndjson @@ -1,13 +1,13 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Parameter Types\n Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.\n\n Scenario: Flight transformer\n Given LHR-CDG has been delayed\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/parameter-types/parameter-types.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":10},"name":"Flight transformer","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"LHR-CDG has been delayed"}],"tags":[]}}],"description":" Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/parameter-types/parameter-types.feature"}} {"pickle":{"astNodeIds":["3"],"id":"5","language":"en","name":"Flight transformer","steps":[{"astNodeIds":["2"],"id":"4","text":"LHR-CDG has been delayed","type":"Context"}],"tags":[],"uri":"samples/parameter-types/parameter-types.feature"}} {"parameterType":{"id":"0","name":"flight","preferForRegularExpressionMatch":false,"regularExpressions":["([A-Z]{3})-([A-Z]{3})"],"sourceReference":{"location":{"line":8},"uri":"samples/parameter-types/parameter-types.feature.ts"},"useForSnippets":true}} {"stepDefinition":{"id":"1","pattern":{"source":"{flight} has been delayed","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/parameter-types/parameter-types.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"7","pickleId":"5","testSteps":[{"id":"6","pickleStepId":"4","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[],"start":0,"value":"LHR"},{"children":[],"start":4,"value":"CDG"}],"start":0,"value":"LHR-CDG"},"parameterTypeName":"flight"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":5000000,"seconds":0}}} +{"testRunStarted":{"id":"6","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"8","pickleId":"5","testRunStartedId":"6","testSteps":[{"id":"7","pickleStepId":"4","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[{"children":[],"start":0,"value":"LHR"},{"children":[],"start":4,"value":"CDG"}],"start":0,"value":"LHR-CDG"},"parameterTypeName":"flight"}]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"9","testCaseId":"8","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"7","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"7","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"6","timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson index b8a2c03b4..5ece61bc1 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/pending/pending.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Pending steps\n During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.\n\n Scenario: Unimplemented step signals pending status\n Given an unimplemented pending step\n\n Scenario: Steps before unimplemented steps are executed\n Given an implemented non-pending step\n And an unimplemented pending step\n\n Scenario: Steps after unimplemented steps are skipped\n Given an unimplemented pending step\n And an implemented step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/pending/pending.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"4","keyword":"Scenario","location":{"column":3,"line":9},"name":"Unimplemented step signals pending status","steps":[{"id":"3","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":10},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"7","keyword":"Scenario","location":{"column":3,"line":12},"name":"Steps before unimplemented steps are executed","steps":[{"id":"5","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":13},"text":"an implemented non-pending step"},{"id":"6","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":14},"text":"an unimplemented pending step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"10","keyword":"Scenario","location":{"column":3,"line":16},"name":"Steps after unimplemented steps are skipped","steps":[{"id":"8","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":17},"text":"an unimplemented pending step"},{"id":"9","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":18},"text":"an implemented step that is skipped"}],"tags":[]}}],"description":" During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Pending steps","tags":[]},"uri":"samples/pending/pending.feature"}} {"pickle":{"astNodeIds":["4"],"id":"12","language":"en","name":"Unimplemented step signals pending status","steps":[{"astNodeIds":["3"],"id":"11","text":"an unimplemented pending step","type":"Context"}],"tags":[],"uri":"samples/pending/pending.feature"}} @@ -7,24 +7,24 @@ {"stepDefinition":{"id":"0","pattern":{"source":"an implemented non-pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/pending/pending.feature.ts"}}} {"stepDefinition":{"id":"1","pattern":{"source":"an implemented step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/pending/pending.feature.ts"}}} {"stepDefinition":{"id":"2","pattern":{"source":"an unimplemented pending step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/pending/pending.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"20","pickleId":"12","testSteps":[{"id":"19","pickleStepId":"11","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"23","pickleId":"15","testSteps":[{"id":"21","pickleStepId":"13","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"22","pickleStepId":"14","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"26","pickleId":"18","testSteps":[{"id":"24","pickleStepId":"16","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"17","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"20","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"19","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"19","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"23","timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"nanos":6000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"28","testStepId":"22","timestamp":{"nanos":8000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"28","testStepId":"22","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":9000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"29","testCaseId":"26","timestamp":{"nanos":11000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"nanos":12000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"29","testStepId":"25","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"29","testStepId":"25","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} +{"testRunStarted":{"id":"19","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"21","pickleId":"12","testRunStartedId":"19","testSteps":[{"id":"20","pickleStepId":"11","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"24","pickleId":"15","testRunStartedId":"19","testSteps":[{"id":"22","pickleStepId":"13","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"23","pickleStepId":"14","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"27","pickleId":"18","testRunStartedId":"19","testSteps":[{"id":"25","pickleStepId":"16","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"26","pickleStepId":"17","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"21","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"20","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"20","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"29","testCaseId":"24","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"22","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"22","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"23","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"23","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"30","testCaseId":"27","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"25","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"25","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"message":"TODO","status":"PENDING"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"26","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"26","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"19","timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson index 58839fa63..81ac0e1cf 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/rules/rules.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/rules/rules.feature"}} {"gherkinDocument":{"comments":[{"location":{"column":1,"line":8},"text":" # Unhappy path"},{"location":{"column":1,"line":15},"text":" # Happy path"},{"location":{"column":1,"line":24},"text":" # Unhappy path"}],"feature":{"children":[{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"10","keyword":"Example","location":{"column":5,"line":9},"name":"Not enough money","steps":[{"id":"6","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":10},"text":"the customer has 100 cents"},{"id":"7","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":11},"text":"there are chocolate bars in stock"},{"id":"8","keyword":"When ","keywordType":"Action","location":{"column":7,"line":12},"text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"9","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":13},"text":"the sale should not happen"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"15","keyword":"Example","location":{"column":5,"line":16},"name":"Enough money","steps":[{"id":"11","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":17},"text":"the customer has 100 cents"},{"id":"12","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":18},"text":"there are chocolate bars in stock"},{"id":"13","keyword":"When ","keywordType":"Action","location":{"column":7,"line":19},"text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"14","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":20},"text":"the sale should happen"}],"tags":[]}}],"description":"","id":"16","keyword":"Rule","location":{"column":3,"line":7},"name":"A sale cannot happen if the customer does not have enough money","tags":[]}},{"rule":{"children":[{"scenario":{"description":"","examples":[],"id":"21","keyword":"Example","location":{"column":5,"line":25},"name":"No chocolates left","steps":[{"id":"17","keyword":"Given ","keywordType":"Context","location":{"column":7,"line":26},"text":"the customer has 100 cents"},{"id":"18","keyword":"And ","keywordType":"Conjunction","location":{"column":7,"line":27},"text":"there are no chocolate bars in stock"},{"id":"19","keyword":"When ","keywordType":"Action","location":{"column":7,"line":28},"text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"20","keyword":"Then ","keywordType":"Outcome","location":{"column":7,"line":29},"text":"the sale should not happen"}],"tags":[]}}],"description":"","id":"23","keyword":"Rule","location":{"column":3,"line":23},"name":"a sale cannot happen if there is no stock","tags":[{"id":"22","location":{"column":3,"line":22},"name":"@some-tag"}]}}],"description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Usage of a `Rule`","tags":[]},"uri":"samples/rules/rules.feature"}} {"pickle":{"astNodeIds":["10"],"id":"28","language":"en","name":"Not enough money","steps":[{"astNodeIds":["6"],"id":"24","text":"the customer has 100 cents","type":"Context"},{"astNodeIds":["7"],"id":"25","text":"there are chocolate bars in stock","type":"Context"},{"astNodeIds":["8"],"id":"26","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action"},{"astNodeIds":["9"],"id":"27","text":"the sale should not happen","type":"Outcome"}],"tags":[],"uri":"samples/rules/rules.feature"}} @@ -10,38 +10,38 @@ {"stepDefinition":{"id":"3","pattern":{"source":"the customer tries to buy a {int} cent chocolate bar","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":16},"uri":"samples/rules/rules.feature.ts"}}} {"stepDefinition":{"id":"4","pattern":{"source":"the sale should not happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"samples/rules/rules.feature.ts"}}} {"stepDefinition":{"id":"5","pattern":{"source":"the sale should happen","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":26},"uri":"samples/rules/rules.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"43","pickleId":"28","testSteps":[{"id":"39","pickleStepId":"24","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"40","pickleStepId":"25","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"41","pickleStepId":"26","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"125"},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"27","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"48","pickleId":"33","testSteps":[{"id":"44","pickleStepId":"29","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"45","pickleStepId":"30","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"46","pickleStepId":"31","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"75"},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"32","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"53","pickleId":"38","testSteps":[{"id":"49","pickleStepId":"34","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"50","pickleStepId":"35","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"51","pickleStepId":"36","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"37","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"43","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"39","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"39","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"40","timestamp":{"nanos":4000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"41","timestamp":{"nanos":6000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"41","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"42","timestamp":{"nanos":8000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"55","testCaseId":"48","timestamp":{"nanos":11000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"nanos":12000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"55","testStepId":"45","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"55","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"55","testStepId":"46","timestamp":{"nanos":16000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"55","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":17000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"nanos":18000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"53","timestamp":{"nanos":21000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"nanos":22000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"50","timestamp":{"nanos":24000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"51","timestamp":{"nanos":26000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"56","testStepId":"52","timestamp":{"nanos":28000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"56","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":31000000,"seconds":0}}} +{"testRunStarted":{"id":"39","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"44","pickleId":"28","testRunStartedId":"39","testSteps":[{"id":"40","pickleStepId":"24","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"41","pickleStepId":"25","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"42","pickleStepId":"26","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"125"},"parameterTypeName":"int"}]}]},{"id":"43","pickleStepId":"27","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"49","pickleId":"33","testRunStartedId":"39","testSteps":[{"id":"45","pickleStepId":"29","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"46","pickleStepId":"30","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"47","pickleStepId":"31","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"75"},"parameterTypeName":"int"}]}]},{"id":"48","pickleStepId":"32","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"54","pickleId":"38","testRunStartedId":"39","testSteps":[{"id":"50","pickleStepId":"34","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":17,"value":"100"},"parameterTypeName":"int"}]}]},{"id":"51","pickleStepId":"35","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"52","pickleStepId":"36","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":28,"value":"1"},"parameterTypeName":"int"}]}]},{"id":"53","pickleStepId":"37","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"55","testCaseId":"44","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"40","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"40","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"41","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"41","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"42","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"42","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"43","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"43","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"49","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"45","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"45","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"46","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"46","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"47","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"47","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"48","timestamp":{"nanos":18000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"48","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":19000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"nanos":20000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"57","testCaseId":"54","timestamp":{"nanos":21000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"50","timestamp":{"nanos":22000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"50","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":23000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"51","timestamp":{"nanos":24000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"51","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":25000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"52","timestamp":{"nanos":26000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"52","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":27000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"53","timestamp":{"nanos":28000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"53","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":29000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"nanos":30000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"39","timestamp":{"nanos":31000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson index 746921a8b..3b1808bad 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/skipped/skipped.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Skipping scenarios\n\n Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.\n\n @skip\n Scenario: Skipping from a Before hook\n Given a step that is skipped\n\n Scenario: Skipping from a step doesn't affect the previous steps\n Given a step that does not skip\n And I skip a step\n\n Scenario: Skipping from a step causes the rest of the scenario to be skipped\n Given I skip a step\n And a step that is skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/skipped/skipped.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Skipping from a Before hook","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"a step that is skipped"}],"tags":[{"id":"5","location":{"column":3,"line":9},"name":"@skip"}]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":13},"name":"Skipping from a step doesn't affect the previous steps","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":14},"text":"a step that does not skip"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":15},"text":"I skip a step"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"12","keyword":"Scenario","location":{"column":3,"line":17},"name":"Skipping from a step causes the rest of the scenario to be skipped","steps":[{"id":"10","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":18},"text":"I skip a step"},{"id":"11","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":19},"text":"a step that is skipped"}],"tags":[]}}],"description":" Hooks and step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Skipping scenarios","tags":[]},"uri":"samples/skipped/skipped.feature"}} {"pickle":{"astNodeIds":["6"],"id":"14","language":"en","name":"Skipping from a Before hook","steps":[{"astNodeIds":["4"],"id":"13","text":"a step that is skipped","type":"Context"}],"tags":[{"astNodeId":"5","name":"@skip"}],"uri":"samples/skipped/skipped.feature"}} @@ -7,27 +7,27 @@ {"stepDefinition":{"id":"1","pattern":{"source":"a step that does not skip","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/skipped/skipped.feature.ts"}}} {"stepDefinition":{"id":"2","pattern":{"source":"a step that is skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":11},"uri":"samples/skipped/skipped.feature.ts"}}} {"stepDefinition":{"id":"3","pattern":{"source":"I skip a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":15},"uri":"samples/skipped/skipped.feature.ts"}}} -{"hook":{"id":"0","sourceReference":{"location":{"line":3},"uri":"samples/skipped/skipped.feature.ts"},"tagExpression":"@skip"}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"23","pickleId":"14","testSteps":[{"hookId":"0","id":"21"},{"id":"22","pickleStepId":"13","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"26","pickleId":"17","testSteps":[{"id":"24","pickleStepId":"15","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"25","pickleStepId":"16","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCase":{"id":"29","pickleId":"20","testSteps":[{"id":"27","pickleStepId":"18","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"28","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"30","testCaseId":"23","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"30","testStepId":"21","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"30","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"30","testStepId":"22","timestamp":{"nanos":4000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"30","testStepId":"22","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":5000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"31","testCaseId":"26","timestamp":{"nanos":7000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"nanos":8000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"31","testStepId":"25","timestamp":{"nanos":10000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"31","testStepId":"25","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":11000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"32","testCaseId":"29","timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"32","testStepId":"27","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"32","testStepId":"27","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"32","testStepId":"28","timestamp":{"nanos":16000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"32","testStepId":"28","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":17000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"nanos":18000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"nanos":19000000,"seconds":0}}} +{"hook":{"id":"0","sourceReference":{"location":{"line":3},"uri":"samples/skipped/skipped.feature.ts"},"tagExpression":"@skip","type":"BEFORE_TEST_CASE"}} +{"testRunStarted":{"id":"21","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"24","pickleId":"14","testRunStartedId":"21","testSteps":[{"hookId":"0","id":"22"},{"id":"23","pickleStepId":"13","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"27","pickleId":"17","testRunStartedId":"21","testSteps":[{"id":"25","pickleStepId":"15","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"26","pickleStepId":"16","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCase":{"id":"30","pickleId":"20","testRunStartedId":"21","testSteps":[{"id":"28","pickleStepId":"18","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"29","pickleStepId":"19","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"31","testCaseId":"24","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"22","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"22","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"23","timestamp":{"nanos":4000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"23","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":5000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"nanos":6000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"32","testCaseId":"27","timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"25","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"25","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"nanos":10000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":11000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"nanos":12000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"33","testCaseId":"30","timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"28","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"28","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"29","timestamp":{"nanos":16000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"29","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":17000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"nanos":18000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":true,"testRunStartedId":"21","timestamp":{"nanos":19000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson index 4bbcea535..49d10f541 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/stack-traces/stack_traces.feature.ndjson @@ -1,12 +1,12 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Stack traces\n Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.\n\n Scenario: A failing step\n When a step throws an exception\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/stack-traces/stack-traces.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"2","keyword":"Scenario","location":{"column":3,"line":8},"name":"A failing step","steps":[{"id":"1","keyword":"When ","keywordType":"Action","location":{"column":5,"line":9},"text":"a step throws an exception"}],"tags":[]}}],"description":" Stack traces can help you diagnose the source of a bug.\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default\n\n The first line of the stack trace will contain a reference to the feature file.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Stack traces","tags":[]},"uri":"samples/stack-traces/stack-traces.feature"}} {"pickle":{"astNodeIds":["2"],"id":"4","language":"en","name":"A failing step","steps":[{"astNodeIds":["1"],"id":"3","text":"a step throws an exception","type":"Action"}],"tags":[],"uri":"samples/stack-traces/stack-traces.feature"}} {"stepDefinition":{"id":"0","pattern":{"source":"a step throws an exception","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/stack-traces/stack-traces.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"6","pickleId":"4","testSteps":[{"id":"5","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"BOOM","type":"Error"},"message":"BOOM\nsamples/stack-traces/stack-traces.feature:9","status":"FAILED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} +{"testRunStarted":{"id":"5","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"7","pickleId":"4","testRunStartedId":"5","testSteps":[{"id":"6","pickleStepId":"3","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"8","testCaseId":"7","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"6","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"6","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"exception":{"message":"BOOM","type":"Error"},"message":"BOOM\nsamples/stack-traces/stack-traces.feature:9","status":"FAILED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"5","timestamp":{"nanos":5000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson index f15440837..aafa4d473 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/undefined/undefined.feature.ndjson @@ -1,4 +1,4 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} {"source":{"data":"Feature: Undefined steps\n\n At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE\n\n Scenario: An undefined step causes a failure\n Given a step that is yet to be defined\n\n Scenario: Steps before undefined steps are executed\n Given an implemented step\n And a step that is yet to be defined\n\n Scenario: Steps after undefined steps are skipped\n Given a step that is yet to be defined\n And a step that will be skipped\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/undefined/undefined.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"3","keyword":"Scenario","location":{"column":3,"line":7},"name":"An undefined step causes a failure","steps":[{"id":"2","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":8},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"6","keyword":"Scenario","location":{"column":3,"line":10},"name":"Steps before undefined steps are executed","steps":[{"id":"4","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":11},"text":"an implemented step"},{"id":"5","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":12},"text":"a step that is yet to be defined"}],"tags":[]}},{"scenario":{"description":"","examples":[],"id":"9","keyword":"Scenario","location":{"column":3,"line":14},"name":"Steps after undefined steps are skipped","steps":[{"id":"7","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":15},"text":"a step that is yet to be defined"},{"id":"8","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":16},"text":"a step that will be skipped"}],"tags":[]}}],"description":" At runtime, Cucumber may encounter a step in a scenario that it cannot match to a\n step definition. In these cases, the scenario is not able to run and so the step status\n will be UNDEFINED, with subsequent steps being SKIPPED and the overall result will be FAILURE","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Undefined steps","tags":[]},"uri":"samples/undefined/undefined.feature"}} {"pickle":{"astNodeIds":["3"],"id":"11","language":"en","name":"An undefined step causes a failure","steps":[{"astNodeIds":["2"],"id":"10","text":"a step that is yet to be defined","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} @@ -6,24 +6,24 @@ {"pickle":{"astNodeIds":["9"],"id":"17","language":"en","name":"Steps after undefined steps are skipped","steps":[{"astNodeIds":["7"],"id":"15","text":"a step that is yet to be defined","type":"Context"},{"astNodeIds":["8"],"id":"16","text":"a step that will be skipped","type":"Context"}],"tags":[],"uri":"samples/undefined/undefined.feature"}} {"stepDefinition":{"id":"0","pattern":{"source":"an implemented step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":3},"uri":"samples/undefined/undefined.feature.ts"}}} {"stepDefinition":{"id":"1","pattern":{"source":"a step that will be skipped","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"samples/undefined/undefined.feature.ts"}}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"19","pickleId":"11","testSteps":[{"id":"18","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} -{"testCase":{"id":"22","pickleId":"14","testSteps":[{"id":"20","pickleStepId":"12","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"21","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} -{"testCase":{"id":"25","pickleId":"17","testSteps":[{"id":"23","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"24","pickleStepId":"16","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"26","testCaseId":"19","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"26","testStepId":"18","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"26","testStepId":"18","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"26","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"22","timestamp":{"nanos":5000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"20","timestamp":{"nanos":6000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"20","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"27","testStepId":"21","timestamp":{"nanos":8000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"27","testStepId":"21","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":9000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} -{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"25","timestamp":{"nanos":11000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"28","testStepId":"23","timestamp":{"nanos":12000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"28","testStepId":"23","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":13000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"28","testStepId":"24","timestamp":{"nanos":14000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"28","testStepId":"24","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":17000000,"seconds":0}}} +{"testRunStarted":{"id":"18","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"20","pickleId":"11","testRunStartedId":"18","testSteps":[{"id":"19","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"23","pickleId":"14","testRunStartedId":"18","testSteps":[{"id":"21","pickleStepId":"12","stepDefinitionIds":["0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"22","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCase":{"id":"26","pickleId":"17","testRunStartedId":"18","testSteps":[{"id":"24","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"25","pickleStepId":"16","stepDefinitionIds":["1"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} +{"testCaseStarted":{"attempt":0,"id":"27","testCaseId":"20","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"27","testStepId":"19","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"27","testStepId":"19","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"27","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"28","testCaseId":"23","timestamp":{"nanos":5000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"nanos":6000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"duration":{"nanos":1000000,"seconds":0},"status":"PASSED"},"timestamp":{"nanos":7000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"22","timestamp":{"nanos":8000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"22","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":9000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"nanos":10000000,"seconds":0},"willBeRetried":false}} +{"testCaseStarted":{"attempt":0,"id":"29","testCaseId":"26","timestamp":{"nanos":11000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"nanos":12000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":13000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"25","timestamp":{"nanos":14000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"25","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"SKIPPED"},"timestamp":{"nanos":15000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"nanos":16000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"18","timestamp":{"nanos":17000000,"seconds":0}}} diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson index ab4bb0bef..40f7ebd4a 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/unknown-parameter-type/unknown_parameter_type.feature.ndjson @@ -1,12 +1,12 @@ -{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"16.5.0"},"os":{"name":"darwin","version":"23.5.0"},"protocolVersion":"25.0.1","runtime":{"name":"node.js","version":"21.7.1"}}} -{"source":{"data":"Feature: Unknown Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"meta":{"ci":{"buildNumber":"154666429","git":{"remote":"https://github.com/cucumber-ltd/shouty.rb.git","revision":"99684bcacf01d95875834d87903dcb072306c9ad"},"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"},"cpu":{"name":"x64"},"implementation":{"name":"fake-cucumber","version":"18.0.0"},"os":{"name":"darwin","version":"23.6.0"},"protocolVersion":"27.0.0","runtime":{"name":"node.js","version":"22.7.0"}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[],"id":"1","keyword":"Scenario","location":{"column":3,"line":5},"name":"undefined parameter type","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":6},"text":"CDG is closed because of a strike"}],"tags":[]}}],"description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Parameter Types","tags":[]},"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"pickle":{"astNodeIds":["1"],"id":"3","language":"en","name":"undefined parameter type","steps":[{"astNodeIds":["0"],"id":"2","text":"CDG is closed because of a strike","type":"Context"}],"tags":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} {"undefinedParameterType":{"expression":"{airport} is closed because of a strike","name":"airport"}} -{"testRunStarted":{"timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"5","pickleId":"3","testSteps":[{"id":"4","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} -{"testCaseStarted":{"attempt":0,"id":"6","testCaseId":"5","timestamp":{"nanos":1000000,"seconds":0}}} -{"testStepStarted":{"testCaseStartedId":"6","testStepId":"4","timestamp":{"nanos":2000000,"seconds":0}}} -{"testStepFinished":{"testCaseStartedId":"6","testStepId":"4","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} -{"testCaseFinished":{"testCaseStartedId":"6","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"nanos":5000000,"seconds":0}}} +{"testRunStarted":{"id":"4","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"6","pickleId":"3","testRunStartedId":"4","testSteps":[{"id":"5","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}]}} +{"testCaseStarted":{"attempt":0,"id":"7","testCaseId":"6","timestamp":{"nanos":1000000,"seconds":0}}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"5","timestamp":{"nanos":2000000,"seconds":0}}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"5","testStepResult":{"duration":{"nanos":0,"seconds":0},"status":"UNDEFINED"},"timestamp":{"nanos":3000000,"seconds":0}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"nanos":4000000,"seconds":0},"willBeRetried":false}} +{"testRunFinished":{"success":false,"testRunStartedId":"4","timestamp":{"nanos":5000000,"seconds":0}}} From 3e47ddda59ee13ae4c6e0631fb13d6bdd0f04d05 Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:05:18 -0600 Subject: [PATCH 216/218] Updated ndjson serializer to support new HookType enum. Updated FluentAssertion property selector rule to ignore the TestRunStartedId. --- .../PayloadProcessing/NdjsonSerializer.cs | 1 + .../CucumberMessages.Tests.csproj | 4 +- .../CucumberMessagesValidator.cs | 2 +- ...ionCucumberMessagePropertySelectionRule.cs | 3 +- .../ambiguous/ambiguous.feature.ndjson | 32 +++--- .../background/background.feature.ndjson | 104 +++++++++--------- 6 files changed, 75 insertions(+), 71 deletions(-) diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs index 5afbadde3..0d9daf9ad 100644 --- a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -23,6 +23,7 @@ public class NdjsonSerializer options.Converters.Add(new CucumberMessageEnumConverter()); options.Converters.Add(new CucumberMessageEnumConverter()); options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj index 7ba711bf8..beec5f507 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj @@ -72,7 +72,9 @@ - + + Always + Always diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs index 73e0abf8f..8e14250b1 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessagesValidator.cs @@ -119,7 +119,7 @@ private void CompareMessageType() { // For TestStepFinished, we will separate out those related to hooks; // the regular comparison will be done for TestStepFinished related to PickleSteps/StepDefinitions - // Hook related TestStepFinished - the order is indetermant, so we will check quantity, and count of Statuses + // Hook related TestStepFinished - the order is indeterminate, so we will check quantity, and count of Statuses // Hook related TestSteps are found by following the testStepId of the Finished message to the related TestStep. If the TestStep has a value for pickleStepId, then it is a regular step. // if it has a hookId, it is a hook step diff --git a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs index f473843df..7fad11c78 100644 --- a/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs +++ b/Tests/CucumberMessages.CompatibilityTests/FluentAsssertionCucumberMessagePropertySelectionRule.cs @@ -33,7 +33,8 @@ public class FluentAsssertionCucumberMessagePropertySelectionRule : IMemberSelec private List PropertiesToSkip = new List() { "Location", "Line", "Column", "Uri", "JavaMethod", "JavaStackTraceElement", "Exception", "Duration", "Start", "FileName", "Message", "Type", "StackTrace", "UseForSnippets", - "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", "TestCaseStartedId","TestStepId", "TestCaseId", "WorkerId", + "Id", "AstNodeIds", "StepDefinitionIds", "HookId", "PickleStepId", "PickleId", + "TestRunStartedId", "TestCaseStartedId", "TestStepId", "TestCaseId", "WorkerId", "ProtocolVersion", "Implementation", "Runtime", "Cpu", "Os", "Ci" }; diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson index d4afb6c48..5f5efb50c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/ambiguous/ambiguous.feature.ndjson @@ -1,16 +1,16 @@ -{"meta":{"protocolVersion":"26.0.0","implementation":{"name":"Reqnroll","version":"2.1.1-local+46152309beb5ca95501132a597284bc53db62081"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} -{"source":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","data":"Feature: ambiguous\r\n\r\nThis feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite\r\n\r\nScenario: Ambiguous\r\n\tGiven a step that matches more than one step binding\r\n\tThen this step gets skipped because of the prior ambiguous step\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} -{"gherkinDocument":{"uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"ambiguous","description":"This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite","children":[{"scenario":{"location":{"line":7,"column":1},"tags":[],"keyword":"Scenario","name":"Ambiguous","description":"","steps":[{"location":{"line":8,"column":2},"keyword":"Given ","keywordType":"Context","text":"a step that matches more than one step binding","id":"0"},{"location":{"line":9,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"this step gets skipped because of the prior ambiguous step","id":"1"}],"examples":[],"id":"2"}}]},"comments":[]}} -{"pickle":{"id":"5","uri":"FeatureFile2413bc9e5c5645a3913962868bb38b67.feature","name":"Ambiguous","language":"en-US","steps":[{"astNodeIds":["0"],"id":"3","type":"Context","text":"a step that matches more than one step binding"},{"astNodeIds":["1"],"id":"4","type":"Outcome","text":"this step gets skipped because of the prior ambiguous step"}],"tags":[],"astNodeIds":["2"]}} -{"stepDefinition":{"id":"6","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"FirstMatchingStep","methodParameterTypes":[]}}}} -{"stepDefinition":{"id":"7","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"SecondMatchingStep","methodParameterTypes":[]}}}} -{"stepDefinition":{"id":"8","pattern":{"source":"this step gets skipped because of the prior ambiguous step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Ambiguous","methodName":"ThirdSkippedStep","methodParameterTypes":[]}}}} -{"testRunStarted":{"timestamp":{"seconds":1726222400,"nanos":625585200}}} -{"testCase":{"id":"9","pickleId":"5","testSteps":[{"id":"11","pickleStepId":"3","stepDefinitionIds":["6","7"],"stepMatchArgumentsLists":[]},{"id":"12","pickleStepId":"4","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"10","testCaseId":"9","timestamp":{"seconds":1726222400,"nanos":664903000}}} -{"testStepStarted":{"testCaseStartedId":"10","testStepId":"11","timestamp":{"seconds":1726222400,"nanos":666997500}}} -{"testStepFinished":{"testCaseStartedId":"10","testStepId":"11","testStepResult":{"duration":{"seconds":0,"nanos":7889700},"message":"","status":"AMBIGUOUS"},"timestamp":{"seconds":1726222400,"nanos":674887200}}} -{"testStepStarted":{"testCaseStartedId":"10","testStepId":"12","timestamp":{"seconds":1726222400,"nanos":679031100}}} -{"testStepFinished":{"testCaseStartedId":"10","testStepId":"12","testStepResult":{"duration":{"seconds":0,"nanos":817600},"message":"","status":"SKIPPED"},"timestamp":{"seconds":1726222400,"nanos":679848700}}} -{"testCaseFinished":{"testCaseStartedId":"10","timestamp":{"seconds":1726222400,"nanos":688807200},"willBeRetried":false}} -{"testRunFinished":{"success":false,"timestamp":{"seconds":1726222400,"nanos":697037200}}} +{"testRunStarted":{"timestamp":{"seconds":1731409038,"nanos":497394500},"id":"1"}} +{"meta":{"protocolVersion":"27.0.0","implementation":{"name":"Reqnroll","version":"2.1.2-local"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.ambiguous.Ambiguous","methodName":"FirstMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"3","pattern":{"source":"a step that matches more than one step binding","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.ambiguous.Ambiguous","methodName":"SecondMatchingStep","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"4","pattern":{"source":"this step gets skipped because of the prior ambiguous step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.ambiguous.Ambiguous","methodName":"ThirdSkippedStep","methodParameterTypes":[]}}}} +{"source":{"uri":"FeatureFilec56ed238335e44d7b346af2b19d8ffff.feature","data":"Feature: ambiguous\r\n\r\nThis feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite\r\n\r\nScenario: Ambiguous\r\n\tGiven a step that matches more than one step binding\r\n\tThen this step gets skipped because of the prior ambiguous step\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFilec56ed238335e44d7b346af2b19d8ffff.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"ambiguous","description":"This feature demonstrates Cucumber Messages emitted when a step results in an ambigous match with Step Definitions\r\n\r\nNOTE: This feature is not present in the CCK, but added to round out the Reqnroll messages validation suite","children":[{"scenario":{"location":{"line":7,"column":1},"tags":[],"keyword":"Scenario","name":"Ambiguous","description":"","steps":[{"location":{"line":8,"column":2},"keyword":"Given ","keywordType":"Context","text":"a step that matches more than one step binding","id":"5"},{"location":{"line":9,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"this step gets skipped because of the prior ambiguous step","id":"6"}],"examples":[],"id":"7"}}]},"comments":[]}} +{"pickle":{"id":"10","uri":"FeatureFilec56ed238335e44d7b346af2b19d8ffff.feature","name":"Ambiguous","language":"en-US","steps":[{"astNodeIds":["5"],"id":"8","type":"Context","text":"a step that matches more than one step binding"},{"astNodeIds":["6"],"id":"9","type":"Outcome","text":"this step gets skipped because of the prior ambiguous step"}],"tags":[],"astNodeIds":["7"]}} +{"testCase":{"id":"11","pickleId":"10","testSteps":[{"id":"13","pickleStepId":"8","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"14","pickleStepId":"9","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"1"}} +{"testCaseStarted":{"attempt":0,"id":"12","testCaseId":"11","timestamp":{"seconds":1731409038,"nanos":524110900}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"13","timestamp":{"seconds":1731409038,"nanos":526836000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"13","testStepResult":{"duration":{"seconds":0,"nanos":8662300},"message":"","status":"AMBIGUOUS"},"timestamp":{"seconds":1731409038,"nanos":535498300}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"14","timestamp":{"seconds":1731409038,"nanos":536942100}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"14","testStepResult":{"duration":{"seconds":0,"nanos":1110300},"message":"","status":"SKIPPED"},"timestamp":{"seconds":1731409038,"nanos":538052400}}} +{"testCaseFinished":{"testCaseStartedId":"12","timestamp":{"seconds":1731409038,"nanos":547681000},"willBeRetried":false}} +{"testRunFinished":{"success":false,"timestamp":{"seconds":1731409038,"nanos":565889700},"testRunStartedId":"1"}} \ No newline at end of file diff --git a/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson b/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson index cfaced93a..b1f557c8c 100644 --- a/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson +++ b/Tests/CucumberMessages.CompatibilityTests/Samples/background/background.feature.ndjson @@ -1,52 +1,52 @@ -{"meta":{"protocolVersion":"26.0.1","implementation":{"name":"Reqnroll","version":"2.1.1-local+098e7ef7a96ecab08f239f15463aec6b9098c778"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} -{"source":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n\r\nRule: A rule with a background\r\n\tBackground: First Transfer Money\r\n\t\tWhen I transfer $50 from savings to checking\r\n\t\tThen My savings account has a balance of $150\r\n\tScenario: total balance unchanged\r\n\t\tWhen the accounts are combined\r\n\t\tThen I have $700\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} -{"gherkinDocument":{"uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"0"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"1"}],"id":"2"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"3"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"4"}],"examples":[],"id":"5"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"6"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"7"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"8"}],"examples":[],"id":"9"}},{"rule":{"location":{"line":19,"column":1},"tags":[],"keyword":"Rule","name":"A rule with a background","description":"","children":[{"background":{"location":{"line":20,"column":2},"keyword":"Background","name":"First Transfer Money","description":"","steps":[{"location":{"line":21,"column":3},"keyword":"When ","keywordType":"Action","text":"I transfer $50 from savings to checking","id":"10"},{"location":{"line":22,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"My savings account has a balance of $150","id":"11"}],"id":"12"}},{"scenario":{"location":{"line":23,"column":2},"tags":[],"keyword":"Scenario","name":"total balance unchanged","description":"","steps":[{"location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"13"},{"location":{"line":25,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"14"}],"examples":[],"id":"15"}}],"id":"16"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} -{"pickle":{"id":"21","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["0"],"id":"17","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"18","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["3"],"id":"19","type":"Action","text":"the accounts are combined"},{"astNodeIds":["4"],"id":"20","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["5"]}} -{"pickle":{"id":"27","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["0"],"id":"22","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"23","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["6"],"id":"24","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["7"],"id":"25","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["8"],"id":"26","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["9"]}} -{"pickle":{"id":"34","uri":"FeatureFile404186dbd4744974b4ca4ff0bad1ea91.feature","name":"total balance unchanged","language":"en-US","steps":[{"astNodeIds":["0"],"id":"28","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["1"],"id":"29","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["10"],"id":"30","type":"Action","text":"I transfer $50 from savings to checking"},{"astNodeIds":["11"],"id":"31","type":"Outcome","text":"My savings account has a balance of $150"},{"astNodeIds":["13"],"id":"32","type":"Action","text":"the accounts are combined"},{"astNodeIds":["14"],"id":"33","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["15"]}} -{"stepDefinition":{"id":"35","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} -{"stepDefinition":{"id":"36","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} -{"stepDefinition":{"id":"37","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} -{"stepDefinition":{"id":"38","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} -{"stepDefinition":{"id":"39","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} -{"testRunStarted":{"timestamp":{"seconds":1727518980,"nanos":932282100}}} -{"testCase":{"id":"40","pickleId":"21","testSteps":[{"id":"42","pickleStepId":"17","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"43","pickleStepId":"18","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"44","pickleStepId":"19","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"45","pickleStepId":"20","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"41","testCaseId":"40","timestamp":{"seconds":1727518980,"nanos":988751100}}} -{"testStepStarted":{"testCaseStartedId":"41","testStepId":"42","timestamp":{"seconds":1727518980,"nanos":991631700}}} -{"testStepFinished":{"testCaseStartedId":"41","testStepId":"42","testStepResult":{"duration":{"seconds":0,"nanos":31313600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":22945300}}} -{"testStepStarted":{"testCaseStartedId":"41","testStepId":"43","timestamp":{"seconds":1727518981,"nanos":25132700}}} -{"testStepFinished":{"testCaseStartedId":"41","testStepId":"43","testStepResult":{"duration":{"seconds":0,"nanos":856400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":25989100}}} -{"testStepStarted":{"testCaseStartedId":"41","testStepId":"44","timestamp":{"seconds":1727518981,"nanos":26528500}}} -{"testStepFinished":{"testCaseStartedId":"41","testStepId":"44","testStepResult":{"duration":{"seconds":0,"nanos":2514700},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":29043200}}} -{"testStepStarted":{"testCaseStartedId":"41","testStepId":"45","timestamp":{"seconds":1727518981,"nanos":29468400}}} -{"testStepFinished":{"testCaseStartedId":"41","testStepId":"45","testStepResult":{"duration":{"seconds":0,"nanos":728600},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":30197000}}} -{"testCaseFinished":{"testCaseStartedId":"41","timestamp":{"seconds":1727518981,"nanos":33150200},"willBeRetried":false}} -{"testCase":{"id":"46","pickleId":"27","testSteps":[{"id":"48","pickleStepId":"22","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"49","pickleStepId":"23","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"50","pickleStepId":"24","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"51","pickleStepId":"25","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"26","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"47","testCaseId":"46","timestamp":{"seconds":1727518981,"nanos":75179500}}} -{"testStepStarted":{"testCaseStartedId":"47","testStepId":"48","timestamp":{"seconds":1727518981,"nanos":75361800}}} -{"testStepFinished":{"testCaseStartedId":"47","testStepId":"48","testStepResult":{"duration":{"seconds":0,"nanos":378000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75739800}}} -{"testStepStarted":{"testCaseStartedId":"47","testStepId":"49","timestamp":{"seconds":1727518981,"nanos":75791400}}} -{"testStepFinished":{"testCaseStartedId":"47","testStepId":"49","testStepResult":{"duration":{"seconds":0,"nanos":133800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":75925200}}} -{"testStepStarted":{"testCaseStartedId":"47","testStepId":"50","timestamp":{"seconds":1727518981,"nanos":75964700}}} -{"testStepFinished":{"testCaseStartedId":"47","testStepId":"50","testStepResult":{"duration":{"seconds":0,"nanos":986400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":76951100}}} -{"testStepStarted":{"testCaseStartedId":"47","testStepId":"51","timestamp":{"seconds":1727518981,"nanos":76991800}}} -{"testStepFinished":{"testCaseStartedId":"47","testStepId":"51","testStepResult":{"duration":{"seconds":0,"nanos":739400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":77731200}}} -{"testStepStarted":{"testCaseStartedId":"47","testStepId":"52","timestamp":{"seconds":1727518981,"nanos":77758700}}} -{"testStepFinished":{"testCaseStartedId":"47","testStepId":"52","testStepResult":{"duration":{"seconds":0,"nanos":283000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":78041700}}} -{"testCaseFinished":{"testCaseStartedId":"47","timestamp":{"seconds":1727518981,"nanos":78211800},"willBeRetried":false}} -{"testCase":{"id":"53","pickleId":"34","testSteps":[{"id":"55","pickleStepId":"28","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"56","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"57","pickleStepId":"30","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"50"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"58","pickleStepId":"31","stepDefinitionIds":["39"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"150"},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"60","pickleStepId":"33","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}]}} -{"testCaseStarted":{"attempt":0,"id":"54","testCaseId":"53","timestamp":{"seconds":1727518981,"nanos":85654600}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"55","timestamp":{"seconds":1727518981,"nanos":85763100}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"55","testStepResult":{"duration":{"seconds":0,"nanos":490900},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86254000}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"56","timestamp":{"seconds":1727518981,"nanos":86283700}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"56","testStepResult":{"duration":{"seconds":0,"nanos":101800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86385500}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"57","timestamp":{"seconds":1727518981,"nanos":86404200}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"57","testStepResult":{"duration":{"seconds":0,"nanos":270000},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86674200}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"58","timestamp":{"seconds":1727518981,"nanos":86695700}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"58","testStepResult":{"duration":{"seconds":0,"nanos":91100},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86786800}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"59","timestamp":{"seconds":1727518981,"nanos":86800600}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"59","testStepResult":{"duration":{"seconds":0,"nanos":173800},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":86974400}}} -{"testStepStarted":{"testCaseStartedId":"54","testStepId":"60","timestamp":{"seconds":1727518981,"nanos":86988900}}} -{"testStepFinished":{"testCaseStartedId":"54","testStepId":"60","testStepResult":{"duration":{"seconds":0,"nanos":225400},"message":"","status":"PASSED"},"timestamp":{"seconds":1727518981,"nanos":87214300}}} -{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":1727518981,"nanos":87242200},"willBeRetried":false}} -{"testRunFinished":{"success":true,"timestamp":{"seconds":1727518981,"nanos":96745400}}} +{"testRunStarted":{"timestamp":{"seconds":1731409033,"nanos":426773000},"id":"1"}} +{"meta":{"protocolVersion":"27.0.0","implementation":{"name":"Reqnroll","version":"2.1.2-local"},"runtime":{"name":"dotNet","version":"8.0.8"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.22631"},"cpu":{"name":"x64"}}} +{"stepDefinition":{"id":"2","pattern":{"source":"I have ${int} in my {word} account","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.background.Background","methodName":"AddMoneyToAccount","methodParameterTypes":["Int32","String"]}}}} +{"stepDefinition":{"id":"3","pattern":{"source":"the accounts are combined","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.background.Background","methodName":"CombineAccounts","methodParameterTypes":[]}}}} +{"stepDefinition":{"id":"4","pattern":{"source":"I have ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.background.Background","methodName":"CheckTotalBalance","methodParameterTypes":["Int32"]}}}} +{"stepDefinition":{"id":"5","pattern":{"source":"I transfer ${int} from {word} to {word}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.background.Background","methodName":"TransferMoney","methodParameterTypes":["Int32","String","String"]}}}} +{"stepDefinition":{"id":"6","pattern":{"source":"My {word} account has a balance of ${int}","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.CucumberMessages.CompatibilityTests.CCK.background.Background","methodName":"CheckAccountBalance","methodParameterTypes":["String","Int32"]}}}} +{"source":{"uri":"FeatureFile6a66613d86d549c894de0f1d2edd0d0d.feature","data":"Feature: background\r\n\r\nSteps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature\r\n\r\nBackground: \r\n# set up bank account balance\r\nGiven I have $500 in my checking account\r\nAnd I have $200 in my savings account\r\n\r\nScenario: Combined Balance\r\n\tWhen the accounts are combined\r\n\tThen I have $700\r\n\r\nScenario: Transfer Money\r\n\tWhen I transfer $150 from savings to checking\r\n\tThen My checking account has a balance of $650\r\n\tAnd My savings account has a balance of $50\r\n\r\nRule: A rule with a background\r\n\tBackground: First Transfer Money\r\n\t\tWhen I transfer $50 from savings to checking\r\n\t\tThen My savings account has a balance of $150\r\n\tScenario: total balance unchanged\r\n\t\tWhen the accounts are combined\r\n\t\tThen I have $700\r\n","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"uri":"FeatureFile6a66613d86d549c894de0f1d2edd0d0d.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"background","description":"Steps provided in a background section should properly result in Cucumber Message TestSteps in the scenarios of the feature","children":[{"background":{"location":{"line":5,"column":1},"keyword":"Background","name":"","description":"","steps":[{"location":{"line":7,"column":1},"keyword":"Given ","keywordType":"Context","text":"I have $500 in my checking account","id":"7"},{"location":{"line":8,"column":1},"keyword":"And ","keywordType":"Conjunction","text":"I have $200 in my savings account","id":"8"}],"id":"9"}},{"scenario":{"location":{"line":10,"column":1},"tags":[],"keyword":"Scenario","name":"Combined Balance","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"10"},{"location":{"line":12,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"11"}],"examples":[],"id":"12"}},{"scenario":{"location":{"line":14,"column":1},"tags":[],"keyword":"Scenario","name":"Transfer Money","description":"","steps":[{"location":{"line":15,"column":2},"keyword":"When ","keywordType":"Action","text":"I transfer $150 from savings to checking","id":"13"},{"location":{"line":16,"column":2},"keyword":"Then ","keywordType":"Outcome","text":"My checking account has a balance of $650","id":"14"},{"location":{"line":17,"column":2},"keyword":"And ","keywordType":"Conjunction","text":"My savings account has a balance of $50","id":"15"}],"examples":[],"id":"16"}},{"rule":{"location":{"line":19,"column":1},"tags":[],"keyword":"Rule","name":"A rule with a background","description":"","children":[{"background":{"location":{"line":20,"column":2},"keyword":"Background","name":"First Transfer Money","description":"","steps":[{"location":{"line":21,"column":3},"keyword":"When ","keywordType":"Action","text":"I transfer $50 from savings to checking","id":"17"},{"location":{"line":22,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"My savings account has a balance of $150","id":"18"}],"id":"19"}},{"scenario":{"location":{"line":23,"column":2},"tags":[],"keyword":"Scenario","name":"total balance unchanged","description":"","steps":[{"location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"the accounts are combined","id":"20"},{"location":{"line":25,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"I have $700","id":"21"}],"examples":[],"id":"22"}}],"id":"23"}}]},"comments":[{"location":{"line":6,"column":1},"text":"# set up bank account balance"}]}} +{"pickle":{"id":"28","uri":"FeatureFile6a66613d86d549c894de0f1d2edd0d0d.feature","name":"Combined Balance","language":"en-US","steps":[{"astNodeIds":["7"],"id":"24","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["8"],"id":"25","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["10"],"id":"26","type":"Action","text":"the accounts are combined"},{"astNodeIds":["11"],"id":"27","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["12"]}} +{"pickle":{"id":"34","uri":"FeatureFile6a66613d86d549c894de0f1d2edd0d0d.feature","name":"Transfer Money","language":"en-US","steps":[{"astNodeIds":["7"],"id":"29","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["8"],"id":"30","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["13"],"id":"31","type":"Action","text":"I transfer $150 from savings to checking"},{"astNodeIds":["14"],"id":"32","type":"Outcome","text":"My checking account has a balance of $650"},{"astNodeIds":["15"],"id":"33","type":"Outcome","text":"My savings account has a balance of $50"}],"tags":[],"astNodeIds":["16"]}} +{"pickle":{"id":"41","uri":"FeatureFile6a66613d86d549c894de0f1d2edd0d0d.feature","name":"total balance unchanged","language":"en-US","steps":[{"astNodeIds":["7"],"id":"35","type":"Context","text":"I have $500 in my checking account"},{"astNodeIds":["8"],"id":"36","type":"Context","text":"I have $200 in my savings account"},{"astNodeIds":["17"],"id":"37","type":"Action","text":"I transfer $50 from savings to checking"},{"astNodeIds":["18"],"id":"38","type":"Outcome","text":"My savings account has a balance of $150"},{"astNodeIds":["20"],"id":"39","type":"Action","text":"the accounts are combined"},{"astNodeIds":["21"],"id":"40","type":"Outcome","text":"I have $700"}],"tags":[],"astNodeIds":["22"]}} +{"testCase":{"id":"42","pickleId":"28","testSteps":[{"id":"44","pickleStepId":"24","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"45","pickleStepId":"25","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"46","pickleStepId":"26","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"47","pickleStepId":"27","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}],"testRunStartedId":"1"}} +{"testCaseStarted":{"attempt":0,"id":"43","testCaseId":"42","timestamp":{"seconds":1731409033,"nanos":456731600}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"44","timestamp":{"seconds":1731409033,"nanos":459671100}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"44","testStepResult":{"duration":{"seconds":0,"nanos":20445200},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":480116300}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"45","timestamp":{"seconds":1731409033,"nanos":482145900}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"45","testStepResult":{"duration":{"seconds":0,"nanos":528900},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":482674800}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"46","timestamp":{"seconds":1731409033,"nanos":482956900}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"46","testStepResult":{"duration":{"seconds":0,"nanos":1432700},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":484389600}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"47","timestamp":{"seconds":1731409033,"nanos":484652700}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"47","testStepResult":{"duration":{"seconds":0,"nanos":504200},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":485156900}}} +{"testCaseFinished":{"testCaseStartedId":"43","timestamp":{"seconds":1731409033,"nanos":487201700},"willBeRetried":false}} +{"testCase":{"id":"48","pickleId":"34","testSteps":[{"id":"50","pickleStepId":"29","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"51","pickleStepId":"30","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"52","pickleStepId":"31","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"150"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"53","pickleStepId":"32","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"},{"group":{"children":[],"value":"650"},"parameterTypeName":"int"}]}]},{"id":"54","pickleStepId":"33","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"50"},"parameterTypeName":"int"}]}]}],"testRunStartedId":"1"}} +{"testCaseStarted":{"attempt":0,"id":"49","testCaseId":"48","timestamp":{"seconds":1731409033,"nanos":500314000}}} +{"testStepStarted":{"testCaseStartedId":"49","testStepId":"50","timestamp":{"seconds":1731409033,"nanos":500362600}}} +{"testStepFinished":{"testCaseStartedId":"49","testStepId":"50","testStepResult":{"duration":{"seconds":0,"nanos":257600},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":500620200}}} +{"testStepStarted":{"testCaseStartedId":"49","testStepId":"51","timestamp":{"seconds":1731409033,"nanos":500659700}}} +{"testStepFinished":{"testCaseStartedId":"49","testStepId":"51","testStepResult":{"duration":{"seconds":0,"nanos":74800},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":500734500}}} +{"testStepStarted":{"testCaseStartedId":"49","testStepId":"52","timestamp":{"seconds":1731409033,"nanos":500757700}}} +{"testStepFinished":{"testCaseStartedId":"49","testStepId":"52","testStepResult":{"duration":{"seconds":0,"nanos":696600},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":501454300}}} +{"testStepStarted":{"testCaseStartedId":"49","testStepId":"53","timestamp":{"seconds":1731409033,"nanos":501481500}}} +{"testStepFinished":{"testCaseStartedId":"49","testStepId":"53","testStepResult":{"duration":{"seconds":0,"nanos":497400},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":501978900}}} +{"testStepStarted":{"testCaseStartedId":"49","testStepId":"54","timestamp":{"seconds":1731409033,"nanos":501997800}}} +{"testStepFinished":{"testCaseStartedId":"49","testStepId":"54","testStepResult":{"duration":{"seconds":0,"nanos":204100},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":502201900}}} +{"testCaseFinished":{"testCaseStartedId":"49","timestamp":{"seconds":1731409033,"nanos":502303900},"willBeRetried":false}} +{"testCase":{"id":"55","pickleId":"41","testSteps":[{"id":"57","pickleStepId":"35","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"500"},"parameterTypeName":"int"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"58","pickleStepId":"36","stepDefinitionIds":["2"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"200"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"}]}]},{"id":"59","pickleStepId":"37","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"50"},"parameterTypeName":"int"},{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"checking"},"parameterTypeName":"string"}]}]},{"id":"60","pickleStepId":"38","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"savings"},"parameterTypeName":"string"},{"group":{"children":[],"value":"150"},"parameterTypeName":"int"}]}]},{"id":"61","pickleStepId":"39","stepDefinitionIds":["3"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"62","pickleStepId":"40","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"value":"700"},"parameterTypeName":"int"}]}]}],"testRunStartedId":"1"}} +{"testCaseStarted":{"attempt":0,"id":"56","testCaseId":"55","timestamp":{"seconds":1731409033,"nanos":503783800}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"57","timestamp":{"seconds":1731409033,"nanos":503810900}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"57","testStepResult":{"duration":{"seconds":0,"nanos":126600},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":503937500}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"58","timestamp":{"seconds":1731409033,"nanos":503962100}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"58","testStepResult":{"duration":{"seconds":0,"nanos":88400},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":504050500}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"59","timestamp":{"seconds":1731409033,"nanos":504066600}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"59","testStepResult":{"duration":{"seconds":0,"nanos":226800},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":504293400}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"60","timestamp":{"seconds":1731409033,"nanos":504311800}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"60","testStepResult":{"duration":{"seconds":0,"nanos":117800},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":504429600}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"61","timestamp":{"seconds":1731409033,"nanos":504448600}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"61","testStepResult":{"duration":{"seconds":0,"nanos":189300},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":504637900}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"62","timestamp":{"seconds":1731409033,"nanos":504655100}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"62","testStepResult":{"duration":{"seconds":0,"nanos":195300},"message":"","status":"PASSED"},"timestamp":{"seconds":1731409033,"nanos":504850400}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":1731409033,"nanos":504878700},"willBeRetried":false}} +{"testRunFinished":{"success":true,"timestamp":{"seconds":1731409033,"nanos":512081500},"testRunStartedId":"1"}} \ No newline at end of file From db3219fd4b2b07347e097c87c7df271896295f8e Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:51:05 -0600 Subject: [PATCH 217/218] Add check for Enabled in Hook event handlers to bail early. This fixes problems with RuntimeTests which fire events but without a full event orchestration as expected by the Publisher. --- .../CucumberMessages/PubSub/CucumberMessagePublisher.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs index 450a4954c..9c0b03a44 100644 --- a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using Reqnroll.EnvironmentAccess; using System.Text.RegularExpressions; +using System.Collections.Generic; namespace Reqnroll.CucumberMessages.PubSub { @@ -351,6 +352,9 @@ private Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) private async Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) { + if (!Enabled) + return; + if (hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun || hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun) { string hookId = SharedIDGenerator.GetNewId(); @@ -374,6 +378,9 @@ private async Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBi private async Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) { + if (!Enabled) + return; + if (hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun || hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun) { var signature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingFinishedEvent.HookBinding); From 01972bf2c97748e74b36f762cfdf8207f153c94b Mon Sep 17 00:00:00 2001 From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:13:53 -0600 Subject: [PATCH 218/218] Modified Configuration's use of ITraceListener to defer resolution of the TraceListener until the moment its needed rather than as a constructor param. This avoids a conflict when the Test Frameworks provide their own implementation (which caused an 'oibject already resolved' error in the object container). Related changes in Tests to match. --- .../Configuration/CucumberConfiguration.cs | 30 ++++++++++--------- .../CucumberCompatibilityTestBase.cs | 6 +++- .../CucumberMessages.configuration.json | 4 +-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs index 47f6cae3f..01148d829 100644 --- a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -1,4 +1,5 @@ -using Reqnroll.CommonModels; +using Reqnroll.BoDi; +using Reqnroll.CommonModels; using Reqnroll.EnvironmentAccess; using Reqnroll.Tracing; using System; @@ -29,16 +30,17 @@ public class CucumberConfiguration : ICucumberConfiguration public string OutputFileName => _resolvedConfiguration.Value.OutputFileName; public IDGenerationStyle IDGenerationStyle => _resolvedConfiguration.Value.IDGenerationStyle; - - private ITraceListener _trace; + private readonly IObjectContainer _objectContainer; + private Lazy _traceListenerLazy; private IEnvironmentWrapper _environmentWrapper; - private Lazy _resolvedConfiguration; + private Lazy _resolvedConfiguration; private bool _enablementOverrideFlag = true; - public CucumberConfiguration(ITraceListener traceListener, IEnvironmentWrapper environmentWrapper) + public CucumberConfiguration(IObjectContainer objectContainer, IEnvironmentWrapper environmentWrapper) { - _trace = traceListener; + _objectContainer = objectContainer; + _traceListenerLazy = new Lazy(() => _objectContainer.Resolve()); _environmentWrapper = environmentWrapper; _resolvedConfiguration = new Lazy(ResolveConfiguration); Current = this; @@ -50,8 +52,8 @@ public void SetEnabled(bool value) _enablementOverrideFlag = value; } #endregion - - + + private ResolvedConfiguration ResolveConfiguration() { var config = ApplyHierarchicalConfiguration(); @@ -61,7 +63,7 @@ private ResolvedConfiguration ResolveConfiguration() if (string.IsNullOrEmpty(resolved.OutputFileName)) { resolved.OutputFileName = "reqnroll_report.ndjson"; - _trace!.WriteToolOutput($"WARNING: Cucumber Messages: Output filename was empty. Setting filename to {resolved.OutputFileName}"); + _traceListenerLazy.Value.WriteToolOutput($"WARNING: Cucumber Messages: Output filename was empty. Setting filename to {resolved.OutputFileName}"); } EnsureOutputDirectory(resolved); return resolved; @@ -82,8 +84,8 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) var relativePathValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_RELATIVE_PATH_ENVIRONMENT_VARIABLE); var fileNameValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILENAME_ENVIRONMENT_VARIABLE); var profileValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ACTIVE_OUTPUT_PROFILE_ENVIRONMENT_VARIABLE); - string profileName = profileValue is Success ? - ((Success)profileValue).Result : + string profileName = profileValue is Success ? + ((Success)profileValue).Result : !string.IsNullOrEmpty(config.ActiveProfileName) ? config.ActiveProfileName : "DEFAULT"; var idGenStyleValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ID_GENERATION_STYLE_ENVIRONMENT_VARIABLE); @@ -117,7 +119,7 @@ private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); result.Enabled = enabledResult is Success ? Convert.ToBoolean(((Success)enabledResult).Result) : result.Enabled; - + return result; } @@ -129,10 +131,10 @@ private ConfigurationDTO AddConfig(ConfigurationDTO rootConfig, ConfigurationDTO { AddOrOverrideProfile(rootConfig.Profiles, overridingProfile); } - if (!String.IsNullOrEmpty(overridingConfig.ActiveProfileName) && !rootConfig.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) + if (!String.IsNullOrEmpty(overridingConfig.ActiveProfileName) && !rootConfig.Profiles.Any(p => p.ProfileName == overridingConfig.ActiveProfileName)) { // The incoming configuration DTO points to a profile that doesn't exist. - _trace.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {rootConfig.ActiveProfileName} instead."); + _traceListenerLazy.Value.WriteToolOutput($"WARNING: Configuration file specifies an active profile that doesn't exist: {overridingConfig.ActiveProfileName}. Using {rootConfig.ActiveProfileName} instead."); } else if (!String.IsNullOrEmpty(overridingConfig.ActiveProfileName)) rootConfig.ActiveProfileName = overridingConfig.ActiveProfileName; diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs index ee84d0bc5..b30b7c3f1 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberCompatibilityTestBase.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Io.Cucumber.Messages.Types; using Moq; +using Reqnroll.BoDi; using Reqnroll.CucumberMessages.Configuration; using Reqnroll.CucumberMessages.PayloadProcessing; using Reqnroll.EnvironmentAccess; @@ -9,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.WebSockets; using System.Reflection; using System.Text; using System.Text.Json; @@ -121,9 +123,11 @@ protected static string ActualsResultLocationDirectory() //var config = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(configFileLocation), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var objectContainerMock = new Mock(); var tracerMock = new Mock(); + objectContainerMock.Setup(x => x.Resolve()).Returns(tracerMock.Object); var env = new EnvironmentWrapper(); - CucumberConfiguration configuration = new CucumberConfiguration(tracerMock.Object, env); + CucumberConfiguration configuration = new CucumberConfiguration(objectContainerMock.Object, env); var resultLocation = Path.Combine(configuration.BaseDirectory, configuration.OutputDirectory); return resultLocation; } diff --git a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json index 615a09738..aff2d3339 100644 --- a/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json +++ b/Tests/CucumberMessages.CompatibilityTests/CucumberMessages.configuration.json @@ -1,9 +1,9 @@ { "fileOutputEnabled": true, - "activeProfileName": "LOCAL", + "activeProfileName": "DEFAULT", "profiles": [ { - "profileName": "LOCAL", + "profileName": "DEFAULT", "basePath": "C:\\Users\\clrud\\source\\repos\\scratch", "outputDirectory": "CucumberMessages", "IDGenerationStyle": "INCREMENTING"