From 44fd54a0f4a78f6791ce616d1d3bcbb759f4bbbe Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Tue, 4 Nov 2025 17:52:40 -0600
Subject: [PATCH 01/19] Initial commit. This adds support for tag exprssions on
Scopes assigned to bindings.
---
Reqnroll.Parser/Reqnroll.Parser.csproj | 4 ++
Reqnroll/Bindings/BindingScope.cs | 21 ++++-------
.../Discovery/BindingSourceProcessor.cs | 9 +++--
.../Discovery/ReqnrollTagExpressionParser.cs | 36 ++++++++++++++++++
.../RuntimeBindingSourceProcessor.cs | 3 +-
.../Provider/BindingProviderService.cs | 4 +-
.../Cucumber/CucumberMessageFactory.cs | 2 +-
.../DefaultDependencyProvider.cs | 4 ++
Reqnroll/Reqnroll.csproj | 6 +++
Reqnroll/Reqnroll.nuspec | 1 +
.../CucumberMessagesValidator.cs | 18 ++++-----
.../BindingSourceProcessorStub.cs | 3 +-
.../Bindings/BindingFactoryTests.cs | 5 ++-
.../CucumberExpressionIntegrationTests.cs | 14 ++++---
.../StepDefinitionMatchServiceTest.cs | 37 ++++++++++++++++---
.../Reqnroll.RuntimeTests.csproj | 6 +++
16 files changed, 128 insertions(+), 45 deletions(-)
create mode 100644 Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
diff --git a/Reqnroll.Parser/Reqnroll.Parser.csproj b/Reqnroll.Parser/Reqnroll.Parser.csproj
index 83caa23b9..207d5ca07 100644
--- a/Reqnroll.Parser/Reqnroll.Parser.csproj
+++ b/Reqnroll.Parser/Reqnroll.Parser.csproj
@@ -18,4 +18,8 @@
+
+
+
+
diff --git a/Reqnroll/Bindings/BindingScope.cs b/Reqnroll/Bindings/BindingScope.cs
index 6ba2c12e8..9d029a57f 100644
--- a/Reqnroll/Bindings/BindingScope.cs
+++ b/Reqnroll/Bindings/BindingScope.cs
@@ -1,38 +1,31 @@
using System;
using System.Linq;
+using Cucumber.TagExpressions;
namespace Reqnroll.Bindings
{
public class BindingScope
{
- public string Tag { get; private set; }
+ public string Tag { get => _tagExpression.ToString(); }
public string FeatureTitle { get; private set; }
public string ScenarioTitle { get; private set; }
-
- public BindingScope(string tag, string featureTitle, string scenarioTitle)
+ private readonly ITagExpression _tagExpression;
+ public BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
{
- Tag = RemoveLeadingAt(tag);
+ _tagExpression = tagExpression;
FeatureTitle = featureTitle;
ScenarioTitle = scenarioTitle;
}
- private string RemoveLeadingAt(string tag)
- {
- if (tag == null || !tag.StartsWith("@"))
- return tag;
-
- return tag.Substring(1); // remove leading "@"
- }
-
public bool Match(StepContext stepContext, out int scopeMatches)
{
scopeMatches = 0;
- var tags = stepContext.Tags;
+ var tags = stepContext.Tags.Select(t => "@" + t).ToList();
if (Tag != null)
{
- if (!tags.Contains(Tag))
+ if (!_tagExpression.Evaluate(tags))
return false;
scopeMatches++;
diff --git a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
index 79dd1b770..4c4a53816 100644
--- a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
+++ b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
@@ -3,20 +3,23 @@
using System.Linq;
using Reqnroll.Bindings.Reflection;
using Reqnroll.PlatformCompatibility;
+using Cucumber.TagExpressions;
namespace Reqnroll.Bindings.Discovery
{
public abstract class BindingSourceProcessor : IBindingSourceProcessor
{
private readonly IBindingFactory _bindingFactory;
+ private readonly ITagExpressionParser _tagExpressionParser;
private BindingSourceType _currentBindingSourceType = null;
private BindingScope[] _typeScopes = null;
private readonly List _stepDefinitionBindingBuilders = new();
- protected BindingSourceProcessor(IBindingFactory bindingFactory)
+ protected BindingSourceProcessor(IBindingFactory bindingFactory, ITagExpressionParser tagExpressionParser)
{
_bindingFactory = bindingFactory;
+ _tagExpressionParser = tagExpressionParser;
}
public bool CanProcessTypeAttribute(string attributeTypeName)
@@ -75,7 +78,7 @@ public virtual void BuildingCompleted()
private IEnumerable GetScopes(IEnumerable attributes)
{
return attributes.Where(attr => attr.AttributeType.TypeEquals(typeof(ScopeAttribute)))
- .Select(attr => new BindingScope(attr.TryGetAttributeValue("Tag"), attr.TryGetAttributeValue("Feature"), attr.TryGetAttributeValue("Scenario")));
+ .Select(attr => new BindingScope(_tagExpressionParser.Parse(attr.TryGetAttributeValue("Tag")), attr.TryGetAttributeValue("Feature"), attr.TryGetAttributeValue("Scenario")));
}
private bool IsBindingType(BindingSourceType bindingSourceType)
@@ -156,7 +159,7 @@ private void ProcessHookAttribute(BindingSourceMethod bindingSourceMethod, Bindi
string[] tags = GetTagsDefinedOnBindingAttribute(hookAttribute);
if (tags != null)
- scopes = scopes.Concat(tags.Select(t => new BindingScope(t, null, null)));
+ scopes = scopes.Concat(tags.Select(t => new BindingScope(_tagExpressionParser.Parse(t), null, null)));
ApplyForScope(scopes.ToArray(), scope => ProcessHookAttribute(bindingSourceMethod, hookAttribute, scope));
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
new file mode 100644
index 000000000..95da8a2a8
--- /dev/null
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
@@ -0,0 +1,36 @@
+using Cucumber.TagExpressions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Reqnroll.Bindings.Discovery
+{
+ public class ReqnrollTagExpressionParser(ITagExpressionParser tagExpressionParser) : ITagExpressionParser
+ {
+ public ITagExpression Parse(string tagExpression)
+ {
+ return Rewrite(tagExpressionParser.Parse(tagExpression));
+ }
+
+ private ITagExpression Rewrite(ITagExpression expression)
+ {
+ return expression switch
+ {
+ NullExpression nullExpression => nullExpression,
+ NotNode notNode => new NotNode(Rewrite(notNode.Operand)),
+ BinaryOpNode binaryOpNode => new BinaryOpNode(binaryOpNode.Op, Rewrite(binaryOpNode.Left), Rewrite(binaryOpNode.Right)),
+ LiteralNode literalNode => new LiteralNode(PrefixLiteral(literalNode.Name)),
+ _ => throw new NotSupportedException($"Unsupported tag expression type: {expression.GetType().FullName}"),
+ };
+ }
+
+ private string PrefixLiteral(string name)
+ {
+ if (name.IsNullOrEmpty() )
+ return name;
+ if (name.StartsWith("@"))
+ return name;
+ return "@" + name;
+ }
+ }
+}
diff --git a/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs
index 3e7372153..82f93afc4 100644
--- a/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs
+++ b/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs
@@ -1,4 +1,5 @@
using Reqnroll.Tracing;
+using Cucumber.TagExpressions;
namespace Reqnroll.Bindings.Discovery
{
@@ -12,7 +13,7 @@ public class RuntimeBindingSourceProcessor : BindingSourceProcessor, IRuntimeBin
private readonly IBindingRegistry _bindingRegistry;
private readonly ITestTracer _testTracer;
- public RuntimeBindingSourceProcessor(IBindingFactory bindingFactory, IBindingRegistry bindingRegistry, ITestTracer testTracer) : base(bindingFactory)
+ public RuntimeBindingSourceProcessor(IBindingFactory bindingFactory, IBindingRegistry bindingRegistry, ITestTracer testTracer, ITagExpressionParser tagExpressionParser) : base(bindingFactory, tagExpressionParser)
{
_bindingRegistry = bindingRegistry;
_testTracer = testTracer;
diff --git a/Reqnroll/Bindings/Provider/BindingProviderService.cs b/Reqnroll/Bindings/Provider/BindingProviderService.cs
index 520d5cfad..c9f40b9a2 100644
--- a/Reqnroll/Bindings/Provider/BindingProviderService.cs
+++ b/Reqnroll/Bindings/Provider/BindingProviderService.cs
@@ -1,4 +1,5 @@
-using Reqnroll.Bindings.Discovery;
+using Cucumber.TagExpressions;
+using Reqnroll.Bindings.Discovery;
using Reqnroll.Bindings.Provider.Data;
using Reqnroll.Bindings.Reflection;
using Reqnroll.BoDi;
@@ -188,6 +189,7 @@ public override void RegisterGlobalContainerDefaults(ObjectContainer container)
base.RegisterGlobalContainerDefaults(container);
container.RegisterTypeAs();
container.RegisterTypeAs();
+ container.RegisterFactoryAs(() => new ReqnrollTagExpressionParser(new TagExpressionParser()));
}
}
diff --git a/Reqnroll/Formatters/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/Formatters/PayloadProcessing/Cucumber/CucumberMessageFactory.cs
index a27af0cf2..2254a14b9 100644
--- a/Reqnroll/Formatters/PayloadProcessing/Cucumber/CucumberMessageFactory.cs
+++ b/Reqnroll/Formatters/PayloadProcessing/Cucumber/CucumberMessageFactory.cs
@@ -230,7 +230,7 @@ public virtual 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;
diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
index 4724766ad..3ebf9bbcb 100644
--- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
+++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
@@ -1,3 +1,4 @@
+using Cucumber.TagExpressions;
using Gherkin.CucumberMessages;
using Reqnroll.Analytics;
using Reqnroll.Analytics.AppInsights;
@@ -28,6 +29,7 @@
using Reqnroll.Tracing;
using Reqnroll.Utils;
using System;
+using System.ComponentModel;
namespace Reqnroll.Infrastructure
{
@@ -134,6 +136,8 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container)
container.RegisterTypeAs();
container.RegisterFactoryAs(() => container.Resolve());
container.RegisterTypeAs();
+
+ var _ = container.RegisterFactoryAs(() => new ReqnrollTagExpressionParser(new TagExpressionParser())).InstancePerDependency;
}
public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer)
diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj
index 7699ff08c..d1de21fb0 100644
--- a/Reqnroll/Reqnroll.csproj
+++ b/Reqnroll/Reqnroll.csproj
@@ -52,6 +52,12 @@
+
+
+ ..\..\cucumber\tag-expressions\dotnet\TagExpressions\bin\Debug\netstandard2.0\Cucumber.TagExpressions.dll
+
+
+
diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec
index 5fdacb80e..9b397e42b 100644
--- a/Reqnroll/Reqnroll.nuspec
+++ b/Reqnroll/Reqnroll.nuspec
@@ -37,6 +37,7 @@
+
diff --git a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
index 85a30a47a..09de4c01a 100644
--- a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
+++ b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
@@ -547,17 +547,15 @@ private EquivalencyAssertionOptions ArrangeFluentAssertionOptions(Equivale
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)
+ // Difficult to compare individual Hook messages (Ids aren't comparable, the Source references aren't compatible,
// and After Hook execution ordering is different between Reqnroll and CCK.
- /*
- 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);
- }
- */
+ foreach (var expectedItem in expectedList)
+ {
+ actualList.Should().Contain(actualItem =>
+ actualItem.TagExpression == expectedItem.TagExpression
+ && actualItem.Type == expectedItem.Type,
+ "actual collection should contain an item equivalent to {0}", expectedItem);
+ }
}
})
.WhenTypeIs>()
diff --git a/Tests/Reqnroll.RuntimeTests/BindingSourceProcessorStub.cs b/Tests/Reqnroll.RuntimeTests/BindingSourceProcessorStub.cs
index d16a93041..c7beb6e16 100644
--- a/Tests/Reqnroll.RuntimeTests/BindingSourceProcessorStub.cs
+++ b/Tests/Reqnroll.RuntimeTests/BindingSourceProcessorStub.cs
@@ -4,6 +4,7 @@
using Reqnroll.Bindings.CucumberExpressions;
using Reqnroll.Bindings.Discovery;
using Reqnroll.Configuration;
+using Cucumber.TagExpressions;
namespace Reqnroll.RuntimeTests
{
@@ -17,7 +18,7 @@ public class BindingSourceProcessorStub : BindingSourceProcessor, IRuntimeBindin
public IEnumerable ValidationErrors => GeneralErrorMessages.Concat(BindingSpecificErrorMessages);
- public BindingSourceProcessorStub() : base(new BindingFactory(new StepDefinitionRegexCalculator(ConfigurationLoader.GetDefault()), new CucumberExpressionStepDefinitionBindingBuilderFactory(new CucumberExpressionParameterTypeRegistry(new BindingRegistry())), new CucumberExpressionDetector()))
+ public BindingSourceProcessorStub() : base(new BindingFactory(new StepDefinitionRegexCalculator(ConfigurationLoader.GetDefault()), new CucumberExpressionStepDefinitionBindingBuilderFactory(new CucumberExpressionParameterTypeRegistry(new BindingRegistry())), new CucumberExpressionDetector()), new ReqnrollTagExpressionParser(new TagExpressionParser()))
{
}
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs
index a5eba674b..d9698ffc7 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs
@@ -7,6 +7,7 @@
using Reqnroll.Bindings.Reflection;
using System.Linq;
using Xunit;
+using Reqnroll.Bindings.Discovery;
namespace Reqnroll.RuntimeTests.Bindings;
@@ -23,10 +24,10 @@ public void CreateStepDefinitionBindingBuilderTest(ExpressionType expressionType
var cucumberExpressionStepDefinitionBindingBuilderFactory = new CucumberExpressionStepDefinitionBindingBuilderFactory(new CucumberExpressionParameterTypeRegistry(Mock.Of()));
var cucumberExpressionDetector = new CucumberExpressionDetector();
var sut = new BindingFactory(stepDefinitionRegexCalculator.Object, cucumberExpressionStepDefinitionBindingBuilderFactory, cucumberExpressionDetector);
-
+ var tagExpressionParser = new ReqnrollTagExpressionParser(new Cucumber.TagExpressions.TagExpressionParser());
var stepDefinitionType = StepDefinitionType.Given;
var bindingMethod = new Mock().Object;
- var bindingScope = new BindingScope("tag1", "feature1", "scenario1");
+ var bindingScope = new BindingScope(tagExpressionParser.Parse("tag1"), "feature1", "scenario1");
var expressionString = "The product {string} has the price {int}$";
// Act
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs
index 4b77d1970..bc03b3204 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs
@@ -1,16 +1,17 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using Reqnroll.BoDi;
+using Cucumber.TagExpressions;
using FluentAssertions;
using Moq;
using Reqnroll.Bindings;
using Reqnroll.Bindings.Discovery;
using Reqnroll.Bindings.Reflection;
+using Reqnroll.BoDi;
using Reqnroll.Infrastructure;
using Reqnroll.UnitTestProvider;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
using Xunit;
namespace Reqnroll.RuntimeTests.Bindings.CucumberExpressions;
@@ -152,6 +153,7 @@ public override void RegisterGlobalContainerDefaults(ObjectContainer container)
base.RegisterGlobalContainerDefaults(container);
var stubUintTestProvider = new Mock();
container.RegisterInstanceAs(stubUintTestProvider.Object, "nunit");
+ container.RegisterFactoryAs(() => new ReqnrollTagExpressionParser(new TagExpressionParser()));
}
}
diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs
index 2f0986df8..5d9945808 100644
--- a/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs
+++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs
@@ -7,6 +7,8 @@
using Reqnroll.Infrastructure;
using FluentAssertions;
using Reqnroll.RuntimeTests.ErrorHandling;
+using Reqnroll.Bindings.Discovery;
+using System;
namespace Reqnroll.RuntimeTests.Infrastructure
{
@@ -53,13 +55,20 @@ private static BindingMethod CreateBindingMethodWithObjectParam(string name = "d
return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, new IBindingParameter[] { new BindingParameter(new RuntimeBindingType(typeof(object)), "param1") }, null);
}
- private StepInstance CreateSimpleWhen(string text = "I do something")
+ private StepInstance CreateSimpleWhen(string text = "I do something", IEnumerable tags = null)
{
+ tags = tags ?? new string[0];
var result = new StepInstance(StepDefinitionType.When, StepDefinitionKeyword.When, "When ", text, null, null,
- new StepContext("MyFeature", "MyScenario", new string[0], new CultureInfo("en-US", false)));
+ new StepContext("MyFeature", "MyScenario", tags, new CultureInfo("en-US", false)));
return result;
}
+ private BindingScope CreateBindingScope(string tag, string featureTitle, string scenarioTitle)
+ {
+ var tagExpressionParser = new ReqnrollTagExpressionParser(new Cucumber.TagExpressions.TagExpressionParser());
+ return new BindingScope(tagExpressionParser.Parse(tag), featureTitle, scenarioTitle);
+ }
+
[Fact]
public void Should_GetBestMatch_succeed_when_proper_match()
{
@@ -76,7 +85,7 @@ public void Should_GetBestMatch_succeed_when_proper_match()
public void Should_GetBestMatch_succeed_when_proper_match_and_non_matching_scopes()
{
whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m1")));
- whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m2"), new BindingScope("non-matching-tag", null, null)));
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m2"), CreateBindingScope("non-matching-tag", null, null)));
var sut = CreateSUT();
@@ -85,6 +94,22 @@ public void Should_GetBestMatch_succeed_when_proper_match_and_non_matching_scope
result.Success.Should().BeTrue();
}
+ [Fact]
+ public void Should_GetBestMatch_succeed_when_proper_match_and_matching_tag_expression()
+ {
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m1")));
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("m2"), CreateBindingScope("@alpha and @beta", null, null)));
+
+ var sut = CreateSUT();
+
+ var result = sut.GetBestMatch(CreateSimpleWhen("I do something", new string[] { "alpha", "beta" }), bindingCulture, out _, out _);
+
+ result.Success.Should().BeTrue();
+ result.StepBinding.Method.Name.Should().Be("m2");
+ }
+
+
+
[Fact]
public void Should_GetBestMatch_succeed_when_proper_match_with_parameters()
{
@@ -139,7 +164,7 @@ public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters
[Fact]
public void Should_GetBestMatch_fail_when_scope_errors_with_single_match()
{
- whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod(), new BindingScope("non-matching-tag", null, null)));
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod(), CreateBindingScope("non-matching-tag", null, null)));
var sut = CreateSUT();
@@ -152,8 +177,8 @@ public void Should_GetBestMatch_fail_when_scope_errors_with_single_match()
[Fact]
public void Should_GetBestMatch_fail_when_scope_errors_with_multiple_matches()
{
- whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"), new BindingScope("non-matching-tag", null, null)));
- whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"), new BindingScope("other-non-matching-tag", null, null)));
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"), CreateBindingScope("non-matching-tag", null, null)));
+ whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"), CreateBindingScope("other-non-matching-tag", null, null)));
var sut = CreateSUT();
diff --git a/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj b/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
index fab9101ac..29491b16c 100644
--- a/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
+++ b/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
@@ -37,6 +37,12 @@
+
+
+ ..\..\..\cucumber\tag-expressions\dotnet\TagExpressions\bin\Debug\netstandard2.0\Cucumber.TagExpressions.dll
+
+
+
From 416be89d1134a5d2e4e216b0cedd4a177440d56f Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:31:40 -0600
Subject: [PATCH 02/19] Fix an issue in which the BindingProviderService (as
invoked OOP and via reflection by the VisualStudio Extension for Reqnroll)
would provide a hardcoded prefix "@" to all Scope Tag values. This is no
longer necessary with Tag Expression support.
---
Reqnroll/Bindings/Provider/BindingProviderService.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/Reqnroll/Bindings/Provider/BindingProviderService.cs b/Reqnroll/Bindings/Provider/BindingProviderService.cs
index c9f40b9a2..4b5d8bef0 100644
--- a/Reqnroll/Bindings/Provider/BindingProviderService.cs
+++ b/Reqnroll/Bindings/Provider/BindingProviderService.cs
@@ -139,9 +139,7 @@ BindingScopeData GetScope(IScopedBinding scopedBinding)
return new BindingScopeData
{
- Tag = scopedBinding.BindingScope.Tag == null
- ? null
- : "@" + scopedBinding.BindingScope.Tag,
+ Tag = scopedBinding.BindingScope.Tag,
FeatureTitle = scopedBinding.BindingScope.FeatureTitle,
ScenarioTitle = scopedBinding.BindingScope.ScenarioTitle
};
@@ -189,7 +187,7 @@ public override void RegisterGlobalContainerDefaults(ObjectContainer container)
base.RegisterGlobalContainerDefaults(container);
container.RegisterTypeAs();
container.RegisterTypeAs();
- container.RegisterFactoryAs(() => new ReqnrollTagExpressionParser(new TagExpressionParser()));
+ var _ = container.RegisterFactoryAs(() => new ReqnrollTagExpressionParser(new TagExpressionParser())).InstancePerDependency;
}
}
From 20b0f081669e390b5757b766fac27645f6ec3891 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Sun, 9 Nov 2025 10:05:18 -0600
Subject: [PATCH 03/19] Draft update to the documentation on the topic of tag
expressions
---
docs/automation/scoped-bindings.md | 28 +++++++++++++++++++++++++---
1 file changed, 25 insertions(+), 3 deletions(-)
diff --git a/docs/automation/scoped-bindings.md b/docs/automation/scoped-bindings.md
index 8401c1025..613c4b8ec 100644
--- a/docs/automation/scoped-bindings.md
+++ b/docs/automation/scoped-bindings.md
@@ -6,7 +6,7 @@ In some cases however, it is necessary to restrict when step definitions or hook
You can restrict the execution of scoped bindings by:
-- tag
+- tag expression
- feature (using the feature title)
- scenario (using the scenario title)
@@ -28,6 +28,23 @@ Use the `[Scope]` attribute to define the scope:
[Scope(Tag = "mytag", Feature = "feature title", Scenario = "scenario title")]
```
+## Tag expressions
+A tag expression is an infix boolean expression. Below are some examples:
+
+|Expression|Description|
+|----------|-----------|
+|@fast| Scenarios tagged with @fast|
+|@wip and not @slow| Scenarios tagged with @wip that aren't also tagged with @slow|
+|@smoke and @fast| Scenarios tagged with both @smoke and @fast|
+|@gui or @database| Scenarios tagged with either @gui or @database|
+
+```{note}
+The '@' prefix is optional within a tag expression.
+```
+For even more advanced tag expressions you can use parenthesis for clarity, or to change operator precedence:
+
+ (@smoke or @ui) and (not @slow)
+
## Scoping Rules
Scope can be defined at the method or class level.
@@ -47,8 +64,7 @@ The following example combines the tag scopes with "OR":
```{code-block} csharp
:caption: Step Definition File
-[Scope(Tag = "thisTag")] [Scope(Tag = "OrThisTag")]
-[Scope(Tag = "thisTag"), Scope(Tag = "OrThisTag")]
+[Scope(Tag = "thisTag or thatTag", Feature = "thisFeature")] [Scope(Tag = "OrThisTag", Feature="thatFeature")]
```
````{note}
@@ -103,6 +119,7 @@ public void PerformSimpleSearch(string title)
}
```
+
## Scoping Tips & Tricks
The following example shows a way to "ignore" executing the scenarios marked with `@manual`. However Reqnroll's tracing will still display the steps, so you can work through the manual scenarios by following the steps in the report.
@@ -129,6 +146,11 @@ public class ManualSteps
}
```
+```{note}
+Can this next section be deleted completely, given that tag expressions can now perform what the example attempts to show?
+If we wish to retain this section, we need a more compelling example.
+```
+
## Beyond Scope
You can define more complex filters using the [`ScenarioContext`](scenario-context.md) class. The following example starts selenium if the scenario is tagged with `@web` _and_ `@automated`.
From 71e6fcc34de716659afa64f9b99e830d22a7f7cc Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Mon, 10 Nov 2025 11:19:09 -0600
Subject: [PATCH 04/19] File scope namespace for the
ReqnrollTagExpressionParser.
---
.../Discovery/ReqnrollTagExpressionParser.cs | 47 +++++++++----------
1 file changed, 23 insertions(+), 24 deletions(-)
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
index 95da8a2a8..0e09b2438 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
@@ -3,34 +3,33 @@
using System.Collections.Generic;
using System.Text;
-namespace Reqnroll.Bindings.Discovery
+namespace Reqnroll.Bindings.Discovery;
+
+public class ReqnrollTagExpressionParser(ITagExpressionParser tagExpressionParser) : ITagExpressionParser
{
- public class ReqnrollTagExpressionParser(ITagExpressionParser tagExpressionParser) : ITagExpressionParser
+ public ITagExpression Parse(string tagExpression)
{
- public ITagExpression Parse(string tagExpression)
- {
- return Rewrite(tagExpressionParser.Parse(tagExpression));
- }
+ return Rewrite(tagExpressionParser.Parse(tagExpression));
+ }
- private ITagExpression Rewrite(ITagExpression expression)
+ private ITagExpression Rewrite(ITagExpression expression)
+ {
+ return expression switch
{
- return expression switch
- {
- NullExpression nullExpression => nullExpression,
- NotNode notNode => new NotNode(Rewrite(notNode.Operand)),
- BinaryOpNode binaryOpNode => new BinaryOpNode(binaryOpNode.Op, Rewrite(binaryOpNode.Left), Rewrite(binaryOpNode.Right)),
- LiteralNode literalNode => new LiteralNode(PrefixLiteral(literalNode.Name)),
- _ => throw new NotSupportedException($"Unsupported tag expression type: {expression.GetType().FullName}"),
- };
- }
+ NullExpression nullExpression => nullExpression,
+ NotNode notNode => new NotNode(Rewrite(notNode.Operand)),
+ BinaryOpNode binaryOpNode => new BinaryOpNode(binaryOpNode.Op, Rewrite(binaryOpNode.Left), Rewrite(binaryOpNode.Right)),
+ LiteralNode literalNode => new LiteralNode(PrefixLiteral(literalNode.Name)),
+ _ => throw new NotSupportedException($"Unsupported tag expression type: {expression.GetType().FullName}"),
+ };
+ }
- private string PrefixLiteral(string name)
- {
- if (name.IsNullOrEmpty() )
- return name;
- if (name.StartsWith("@"))
- return name;
- return "@" + name;
- }
+ private string PrefixLiteral(string name)
+ {
+ if (name.IsNullOrEmpty() )
+ return name;
+ if (name.StartsWith("@"))
+ return name;
+ return "@" + name;
}
}
From 7033a85dafe63635124c6985c94837b5bc579686 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 10:05:45 -0600
Subject: [PATCH 05/19] Added nuget package reference to
Cucumber.TagExpressions; dropping the direct assembly reference.
---
Reqnroll/Reqnroll.csproj | 17 ++++-------------
.../Reqnroll.RuntimeTests.csproj | 6 ------
2 files changed, 4 insertions(+), 19 deletions(-)
diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj
index 65bfba3c5..4127590c7 100644
--- a/Reqnroll/Reqnroll.csproj
+++ b/Reqnroll/Reqnroll.csproj
@@ -18,6 +18,7 @@
+
@@ -38,9 +39,7 @@
-
+
@@ -69,11 +68,7 @@
-
+
@@ -81,11 +76,7 @@
-
+
diff --git a/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj b/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
index 29491b16c..fab9101ac 100644
--- a/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
+++ b/Tests/Reqnroll.RuntimeTests/Reqnroll.RuntimeTests.csproj
@@ -37,12 +37,6 @@
-
-
- ..\..\..\cucumber\tag-expressions\dotnet\TagExpressions\bin\Debug\netstandard2.0\Cucumber.TagExpressions.dll
-
-
-
From d6cbd0a24ea05942b8dbbfce12b72978b81133e4 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 15:39:19 -0600
Subject: [PATCH 06/19] Update CHANGELOG.md
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cfba26df..ce9e34c9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* Formatters: configured OutputFilePath may now contain variable substitution parameters for build metadata, timestamp, and environment variables. (#930)
* Improved packaging of Reqnroll NuGet packages (#914)
+* Tag Expressions: Step Definition Scopes and Hook Bindings may now use tag expressions (such as [Scope("@db and not @slow)]) (#911)
## Bug fixes:
From 259e77bfc886b308d8f8af061924c20442f3857c Mon Sep 17 00:00:00 2001
From: Julian Verdurmen <5808377+304NotModified@users.noreply.github.com>
Date: Mon, 15 Dec 2025 21:59:51 +0100
Subject: [PATCH 07/19] Update CHANGELOG.md
---
CHANGELOG.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0c1898e5..c13d599ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,6 @@
* Support for linked feature files (files used from outside of the project folder). To use this feature, the `ReqnrollUseIntermediateOutputPathForCodeBehind` flag must be enabled (see above). (#948)
* Updated TUnit integration to support TUnit v1.3.25 and .NET 10 SDK compatibility (#918)
-
## Bug fixes:
* Fix: Error during build "System.TypeLoadException: Method 'DisposeAsync' in type 'System.Text.Json.Utf8JsonWriter" (partial fix for some occurrences) (#921, #914)
From 8e445108c0179e498aba85f108e58ff528254da8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?=
Date: Wed, 17 Dec 2025 12:12:31 +0100
Subject: [PATCH 08/19] code cleanup
---
CHANGELOG.md | 2 +-
Reqnroll.Parser/Reqnroll.Parser.csproj | 4 -
Reqnroll/Bindings/BindingScope.cs | 25 +-
.../Discovery/ReqnrollTagExpressionParser.cs | 2 -
.../Provider/BindingProviderService.cs | 1 -
.../DefaultDependencyProvider.cs | 2 -
Reqnroll/Reqnroll.csproj | 16 +-
.../CucumberMessagesValidator.cs | 32 +-
.../CucumberExpressionIntegrationTests.cs | 14 +-
.../StepDefinitionMatchServiceTest.cs | 350 +++++++++---------
docs/automation/scoped-bindings.md | 5 +-
11 files changed, 223 insertions(+), 230 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c13d599ba..6284c68b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
* Formatters: configured OutputFilePath may now contain variable substitution parameters for build metadata, timestamp, and environment variables. (#930)
* Improved packaging of Reqnroll NuGet packages (#914)
-* Tag Expressions: Step Definition Scopes and Hook Bindings may now use tag expressions (such as [Scope("@db and not @slow)]) (#911)
+* Tag Expressions: step definition scopes and hooks may now use tag expressions (such as `@db and not @slow`) (#911)
* Improved up-to-date checking for feature files that results in faster builds. As part of this the code-behind files are deleted on clean or rebuild. (#941)
* Support for storing the code-behind files in the intermediate output folder (obj folder) by setting the `ReqnrollUseIntermediateOutputPathForCodeBehind` MSBuild property to `true`. (#947)
* Support for linked feature files (files used from outside of the project folder). To use this feature, the `ReqnrollUseIntermediateOutputPathForCodeBehind` flag must be enabled (see above). (#948)
diff --git a/Reqnroll.Parser/Reqnroll.Parser.csproj b/Reqnroll.Parser/Reqnroll.Parser.csproj
index 207d5ca07..83caa23b9 100644
--- a/Reqnroll.Parser/Reqnroll.Parser.csproj
+++ b/Reqnroll.Parser/Reqnroll.Parser.csproj
@@ -18,8 +18,4 @@
-
-
-
-
diff --git a/Reqnroll/Bindings/BindingScope.cs b/Reqnroll/Bindings/BindingScope.cs
index 9d029a57f..11e62ac22 100644
--- a/Reqnroll/Bindings/BindingScope.cs
+++ b/Reqnroll/Bindings/BindingScope.cs
@@ -4,28 +4,23 @@
namespace Reqnroll.Bindings
{
- public class BindingScope
+ public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
{
- public string Tag { get => _tagExpression.ToString(); }
- public string FeatureTitle { get; private set; }
- public string ScenarioTitle { get; private set; }
- private readonly ITagExpression _tagExpression;
- public BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
- {
- _tagExpression = tagExpression;
- FeatureTitle = featureTitle;
- ScenarioTitle = scenarioTitle;
- }
+ public string Tag => tagExpression.ToString();
+
+ public string FeatureTitle { get; } = featureTitle;
+
+ public string ScenarioTitle { get; } = scenarioTitle;
public bool Match(StepContext stepContext, out int scopeMatches)
{
scopeMatches = 0;
- var tags = stepContext.Tags.Select(t => "@" + t).ToList();
-
if (Tag != null)
{
- if (!_tagExpression.Evaluate(tags))
+ var tags = stepContext.Tags.Select(t => "@" + t).ToList();
+
+ if (!tagExpression.Evaluate(tags))
return false;
scopeMatches++;
@@ -57,7 +52,7 @@ public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
+ if (obj.GetType() != GetType()) return false;
return Equals((BindingScope) obj);
}
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
index 0e09b2438..d736daaf5 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
@@ -1,7 +1,5 @@
using Cucumber.TagExpressions;
using System;
-using System.Collections.Generic;
-using System.Text;
namespace Reqnroll.Bindings.Discovery;
diff --git a/Reqnroll/Bindings/Provider/BindingProviderService.cs b/Reqnroll/Bindings/Provider/BindingProviderService.cs
index 4b5d8bef0..e9c5123cd 100644
--- a/Reqnroll/Bindings/Provider/BindingProviderService.cs
+++ b/Reqnroll/Bindings/Provider/BindingProviderService.cs
@@ -3,7 +3,6 @@
using Reqnroll.Bindings.Provider.Data;
using Reqnroll.Bindings.Reflection;
using Reqnroll.BoDi;
-using Reqnroll.CommonModels;
using Reqnroll.Configuration;
using Reqnroll.EnvironmentAccess;
using Reqnroll.Formatters.Configuration;
diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
index 6bf8b5ebf..bbe59c09a 100644
--- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
+++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs
@@ -28,8 +28,6 @@
using Reqnroll.Time;
using Reqnroll.Tracing;
using Reqnroll.Utils;
-using System;
-using System.ComponentModel;
namespace Reqnroll.Infrastructure
{
diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj
index 4127590c7..2ce92c6fe 100644
--- a/Reqnroll/Reqnroll.csproj
+++ b/Reqnroll/Reqnroll.csproj
@@ -39,7 +39,9 @@
-
+
@@ -68,7 +70,11 @@
-
+
@@ -76,7 +82,11 @@
-
+
diff --git a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
index 09de4c01a..83ca9102c 100644
--- a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
+++ b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs
@@ -3,9 +3,7 @@
using FluentAssertions.Execution;
using Io.Cucumber.Messages.Types;
using Reqnroll.Formatters.PayloadProcessing.Cucumber;
-using System.Collections.Concurrent;
using System.Reflection;
-using System.Runtime.Versioning;
namespace Reqnroll.Formatters.Tests;
@@ -139,10 +137,10 @@ private void CompareMessageType(int partitionNumber)
if (!_expectedElementsByType.ContainsKey(typeof(T)))
return;
- int actualsPartitionNumber = MapPartitionNumber(partitionNumber);
+ int actualPartitionNumber = MapPartitionNumber(partitionNumber);
var actual = _actualElementsByType.TryGetValue(typeof(T), out HashSet
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -45,8 +49,7 @@
-
+
all
false
diff --git a/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.cs b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.cs
new file mode 100644
index 000000000..5365f33a5
--- /dev/null
+++ b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Reqnroll;
+
+namespace Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions;
+
+[Binding]
+internal class tag_expressions
+{
+ [Given("something")]
+ public void GivenSomething()
+ {
+ // No-op
+ }
+
+ [Given("something else")]
+ public void GivenSomethingElse()
+ {
+ // No-op
+ }
+
+ [BeforeScenario("@tag2 and @tag1")]
+ public void BeforeScenario_Tag1AndTag2(IReqnrollOutputHelper reqnrollOutputHelper)
+ {
+ reqnrollOutputHelper.WriteLine("BeforeScenario with @tag2 and @tag1 executed");
+ }
+
+ [BeforeScenario("@tag1 and not @tag3")]
+ public void BeforeScenario_Tag1AndNotTag3(IReqnrollOutputHelper reqnrollOutputHelper)
+ {
+ reqnrollOutputHelper.WriteLine("BeforeScenario with @tag1 and not @tag3 executed");
+ }
+}
diff --git a/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.feature b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.feature
new file mode 100644
index 000000000..8840d4808
--- /dev/null
+++ b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.feature
@@ -0,0 +1,11 @@
+Feature: tag_expressions
+
+This demonstrates use of tag expressions in the binding code
+
+@tag1 @tag2
+Scenario: Multiple tags Scenario
+ Given something
+
+@tag1
+Scenario: Single tag Scenario
+ Given something else
\ No newline at end of file
diff --git a/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson
new file mode 100644
index 000000000..e813e9491
--- /dev/null
+++ b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson
@@ -0,0 +1,30 @@
+{"testRunStarted":{"timestamp":{"seconds":1767969864,"nanos":496985000},"id":"bRSwiaInAaNtSGrxQ26NGg"}}
+{"meta":{"protocolVersion":"30.1.0","implementation":{"name":"Reqnroll","version":"3.3.2-local"},"runtime":{"name":"dotNet","version":"8.0.22"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.26200"},"cpu":{"name":"x64"},"ci":{"name":"Azure Pipelines","url":"/_build/results?buildId=&_a=summary","buildNumber":"20231001.1","git":{"remote":"https://dev.azure.com/reqnroll/reqnroll/_git/reqnroll","revision":"main","branch":"1b1c2588e46d5c995d54da1082b618fa13553eb3"}}}}
+{"stepDefinition":{"id":"dhRLlHi7lRRDpSJFBVrE4A","pattern":{"source":"something","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"GivenSomething","methodParameterTypes":[]}}}}
+{"stepDefinition":{"id":"81F7yjubZ7_8e7QRVhMv0Q","pattern":{"source":"something else","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"GivenSomethingElse","methodParameterTypes":[]}}}}
+{"hook":{"id":"Tg7Dkp6sdwwGm_br7xMBow","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndTag2","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"( @tag2 and @tag1 )","type":"BEFORE_TEST_CASE"}}
+{"hook":{"id":"M-xZyqHpbmKvaed6Wd_o_g","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndNotTag3","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"( @tag1 and not ( @tag3 ) )","type":"BEFORE_TEST_CASE"}}
+{"source":{"uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","data":"Feature: tag_expressions\r\n\r\nThis demonstrates use of tag expressions in the binding code\r\n\r\n@tag1 @tag2\r\nScenario: Multiple tags Scenario\r\n\tGiven something\r\n\r\n@tag1\r\nScenario: Single tag Scenario\r\n\tGiven something else","mediaType":"text/x.cucumber.gherkin+plain"}}
+{"gherkinDocument":{"uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"tag_expressions","description":"This demonstrates use of tag expressions in the binding code","children":[{"scenario":{"location":{"line":6,"column":1},"tags":[{"location":{"line":5,"column":1},"name":"@tag1","id":"a2083bb3d3ec7e59987d654d3ea5b036"},{"location":{"line":5,"column":7},"name":"@tag2","id":"74584f1898fd035b9d652243be10bf03"}],"keyword":"Scenario","name":"Multiple tags Scenario","description":"","steps":[{"location":{"line":7,"column":2},"keyword":"Given ","keywordType":"Context","text":"something","id":"2abb892b35577852b30d56f45fafcca6"}],"examples":[],"id":"ece4621162ed855a88e4fc6e70649b21"}},{"scenario":{"location":{"line":10,"column":1},"tags":[{"location":{"line":9,"column":1},"name":"@tag1","id":"ea929e908274ba5992f89f9b1b7ac108"}],"keyword":"Scenario","name":"Single tag Scenario","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"Given ","keywordType":"Context","text":"something else","id":"8364e91db56ce753a8171a9d94f7b932"}],"examples":[],"id":"a0c8dc85a3a92d50a753faca2fbe6988"}}]},"comments":[]}}
+{"pickle":{"id":"6f5e1efb11b5f65e81fcf1eb5f99f1cb","uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","name":"Multiple tags Scenario","language":"en-US","steps":[{"astNodeIds":["2abb892b35577852b30d56f45fafcca6"],"id":"43d53ae3ea4d0759bef437904717111c","type":"Context","text":"something"}],"tags":[{"name":"@tag1","astNodeId":"a2083bb3d3ec7e59987d654d3ea5b036"},{"name":"@tag2","astNodeId":"74584f1898fd035b9d652243be10bf03"}],"astNodeIds":["ece4621162ed855a88e4fc6e70649b21"]}}
+{"pickle":{"id":"0e2b0c6b58572158ad5cb95076db4d0c","uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","name":"Single tag Scenario","language":"en-US","steps":[{"astNodeIds":["8364e91db56ce753a8171a9d94f7b932"],"id":"d8c0458aa6f5f95b872d901c54e9abe4","type":"Context","text":"something else"}],"tags":[{"name":"@tag1","astNodeId":"ea929e908274ba5992f89f9b1b7ac108"}],"astNodeIds":["a0c8dc85a3a92d50a753faca2fbe6988"]}}
+{"testCase":{"id":"hk0v-PL6pvYQ5iAgFJityw","pickleId":"6f5e1efb11b5f65e81fcf1eb5f99f1cb","testSteps":[{"hookId":"Tg7Dkp6sdwwGm_br7xMBow","id":"8B4wKp7IPGjIKxk9yPXtXg"},{"hookId":"M-xZyqHpbmKvaed6Wd_o_g","id":"kLYLbx1I-dguJ6AOonxVQw"},{"id":"BJDuKzddm-Z9vtwWxkT20g","pickleStepId":"43d53ae3ea4d0759bef437904717111c","stepDefinitionIds":["dhRLlHi7lRRDpSJFBVrE4A"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg"}}
+{"testCaseStarted":{"attempt":0,"id":"k_tv2q_87GWLOWZ3oTCkfQ","testCaseId":"hk0v-PL6pvYQ5iAgFJityw","timestamp":{"seconds":1767969864,"nanos":695178500}}}
+{"testStepStarted":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"8B4wKp7IPGjIKxk9yPXtXg","timestamp":{"seconds":1767969864,"nanos":702375300}}}
+{"attachment":{"body":"BeforeScenario with @tag2 and @tag1 executed","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"8B4wKp7IPGjIKxk9yPXtXg","testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg","testRunHookStartedId":"","timestamp":{"seconds":1767969864,"nanos":711465800}}}
+{"testStepFinished":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"8B4wKp7IPGjIKxk9yPXtXg","testStepResult":{"duration":{"seconds":0,"nanos":11469500},"status":"PASSED"},"timestamp":{"seconds":1767969864,"nanos":713844800}}}
+{"testStepStarted":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"kLYLbx1I-dguJ6AOonxVQw","timestamp":{"seconds":1767969864,"nanos":716500100}}}
+{"attachment":{"body":"BeforeScenario with @tag1 and not @tag3 executed","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"kLYLbx1I-dguJ6AOonxVQw","testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg","testRunHookStartedId":"","timestamp":{"seconds":1767969864,"nanos":717397400}}}
+{"testStepFinished":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"kLYLbx1I-dguJ6AOonxVQw","testStepResult":{"duration":{"seconds":0,"nanos":992400},"status":"PASSED"},"timestamp":{"seconds":1767969864,"nanos":717492500}}}
+{"testStepStarted":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"BJDuKzddm-Z9vtwWxkT20g","timestamp":{"seconds":1767969864,"nanos":727771300}}}
+{"testStepFinished":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","testStepId":"BJDuKzddm-Z9vtwWxkT20g","testStepResult":{"duration":{"seconds":0,"nanos":5367500},"status":"PASSED"},"timestamp":{"seconds":1767969864,"nanos":733138800}}}
+{"testCaseFinished":{"testCaseStartedId":"k_tv2q_87GWLOWZ3oTCkfQ","timestamp":{"seconds":1767969864,"nanos":739876700},"willBeRetried":false}}
+{"testCase":{"id":"6vGXEpNFy5ZYrcITUkBVGQ","pickleId":"0e2b0c6b58572158ad5cb95076db4d0c","testSteps":[{"hookId":"M-xZyqHpbmKvaed6Wd_o_g","id":"yTo5YpTmDpt1PKumg3UFaA"},{"id":"b7x0SoYUfReCkZsCC56Qqw","pickleStepId":"d8c0458aa6f5f95b872d901c54e9abe4","stepDefinitionIds":["81F7yjubZ7_8e7QRVhMv0Q"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg"}}
+{"testCaseStarted":{"attempt":0,"id":"ZxcIoUGNmNopFIoKM4khKw","testCaseId":"6vGXEpNFy5ZYrcITUkBVGQ","timestamp":{"seconds":1767969864,"nanos":760685400}}}
+{"testStepStarted":{"testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","testStepId":"yTo5YpTmDpt1PKumg3UFaA","timestamp":{"seconds":1767969864,"nanos":760844300}}}
+{"attachment":{"body":"BeforeScenario with @tag1 and not @tag3 executed","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","testStepId":"yTo5YpTmDpt1PKumg3UFaA","testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg","testRunHookStartedId":"","timestamp":{"seconds":1767969864,"nanos":761069300}}}
+{"testStepFinished":{"testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","testStepId":"yTo5YpTmDpt1PKumg3UFaA","testStepResult":{"duration":{"seconds":0,"nanos":248800},"status":"PASSED"},"timestamp":{"seconds":1767969864,"nanos":761093100}}}
+{"testStepStarted":{"testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","testStepId":"b7x0SoYUfReCkZsCC56Qqw","timestamp":{"seconds":1767969864,"nanos":761380600}}}
+{"testStepFinished":{"testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","testStepId":"b7x0SoYUfReCkZsCC56Qqw","testStepResult":{"duration":{"seconds":0,"nanos":523500},"status":"PASSED"},"timestamp":{"seconds":1767969864,"nanos":761904100}}}
+{"testCaseFinished":{"testCaseStartedId":"ZxcIoUGNmNopFIoKM4khKw","timestamp":{"seconds":1767969864,"nanos":762084700},"willBeRetried":false}}
+{"testRunFinished":{"success":true,"timestamp":{"seconds":1767969864,"nanos":771489300},"testRunStartedId":"bRSwiaInAaNtSGrxQ26NGg"}}
\ No newline at end of file
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs
index ed7a10eb1..6814e4583 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs
@@ -55,16 +55,16 @@ public void Single_term_expressions_with_at_prefix_remain_unchanged(string tagNa
[InlineData("not tag1")]
[InlineData("tag1 and @tag2")]
[InlineData("@tag1 or tag2")]
- public void Multi_term_expressions_without_at_prefix_throw_exception(string expression)
+ public void Multi_term_expressions_without_at_prefix_returns_InvalidTagExpression(string expression)
{
// Arrange
var parser = CreateParser();
// Act
- Action act = () => parser.Parse(expression);
+ var result = parser.Parse(expression);
// Assert
- act.Should().Throw();
+ result.Should().BeOfType();
}
From 4403a3eb7f8cbb3ebfce9018017a4811afb9c4b8 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 14 Jan 2026 08:57:20 -0600
Subject: [PATCH 15/19] Added properties to surface tag expression errors via
BindingSourceProcessor. Added BindingSourceProcessor test for handling of bad
Scope tag expressions.
---
Reqnroll/Bindings/BindingFactory.cs | 4 ++--
Reqnroll/Bindings/BindingScope.cs | 7 ++++--
.../Discovery/BindingSourceProcessor.cs | 6 ++---
Reqnroll/Bindings/HookBinding.cs | 6 ++++-
Reqnroll/Bindings/IBindingFactory.cs | 2 +-
Reqnroll/Bindings/IHookBinding.cs | 2 ++
.../Provider/BindingProviderService.cs | 4 +++-
.../Provider/Data/BindingScopeData.cs | 1 +
Reqnroll/Bindings/Provider/Data/HookData.cs | 2 ++
.../Discovery/BindingSourceProcessorTests.cs | 22 +++++++++++++++++++
10 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/Reqnroll/Bindings/BindingFactory.cs b/Reqnroll/Bindings/BindingFactory.cs
index e7d54d347..aeb063cba 100644
--- a/Reqnroll/Bindings/BindingFactory.cs
+++ b/Reqnroll/Bindings/BindingFactory.cs
@@ -10,9 +10,9 @@ public class BindingFactory(
: IBindingFactory
{
public IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope,
- int hookOrder)
+ int hookOrder, string errorMessage = null)
{
- return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder);
+ return new HookBinding(bindingMethod, hookType, bindingScope, hookOrder, errorMessage);
}
public IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope,
diff --git a/Reqnroll/Bindings/BindingScope.cs b/Reqnroll/Bindings/BindingScope.cs
index f89e0842a..cc5ed704e 100644
--- a/Reqnroll/Bindings/BindingScope.cs
+++ b/Reqnroll/Bindings/BindingScope.cs
@@ -4,7 +4,7 @@
namespace Reqnroll.Bindings
{
- public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
+ public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle, string errorMessage = null)
{
public string Tag => tagExpression.ToString();
public ITagExpression TagExpression => tagExpression;
@@ -12,6 +12,8 @@ public class BindingScope(ITagExpression tagExpression, string featureTitle, str
public string FeatureTitle { get; } = featureTitle;
public string ScenarioTitle { get; } = scenarioTitle;
+ public bool IsValid => ErrorMessage == null;
+ public string ErrorMessage => errorMessage;
public bool Match(StepContext stepContext, out int scopeMatches)
{
@@ -46,7 +48,7 @@ public bool Match(StepContext stepContext, out int scopeMatches)
protected bool Equals(BindingScope other)
{
- return string.Equals(Tag, other.Tag) && string.Equals(FeatureTitle, other.FeatureTitle) && string.Equals(ScenarioTitle, other.ScenarioTitle);
+ return string.Equals(Tag, other.Tag) && string.Equals(FeatureTitle, other.FeatureTitle) && string.Equals(ScenarioTitle, other.ScenarioTitle) && string.Equals(ErrorMessage, other.ErrorMessage);
}
public override bool Equals(object obj)
@@ -64,6 +66,7 @@ public override int GetHashCode()
var hashCode = (Tag != null ? Tag.GetHashCode() : 0);
hashCode = (hashCode*397) ^ (FeatureTitle != null ? FeatureTitle.GetHashCode() : 0);
hashCode = (hashCode*397) ^ (ScenarioTitle != null ? ScenarioTitle.GetHashCode() : 0);
+ hashCode = (hashCode*397) ^ (ErrorMessage != null ? ErrorMessage.GetHashCode() : 0);
return hashCode;
}
}
diff --git a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
index 4a27248dd..1a2d258a7 100644
--- a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
+++ b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
@@ -182,15 +182,15 @@ private void ProcessHookAttribute(BindingSourceMethod bindingSourceMethod, Bindi
int order = GetHookOrder(hookAttribute);
var validationResult = ValidateHook(bindingSourceMethod, hookAttribute, hookType);
- validationResult += ValidateBindingScope(scope);
+ var scopeValidationResult = ValidateBindingScope(scope);
if (!validationResult.IsValid)
{
- OnValidationError(validationResult, true);
+ OnValidationError(validationResult + scopeValidationResult, true);
return;
}
- var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order);
+ var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order, scopeValidationResult.IsValid ? null : scopeValidationResult.ErrorMessages[0]);
ProcessHookBinding(hookBinding);
}
diff --git a/Reqnroll/Bindings/HookBinding.cs b/Reqnroll/Bindings/HookBinding.cs
index ddcbb4c8f..7ac42e3b1 100644
--- a/Reqnroll/Bindings/HookBinding.cs
+++ b/Reqnroll/Bindings/HookBinding.cs
@@ -9,11 +9,15 @@ public class HookBinding : MethodBinding, IHookBinding
public BindingScope BindingScope { get; private set; }
public bool IsScoped { get { return BindingScope != null; } }
- public HookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, int hookOrder) : base(bindingMethod)
+ public bool IsValid => ErrorMessage == null;
+ public string ErrorMessage { get; }
+
+ public HookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope, int hookOrder, string errorMessage = null) : base(bindingMethod)
{
HookOrder = hookOrder;
HookType = hookType;
BindingScope = bindingScope;
+ ErrorMessage = errorMessage;
}
protected bool Equals(HookBinding other)
diff --git a/Reqnroll/Bindings/IBindingFactory.cs b/Reqnroll/Bindings/IBindingFactory.cs
index 6ed5006aa..b93542076 100644
--- a/Reqnroll/Bindings/IBindingFactory.cs
+++ b/Reqnroll/Bindings/IBindingFactory.cs
@@ -5,7 +5,7 @@ namespace Reqnroll.Bindings
public interface IBindingFactory
{
IHookBinding CreateHookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope bindingScope,
- int hookOrder);
+ int hookOrder, string errorMessage = null);
IStepDefinitionBindingBuilder CreateStepDefinitionBindingBuilder(StepDefinitionType stepDefinitionType, IBindingMethod bindingMethod, BindingScope bindingScope,
string expressionString, ExpressionType expressionType);
diff --git a/Reqnroll/Bindings/IHookBinding.cs b/Reqnroll/Bindings/IHookBinding.cs
index 32b1f0923..8ab0e5167 100644
--- a/Reqnroll/Bindings/IHookBinding.cs
+++ b/Reqnroll/Bindings/IHookBinding.cs
@@ -4,5 +4,7 @@ public interface IHookBinding : IScopedBinding, IBinding
{
HookType HookType { get; }
int HookOrder { get; }
+ bool IsValid { get; }
+ string ErrorMessage { get; }
}
}
\ No newline at end of file
diff --git a/Reqnroll/Bindings/Provider/BindingProviderService.cs b/Reqnroll/Bindings/Provider/BindingProviderService.cs
index 3d8b16af3..e6729b738 100644
--- a/Reqnroll/Bindings/Provider/BindingProviderService.cs
+++ b/Reqnroll/Bindings/Provider/BindingProviderService.cs
@@ -108,6 +108,7 @@ HookData CreateHook(IHookBinding hookBinding)
Type = hookBinding.HookType.ToString(),
HookOrder = hookBinding.HookOrder,
Scope = GetScope(hookBinding),
+ Error = hookBinding.ErrorMessage
};
return hook;
@@ -141,7 +142,8 @@ BindingScopeData GetScope(IScopedBinding scopedBinding)
{
Tag = string.IsNullOrEmpty(tagScope) ? null : tagScope,
FeatureTitle = scopedBinding.BindingScope.FeatureTitle,
- ScenarioTitle = scopedBinding.BindingScope.ScenarioTitle
+ ScenarioTitle = scopedBinding.BindingScope.ScenarioTitle,
+ Error = scopedBinding.BindingScope.ErrorMessage
};
}
diff --git a/Reqnroll/Bindings/Provider/Data/BindingScopeData.cs b/Reqnroll/Bindings/Provider/Data/BindingScopeData.cs
index b8ec8cbd1..ab0811456 100644
--- a/Reqnroll/Bindings/Provider/Data/BindingScopeData.cs
+++ b/Reqnroll/Bindings/Provider/Data/BindingScopeData.cs
@@ -6,4 +6,5 @@ public class BindingScopeData
public string Tag { get; set; }
public string FeatureTitle { get; set; }
public string ScenarioTitle { get; set; }
+ public string Error { get; set; }
}
diff --git a/Reqnroll/Bindings/Provider/Data/HookData.cs b/Reqnroll/Bindings/Provider/Data/HookData.cs
index f2b121890..5401500c9 100644
--- a/Reqnroll/Bindings/Provider/Data/HookData.cs
+++ b/Reqnroll/Bindings/Provider/Data/HookData.cs
@@ -6,4 +6,6 @@ public class HookData
public BindingScopeData Scope { get; set; }
public string Type { get; set; }
public int HookOrder { get; set; }
+ public string Error { get; set; }
+
}
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
index f795a843a..60fd8eaa9 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
@@ -94,6 +94,13 @@ private BindingMethod CreateSyntheticBindingMethod()
return new BindingMethod(bindingType, "MyMethod", Array.Empty(), RuntimeBindingType.Void);
}
+ private BindingSourceAttribute CreateSyntheticScopeBindingSourceAttributeWithTagExpression(string tagExpression)
+ {
+ var bsAttribute = CreateBindingSourceAttribute("ScopeAttribute", "Reqnroll.ScopeAttribute");
+ bsAttribute.NamedAttributeValues.Add("Tag", new BindingSourceAttributeValueProvider(tagExpression));
+ return bsAttribute;
+ }
+
[Binding]
class StepDefClassWithAsyncVoid
{
@@ -175,6 +182,21 @@ public void Non_static_feature_and_test_run_hook_errors_should_be_captured(HookT
sut.ValidationErrors.Should().Contain(m => m.Contains("The binding methods for before/after feature and before/after test run events must be static"));
}
+ [Fact]
+ public void InvalidScopeTagExpressionsOnBindingMethodErrors_should_be_captured()
+ {
+ var sut = CreateBindingSourceProcessor();
+ var bindingSourceType = CreateSyntheticBindingSourceType();
+ var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod();
+ // add invalid tag expression
+ var scopeAttribute = CreateSyntheticScopeBindingSourceAttributeWithTagExpression("@foo not (");
+ bindingSourceMethod.Attributes = bindingSourceMethod.Attributes.Append(scopeAttribute).ToArray();
+ sut.ProcessType(bindingSourceType).Should().BeTrue();
+ sut.ProcessMethod(bindingSourceMethod);
+ sut.BuildingCompleted();
+ sut.ValidationErrors.Should().Contain(m => m.Contains("could not be parsed because of syntax error"));
+ }
+
private static BindingSourceMethod CreateBindingSourceMethod(Type bindingType, string methodName, params BindingSourceAttribute[] attributes)
{
var methodInfo = bindingType.GetMethod(methodName);
From ec0263a89f26f6901e7d994495a0c2ff38bab7c9 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 14 Jan 2026 13:05:30 -0600
Subject: [PATCH 16/19] Refactored tag expression support with addition of a
ReqnollTagExpression that tracks the original tag text. BindingScope now
pulls its ErrorMessage directly from its input tagExpression. Fixed errors in
BindingSourceProcessor handling of scope errors on hooks. Added tests to
BindingSourceProcessorTests demonstrating handling of bad scope expressions.
---
Reqnroll/Bindings/BindingScope.cs | 5 +++--
.../Discovery/BindingSourceProcessor.cs | 6 +++---
.../Discovery/InvalidTagExpression.cs | 9 +++-----
.../Discovery/ReqnrollTagExpression.cs | 21 +++++++++++++++++++
.../Discovery/ReqnrollTagExpressionParser.cs | 8 ++++---
.../Discovery/BindingSourceProcessorTests.cs | 15 +++++++++++++
6 files changed, 50 insertions(+), 14 deletions(-)
create mode 100644 Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
diff --git a/Reqnroll/Bindings/BindingScope.cs b/Reqnroll/Bindings/BindingScope.cs
index cc5ed704e..aa24d0fbc 100644
--- a/Reqnroll/Bindings/BindingScope.cs
+++ b/Reqnroll/Bindings/BindingScope.cs
@@ -1,10 +1,11 @@
using System;
using System.Linq;
using Cucumber.TagExpressions;
+using Reqnroll.Bindings.Discovery;
namespace Reqnroll.Bindings
{
- public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle, string errorMessage = null)
+ public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
{
public string Tag => tagExpression.ToString();
public ITagExpression TagExpression => tagExpression;
@@ -13,7 +14,7 @@ public class BindingScope(ITagExpression tagExpression, string featureTitle, str
public string ScenarioTitle { get; } = scenarioTitle;
public bool IsValid => ErrorMessage == null;
- public string ErrorMessage => errorMessage;
+ public string ErrorMessage => tagExpression is InvalidTagExpression ? tagExpression.ToString() : null;
public bool Match(StepContext stepContext, out int scopeMatches)
{
diff --git a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
index 1a2d258a7..4973d2e1c 100644
--- a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
+++ b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
@@ -183,11 +183,11 @@ private void ProcessHookAttribute(BindingSourceMethod bindingSourceMethod, Bindi
var validationResult = ValidateHook(bindingSourceMethod, hookAttribute, hookType);
var scopeValidationResult = ValidateBindingScope(scope);
-
+ validationResult += scopeValidationResult;
if (!validationResult.IsValid)
{
- OnValidationError(validationResult + scopeValidationResult, true);
- return;
+ OnValidationError(validationResult, true);
+
}
var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order, scopeValidationResult.IsValid ? null : scopeValidationResult.ErrorMessages[0]);
diff --git a/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
index be519e518..5cca5ff84 100644
--- a/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
+++ b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
@@ -2,17 +2,14 @@
using System;
namespace Reqnroll.Bindings.Discovery;
-
-public class InvalidTagExpression : ITagExpression
+public class InvalidTagExpression : ReqnrollTagExpression, ITagExpression
{
public string Message { get; }
- public string OriginalTagExpression { get; }
- public InvalidTagExpression(string originalTagExpression, string message)
+ public InvalidTagExpression(ITagExpression expression, string originalTagExpression, string message) : base(expression, originalTagExpression)
{
- OriginalTagExpression = originalTagExpression;
Message = message;
}
- public bool Evaluate(System.Collections.Generic.IEnumerable tags)
+ public override bool Evaluate(System.Collections.Generic.IEnumerable tags)
{
throw new InvalidOperationException("Cannot evaluate an invalid tag expression: " + Message);
}
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
new file mode 100644
index 000000000..317f75237
--- /dev/null
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
@@ -0,0 +1,21 @@
+using Cucumber.TagExpressions;
+using System.Collections.Generic;
+
+namespace Reqnroll.Bindings.Discovery;
+
+public class ReqnrollTagExpression : ITagExpression
+{
+ public ReqnrollTagExpression(ITagExpression inner, string tagExpressionText)
+ {
+ TagExpressionText = tagExpressionText;
+ _inner = inner;
+ }
+ public string TagExpressionText { get; }
+
+ private ITagExpression _inner;
+
+ public virtual bool Evaluate(IEnumerable inputs)
+ {
+ return _inner.Evaluate(inputs);
+ }
+}
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
index 62a2a73fe..bda801325 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
@@ -8,9 +8,11 @@ public class ReqnrollTagExpressionParser : IReqnrollTagExpressionParser
public ITagExpression Parse(string tagExpression)
{
var tagExpressionParser = new TagExpressionParser();
+ ITagExpression result = null;
try {
- var parsedExpression = tagExpressionParser.Parse(tagExpression);
- return Rewrite(parsedExpression);
+ result = tagExpressionParser.Parse(tagExpression);
+ result = Rewrite(result);
+ return new ReqnrollTagExpression(result, tagExpression);
}
catch (TagExpressionException ex)
{
@@ -19,7 +21,7 @@ public ITagExpression Parse(string tagExpression)
{
msg += $" (at offset {ex.TagToken.Position})";
}
- return new InvalidTagExpression(tagExpression, msg);
+ return new InvalidTagExpression(null, tagExpression, msg);
}
}
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
index 60fd8eaa9..1e8a7ad2d 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
@@ -197,6 +197,21 @@ public void InvalidScopeTagExpressionsOnBindingMethodErrors_should_be_captured()
sut.ValidationErrors.Should().Contain(m => m.Contains("could not be parsed because of syntax error"));
}
+ [Fact]
+ public void InvalidScopeTagExpressionsOnHookBindingMethodErrors_should_be_captured()
+ {
+ var sut = CreateBindingSourceProcessor();
+ var bindingSourceType = CreateSyntheticBindingSourceType();
+ var bindingSourceMethod = CreateSyntheticHookBindingSourceMethod(HookType.BeforeScenario);
+ // add invalid tag expression
+ var scopeAttribute = CreateSyntheticScopeBindingSourceAttributeWithTagExpression("@foo and or");
+ bindingSourceMethod.Attributes = bindingSourceMethod.Attributes.Append(scopeAttribute).ToArray();
+ sut.ProcessType(bindingSourceType).Should().BeTrue();
+ sut.ProcessMethod(bindingSourceMethod);
+ sut.BuildingCompleted();
+ sut.ValidationErrors.Should().Contain(m => m.Contains("could not be parsed because of syntax error"));
+ }
+
private static BindingSourceMethod CreateBindingSourceMethod(Type bindingType, string methodName, params BindingSourceAttribute[] attributes)
{
var methodInfo = bindingType.GetMethod(methodName);
From fa433c47d36464caa21feae871273826cbb4a306 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Wed, 14 Jan 2026 14:44:30 -0600
Subject: [PATCH 17/19] Fix ReqnrollTagExpression missing ToString() override
---
Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
index 317f75237..3d2f72db1 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
@@ -5,14 +5,19 @@ namespace Reqnroll.Bindings.Discovery;
public class ReqnrollTagExpression : ITagExpression
{
+ public string TagExpressionText { get; }
+ private ITagExpression _inner;
+
public ReqnrollTagExpression(ITagExpression inner, string tagExpressionText)
{
TagExpressionText = tagExpressionText;
_inner = inner;
}
- public string TagExpressionText { get; }
- private ITagExpression _inner;
+ public override string ToString()
+ {
+ return _inner.ToString();
+ }
public virtual bool Evaluate(IEnumerable inputs)
{
From 813d9c2dd771ec6f7cdb8f33acbebfdd025a9733 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?=
Date: Fri, 16 Jan 2026 15:05:35 +0100
Subject: [PATCH 18/19] small fixes
---
Reqnroll/Bindings/BindingScope.cs | 2 +-
.../Bindings/Discovery/BindingSourceProcessor.cs | 8 ++++----
.../Bindings/Discovery/InvalidTagExpression.cs | 11 ++++-------
.../Bindings/Discovery/ReqnrollTagExpression.cs | 15 ++++-----------
.../Discovery/ReqnrollTagExpressionParser.cs | 8 ++++----
Reqnroll/Bindings/HookBinding.cs | 3 ++-
Reqnroll/Bindings/Provider/Data/HookData.cs | 1 -
.../Discovery/BindingSourceProcessorTests.cs | 6 +++---
8 files changed, 22 insertions(+), 32 deletions(-)
diff --git a/Reqnroll/Bindings/BindingScope.cs b/Reqnroll/Bindings/BindingScope.cs
index aa24d0fbc..f9a0bf990 100644
--- a/Reqnroll/Bindings/BindingScope.cs
+++ b/Reqnroll/Bindings/BindingScope.cs
@@ -7,7 +7,7 @@ namespace Reqnroll.Bindings
{
public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle)
{
- public string Tag => tagExpression.ToString();
+ public string Tag => tagExpression is ReqnrollTagExpression reqnrollTagExpression ? reqnrollTagExpression.TagExpressionText : tagExpression.ToString();
public ITagExpression TagExpression => tagExpression;
public string FeatureTitle { get; } = featureTitle;
diff --git a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
index 4973d2e1c..ff83735f6 100644
--- a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
+++ b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
@@ -187,10 +187,10 @@ private void ProcessHookAttribute(BindingSourceMethod bindingSourceMethod, Bindi
if (!validationResult.IsValid)
{
OnValidationError(validationResult, true);
-
}
- var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order, scopeValidationResult.IsValid ? null : scopeValidationResult.ErrorMessages[0]);
+ var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order,
+ scopeValidationResult.IsValid ? null : scopeValidationResult.CombinedErrorMessages);
ProcessHookBinding(hookBinding);
}
@@ -363,9 +363,9 @@ protected virtual BindingValidationResult ValidateBindingScope(BindingScope bind
{
var result = BindingValidationResult.Valid;
- if (bindingScope != null && bindingScope.TagExpression is InvalidTagExpression invalidTagExpression)
+ if (bindingScope is { TagExpression: InvalidTagExpression invalidTagExpression })
{
- result += BindingValidationResult.Error(invalidTagExpression.Message);
+ result += BindingValidationResult.Error($"Invalid scope: {invalidTagExpression}");
}
return result;
}
diff --git a/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
index 5cca5ff84..235930ba1 100644
--- a/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
+++ b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs
@@ -2,19 +2,16 @@
using System;
namespace Reqnroll.Bindings.Discovery;
-public class InvalidTagExpression : ReqnrollTagExpression, ITagExpression
+public class InvalidTagExpression(ITagExpression expression, string originalTagExpression, string message) : ReqnrollTagExpression(expression, originalTagExpression)
{
- public string Message { get; }
- public InvalidTagExpression(ITagExpression expression, string originalTagExpression, string message) : base(expression, originalTagExpression)
- {
- Message = message;
- }
+ public string Message { get; } = message;
+
public override bool Evaluate(System.Collections.Generic.IEnumerable tags)
{
throw new InvalidOperationException("Cannot evaluate an invalid tag expression: " + Message);
}
public override string ToString()
{
- return "Invalid Tag Expression: " + Message;
+ return "Invalid tag expression: " + Message;
}
}
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
index 3d2f72db1..7158abc6d 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs
@@ -3,24 +3,17 @@
namespace Reqnroll.Bindings.Discovery;
-public class ReqnrollTagExpression : ITagExpression
+public class ReqnrollTagExpression(ITagExpression inner, string tagExpressionText) : ITagExpression
{
- public string TagExpressionText { get; }
- private ITagExpression _inner;
-
- public ReqnrollTagExpression(ITagExpression inner, string tagExpressionText)
- {
- TagExpressionText = tagExpressionText;
- _inner = inner;
- }
+ public string TagExpressionText { get; } = tagExpressionText;
public override string ToString()
{
- return _inner.ToString();
+ return inner.ToString();
}
public virtual bool Evaluate(IEnumerable inputs)
{
- return _inner.Evaluate(inputs);
+ return inner.Evaluate(inputs);
}
}
diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
index bda801325..0a516859c 100644
--- a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
+++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs
@@ -8,10 +8,10 @@ public class ReqnrollTagExpressionParser : IReqnrollTagExpressionParser
public ITagExpression Parse(string tagExpression)
{
var tagExpressionParser = new TagExpressionParser();
- ITagExpression result = null;
- try {
- result = tagExpressionParser.Parse(tagExpression);
- result = Rewrite(result);
+ try
+ {
+ var result = tagExpressionParser.Parse(tagExpression);
+ result = Rewrite(result);
return new ReqnrollTagExpression(result, tagExpression);
}
catch (TagExpressionException ex)
diff --git a/Reqnroll/Bindings/HookBinding.cs b/Reqnroll/Bindings/HookBinding.cs
index 7ac42e3b1..e0e026674 100644
--- a/Reqnroll/Bindings/HookBinding.cs
+++ b/Reqnroll/Bindings/HookBinding.cs
@@ -22,7 +22,7 @@ public HookBinding(IBindingMethod bindingMethod, HookType hookType, BindingScope
protected bool Equals(HookBinding other)
{
- return HookType == other.HookType && HookOrder == other.HookOrder && Equals(BindingScope, other.BindingScope) && base.Equals(other);
+ return HookType == other.HookType && HookOrder == other.HookOrder && Equals(BindingScope, other.BindingScope) && string.Equals(ErrorMessage, other.ErrorMessage) && base.Equals(other);
}
public override bool Equals(object obj)
@@ -40,6 +40,7 @@ public override int GetHashCode()
var hashCode = (int) HookType;
hashCode = (hashCode*397) ^ HookOrder;
hashCode = (hashCode*397) ^ (BindingScope != null ? BindingScope.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (ErrorMessage != null ? ErrorMessage.GetHashCode() : 0);
return hashCode;
}
}
diff --git a/Reqnroll/Bindings/Provider/Data/HookData.cs b/Reqnroll/Bindings/Provider/Data/HookData.cs
index 5401500c9..21e81cc0f 100644
--- a/Reqnroll/Bindings/Provider/Data/HookData.cs
+++ b/Reqnroll/Bindings/Provider/Data/HookData.cs
@@ -7,5 +7,4 @@ public class HookData
public string Type { get; set; }
public int HookOrder { get; set; }
public string Error { get; set; }
-
}
diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
index 1e8a7ad2d..4cd53b88a 100644
--- a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs
@@ -183,7 +183,7 @@ public void Non_static_feature_and_test_run_hook_errors_should_be_captured(HookT
}
[Fact]
- public void InvalidScopeTagExpressionsOnBindingMethodErrors_should_be_captured()
+ public void InvalidScopeTagExpressionsOnStepDefBindingMethodErrors_should_be_captured()
{
var sut = CreateBindingSourceProcessor();
var bindingSourceType = CreateSyntheticBindingSourceType();
@@ -194,7 +194,7 @@ public void InvalidScopeTagExpressionsOnBindingMethodErrors_should_be_captured()
sut.ProcessType(bindingSourceType).Should().BeTrue();
sut.ProcessMethod(bindingSourceMethod);
sut.BuildingCompleted();
- sut.ValidationErrors.Should().Contain(m => m.Contains("could not be parsed because of syntax error"));
+ sut.ValidationErrors.Should().Contain(m => m.Contains("Invalid scope") && m.Contains("could not be parsed because of syntax error"));
}
[Fact]
@@ -209,7 +209,7 @@ public void InvalidScopeTagExpressionsOnHookBindingMethodErrors_should_be_captur
sut.ProcessType(bindingSourceType).Should().BeTrue();
sut.ProcessMethod(bindingSourceMethod);
sut.BuildingCompleted();
- sut.ValidationErrors.Should().Contain(m => m.Contains("could not be parsed because of syntax error"));
+ sut.ValidationErrors.Should().Contain(m => m.Contains("Invalid scope") && m.Contains("could not be parsed because of syntax error"));
}
private static BindingSourceMethod CreateBindingSourceMethod(Type bindingType, string methodName, params BindingSourceAttribute[] attributes)
From e9566b464d79b7ee3685969bf3c989aa7c424bd5 Mon Sep 17 00:00:00 2001
From: Chris Rudolphi <1702962+clrudolphi@users.noreply.github.com>
Date: Sat, 17 Jan 2026 14:03:33 -0600
Subject: [PATCH 19/19] Adjusted acceptance test file for tag-expressions
Formatters test scenario.
---
.../Samples/Resources/tag-expressions/tag_expressions.ndjson | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson
index e813e9491..1f3b69a49 100644
--- a/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson
+++ b/Tests/Reqnroll.Formatters.Tests/Samples/Resources/tag-expressions/tag_expressions.ndjson
@@ -2,8 +2,8 @@
{"meta":{"protocolVersion":"30.1.0","implementation":{"name":"Reqnroll","version":"3.3.2-local"},"runtime":{"name":"dotNet","version":"8.0.22"},"os":{"name":"Windows","version":"Microsoft Windows 10.0.26200"},"cpu":{"name":"x64"},"ci":{"name":"Azure Pipelines","url":"/_build/results?buildId=&_a=summary","buildNumber":"20231001.1","git":{"remote":"https://dev.azure.com/reqnroll/reqnroll/_git/reqnroll","revision":"main","branch":"1b1c2588e46d5c995d54da1082b618fa13553eb3"}}}}
{"stepDefinition":{"id":"dhRLlHi7lRRDpSJFBVrE4A","pattern":{"source":"something","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"GivenSomething","methodParameterTypes":[]}}}}
{"stepDefinition":{"id":"81F7yjubZ7_8e7QRVhMv0Q","pattern":{"source":"something else","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"GivenSomethingElse","methodParameterTypes":[]}}}}
-{"hook":{"id":"Tg7Dkp6sdwwGm_br7xMBow","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndTag2","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"( @tag2 and @tag1 )","type":"BEFORE_TEST_CASE"}}
-{"hook":{"id":"M-xZyqHpbmKvaed6Wd_o_g","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndNotTag3","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"( @tag1 and not ( @tag3 ) )","type":"BEFORE_TEST_CASE"}}
+{"hook":{"id":"Tg7Dkp6sdwwGm_br7xMBow","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndTag2","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"@tag2 and @tag1","type":"BEFORE_TEST_CASE"}}
+{"hook":{"id":"M-xZyqHpbmKvaed6Wd_o_g","sourceReference":{"javaMethod":{"className":"DefaultTestProject.Reqnroll.Formatters.Tests.Samples.Resources.tag_expressions.tag_expressions","methodName":"BeforeScenario_Tag1AndNotTag3","methodParameterTypes":["IReqnrollOutputHelper"]}},"tagExpression":"@tag1 and not @tag3","type":"BEFORE_TEST_CASE"}}
{"source":{"uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","data":"Feature: tag_expressions\r\n\r\nThis demonstrates use of tag expressions in the binding code\r\n\r\n@tag1 @tag2\r\nScenario: Multiple tags Scenario\r\n\tGiven something\r\n\r\n@tag1\r\nScenario: Single tag Scenario\r\n\tGiven something else","mediaType":"text/x.cucumber.gherkin+plain"}}
{"gherkinDocument":{"uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en-US","keyword":"Feature","name":"tag_expressions","description":"This demonstrates use of tag expressions in the binding code","children":[{"scenario":{"location":{"line":6,"column":1},"tags":[{"location":{"line":5,"column":1},"name":"@tag1","id":"a2083bb3d3ec7e59987d654d3ea5b036"},{"location":{"line":5,"column":7},"name":"@tag2","id":"74584f1898fd035b9d652243be10bf03"}],"keyword":"Scenario","name":"Multiple tags Scenario","description":"","steps":[{"location":{"line":7,"column":2},"keyword":"Given ","keywordType":"Context","text":"something","id":"2abb892b35577852b30d56f45fafcca6"}],"examples":[],"id":"ece4621162ed855a88e4fc6e70649b21"}},{"scenario":{"location":{"line":10,"column":1},"tags":[{"location":{"line":9,"column":1},"name":"@tag1","id":"ea929e908274ba5992f89f9b1b7ac108"}],"keyword":"Scenario","name":"Single tag Scenario","description":"","steps":[{"location":{"line":11,"column":2},"keyword":"Given ","keywordType":"Context","text":"something else","id":"8364e91db56ce753a8171a9d94f7b932"}],"examples":[],"id":"a0c8dc85a3a92d50a753faca2fbe6988"}}]},"comments":[]}}
{"pickle":{"id":"6f5e1efb11b5f65e81fcf1eb5f99f1cb","uri":"FeatureFileb9f74128b2e34d0f90b3094850ed4e84.feature","name":"Multiple tags Scenario","language":"en-US","steps":[{"astNodeIds":["2abb892b35577852b30d56f45fafcca6"],"id":"43d53ae3ea4d0759bef437904717111c","type":"Context","text":"something"}],"tags":[{"name":"@tag1","astNodeId":"a2083bb3d3ec7e59987d654d3ea5b036"},{"name":"@tag2","astNodeId":"74584f1898fd035b9d652243be10bf03"}],"astNodeIds":["ece4621162ed855a88e4fc6e70649b21"]}}