diff --git a/CHANGELOG.md b/CHANGELOG.md index 449ccd9c5..6dc3dd10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # [vNext] ## Improvements: +* Tag Expressions: step definition scopes and hooks may now use tag expressions (such as `@db and not @slow`) (#911) ## Bug fixes: *Contributors of this release (in alphabetical order):* +@clrudolphi # v3.3.2 - 2026-01-14 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 6ba2c12e8..f9a0bf990 100644 --- a/Reqnroll/Bindings/BindingScope.cs +++ b/Reqnroll/Bindings/BindingScope.cs @@ -1,38 +1,30 @@ using System; using System.Linq; +using Cucumber.TagExpressions; +using Reqnroll.Bindings.Discovery; namespace Reqnroll.Bindings { - public class BindingScope + public class BindingScope(ITagExpression tagExpression, string featureTitle, string scenarioTitle) { - public string Tag { get; private set; } - public string FeatureTitle { get; private set; } - public string ScenarioTitle { get; private set; } + public string Tag => tagExpression is ReqnrollTagExpression reqnrollTagExpression ? reqnrollTagExpression.TagExpressionText : tagExpression.ToString(); + public ITagExpression TagExpression => tagExpression; - public BindingScope(string tag, string featureTitle, string scenarioTitle) - { - Tag = RemoveLeadingAt(tag); - FeatureTitle = featureTitle; - ScenarioTitle = scenarioTitle; - } + public string FeatureTitle { get; } = featureTitle; - private string RemoveLeadingAt(string tag) - { - if (tag == null || !tag.StartsWith("@")) - return tag; - - return tag.Substring(1); // remove leading "@" - } + public string ScenarioTitle { get; } = scenarioTitle; + public bool IsValid => ErrorMessage == null; + public string ErrorMessage => tagExpression is InvalidTagExpression ? tagExpression.ToString() : null; public bool Match(StepContext stepContext, out int scopeMatches) { scopeMatches = 0; - var tags = stepContext.Tags; - - if (Tag != null) + if (tagExpression is not NullExpression) { - if (!tags.Contains(Tag)) + var tags = stepContext.Tags.Select(t => "@" + t).ToList(); + + if (!tagExpression.Evaluate(tags)) return false; scopeMatches++; @@ -57,14 +49,14 @@ 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) { 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); } @@ -75,6 +67,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 79dd1b770..ff83735f6 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 IReqnrollTagExpressionParser _tagExpressionParser; private BindingSourceType _currentBindingSourceType = null; private BindingScope[] _typeScopes = null; private readonly List _stepDefinitionBindingBuilders = new(); - protected BindingSourceProcessor(IBindingFactory bindingFactory) + protected BindingSourceProcessor(IBindingFactory bindingFactory, IReqnrollTagExpressionParser 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)); @@ -179,13 +182,15 @@ private void ProcessHookAttribute(BindingSourceMethod bindingSourceMethod, Bindi int order = GetHookOrder(hookAttribute); var validationResult = ValidateHook(bindingSourceMethod, hookAttribute, hookType); + var scopeValidationResult = ValidateBindingScope(scope); + validationResult += scopeValidationResult; if (!validationResult.IsValid) { OnValidationError(validationResult, true); - return; } - var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order); + var hookBinding = _bindingFactory.CreateHookBinding(bindingSourceMethod.BindingMethod, hookType, scope, order, + scopeValidationResult.IsValid ? null : scopeValidationResult.CombinedErrorMessages); ProcessHookBinding(hookBinding); } @@ -250,6 +255,8 @@ private void ProcessStepDefinitionAttribute(BindingSourceMethod bindingSourceMet var expressionType = stepDefinitionAttribute.TryGetAttributeValue(nameof(StepDefinitionBaseAttribute.ExpressionType)); var validationResult = ValidateStepDefinition(bindingSourceMethod, stepDefinitionAttribute); + validationResult += ValidateBindingScope(scope); + if (!validationResult.IsValid) { OnValidationError(validationResult, false); @@ -352,6 +359,17 @@ protected virtual BindingValidationResult ValidateHook(BindingSourceMethod bindi return result; } + protected virtual BindingValidationResult ValidateBindingScope(BindingScope bindingScope) + { + var result = BindingValidationResult.Valid; + + if (bindingScope is { TagExpression: InvalidTagExpression invalidTagExpression }) + { + result += BindingValidationResult.Error($"Invalid scope: {invalidTagExpression}"); + } + return result; + } + protected bool IsScenarioSpecificHook(HookType hookType) { return diff --git a/Reqnroll/Bindings/Discovery/IReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/IReqnrollTagExpressionParser.cs new file mode 100644 index 000000000..8d62c5dc3 --- /dev/null +++ b/Reqnroll/Bindings/Discovery/IReqnrollTagExpressionParser.cs @@ -0,0 +1,16 @@ +using Cucumber.TagExpressions; + +namespace Reqnroll.Bindings.Discovery; + +/// +/// Defines a parser for tag expressions. +/// +public interface IReqnrollTagExpressionParser +{ + /// + /// Parses the specified text into an . + /// + /// The tag expression string to parse. + /// An representing the parsed expression. + ITagExpression Parse(string text); +} \ No newline at end of file diff --git a/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs new file mode 100644 index 000000000..235930ba1 --- /dev/null +++ b/Reqnroll/Bindings/Discovery/InvalidTagExpression.cs @@ -0,0 +1,17 @@ +using Cucumber.TagExpressions; +using System; + +namespace Reqnroll.Bindings.Discovery; +public class InvalidTagExpression(ITagExpression expression, string originalTagExpression, string message) : ReqnrollTagExpression(expression, originalTagExpression) +{ + 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; + } +} diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs new file mode 100644 index 000000000..7158abc6d --- /dev/null +++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpression.cs @@ -0,0 +1,19 @@ +using Cucumber.TagExpressions; +using System.Collections.Generic; + +namespace Reqnroll.Bindings.Discovery; + +public class ReqnrollTagExpression(ITagExpression inner, string tagExpressionText) : ITagExpression +{ + public string TagExpressionText { get; } = tagExpressionText; + + public override string ToString() + { + return inner.ToString(); + } + + public virtual bool Evaluate(IEnumerable inputs) + { + return inner.Evaluate(inputs); + } +} diff --git a/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs new file mode 100644 index 000000000..0a516859c --- /dev/null +++ b/Reqnroll/Bindings/Discovery/ReqnrollTagExpressionParser.cs @@ -0,0 +1,65 @@ +using Cucumber.TagExpressions; +using System; + +namespace Reqnroll.Bindings.Discovery; + +public class ReqnrollTagExpressionParser : IReqnrollTagExpressionParser +{ + public ITagExpression Parse(string tagExpression) + { + var tagExpressionParser = new TagExpressionParser(); + try + { + var result = tagExpressionParser.Parse(tagExpression); + result = Rewrite(result); + return new ReqnrollTagExpression(result, tagExpression); + } + catch (TagExpressionException ex) + { + var msg = ex.Message; + if (ex.TagToken != null) + { + msg += $" (at offset {ex.TagToken.Position})"; + } + return new InvalidTagExpression(null, tagExpression, msg); + } + } + + // iff the expression is a literal node, prefix it with '@' if not already present + private ITagExpression Rewrite(ITagExpression expression) + { + if (expression is LiteralNode) + { + return PrefixLiteralNode(expression); + } + if (ConfirmExpressionHasAtPrefixes(expression)) + return expression; + throw new TagExpressionException("In multi-term tag expressions, all tag names must start with '@'."); + } + + private bool ConfirmExpressionHasAtPrefixes(ITagExpression expression) + { + switch (expression) + { + case NullExpression: + return true; + case BinaryOpNode binaryNode: + return ConfirmExpressionHasAtPrefixes(binaryNode.Left) && ConfirmExpressionHasAtPrefixes(binaryNode.Right); + case NotNode notNode: + return ConfirmExpressionHasAtPrefixes(notNode.Operand); + case LiteralNode literalNode: + return literalNode.Name.StartsWith("@"); + default: + throw new InvalidOperationException($"Unknown tag expression node type: {expression.GetType().FullName}"); + } + } + + private ITagExpression PrefixLiteralNode(ITagExpression expression) + { + var literalNode = (LiteralNode)expression; + if (literalNode.Name.IsNullOrEmpty() || literalNode.Name.StartsWith("@")) + return literalNode; + + return new LiteralNode("@" + literalNode.Name); + } +} diff --git a/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/RuntimeBindingSourceProcessor.cs index 3e7372153..e2fea6db6 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, IReqnrollTagExpressionParser tagExpressionParser) : base(bindingFactory, tagExpressionParser) { _bindingRegistry = bindingRegistry; _testTracer = testTracer; diff --git a/Reqnroll/Bindings/HookBinding.cs b/Reqnroll/Bindings/HookBinding.cs index ddcbb4c8f..e0e026674 100644 --- a/Reqnroll/Bindings/HookBinding.cs +++ b/Reqnroll/Bindings/HookBinding.cs @@ -9,16 +9,20 @@ 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) { - 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) @@ -36,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/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 520d5cfad..e6729b738 100644 --- a/Reqnroll/Bindings/Provider/BindingProviderService.cs +++ b/Reqnroll/Bindings/Provider/BindingProviderService.cs @@ -1,8 +1,8 @@ -using Reqnroll.Bindings.Discovery; +using Cucumber.TagExpressions; +using Reqnroll.Bindings.Discovery; 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; @@ -108,6 +108,7 @@ HookData CreateHook(IHookBinding hookBinding) Type = hookBinding.HookType.ToString(), HookOrder = hookBinding.HookOrder, Scope = GetScope(hookBinding), + Error = hookBinding.ErrorMessage }; return hook; @@ -136,13 +137,13 @@ BindingScopeData GetScope(IScopedBinding scopedBinding) if (!scopedBinding.IsScoped) return null; + string tagScope = scopedBinding.BindingScope.Tag; return new BindingScopeData { - Tag = scopedBinding.BindingScope.Tag == null - ? null - : "@" + scopedBinding.BindingScope.Tag, + 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..21e81cc0f 100644 --- a/Reqnroll/Bindings/Provider/Data/HookData.cs +++ b/Reqnroll/Bindings/Provider/Data/HookData.cs @@ -6,4 +6,5 @@ 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/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 3121282b3..f4f64ee93 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; @@ -27,7 +28,6 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.Utils; -using System; namespace Reqnroll.Infrastructure { @@ -135,6 +135,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterFactoryAs(() => container.Resolve()); container.RegisterTypeAs(); + container.RegisterTypeAs(); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 88cb7a04a..9fb28398b 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -21,6 +21,7 @@ + diff --git a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesValidator.cs index 85a30a47a..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? actualElements) && actualElements.Count > 0 ? - actualElements.OfType().Where(e => _actualPartitions[e!] == actualsPartitionNumber).ToList() : new List(); + actualElements.OfType().Where(e => _actualPartitions[e!] == actualPartitionNumber).ToList() : new List(); var expected = _expectedElementsByType[typeof(T)].AsEnumerable().OfType().Where(e => _expectedPartitions[e!] == partitionNumber).ToList(); @@ -191,11 +189,11 @@ private void ActualTestExecutionMessagesShouldReferBackToTheSameStepTextAsExpect for (int i = 0; i < _numPartitions; i++) { var partitionNumber = i + 1; - int actualsPartitionNumber = MapPartitionNumber(partitionNumber); + int actualPartitionNumber = MapPartitionNumber(partitionNumber); // For each TestStepStarted message, ensure that the pickle step referred to is the same in Actual and Expected for the corresponding testStepStarted message - var actualTestStepStartedTestStepIds = _actualElementsByType[typeof(TestStepStarted)].OfType().Where(e => _actualPartitions[e!] == actualsPartitionNumber).Select(tss => tss.TestStepId).ToList(); - var expectedTestStepStartedTestStepIds = _expectedElementsByType[typeof(TestStepStarted)].OfType().Where(e => _expectedPartitions[e!] == partitionNumber).Select(tss => tss.TestStepId).ToList(); + var actualTestStepStartedTestStepIds = _actualElementsByType[typeof(TestStepStarted)].OfType().Where(e => _actualPartitions[e] == actualPartitionNumber).Select(tss => tss.TestStepId).ToList(); + var expectedTestStepStartedTestStepIds = _expectedElementsByType[typeof(TestStepStarted)].OfType().Where(e => _expectedPartitions[e] == partitionNumber).Select(tss => tss.TestStepId).ToList(); // Making the assumption here that the order of TestStepStarted messages is the same in both Actual and Expected within a Partition // pair these up, and walk back to the pickle step text and compare @@ -330,7 +328,7 @@ private void EachTestCaseAndStepsShouldProperlyReferToAPickleAndStepDefinitionOr if (step.PickleStepId != null) _actualElementsById.Should().ContainKey(step.PickleStepId, "a step references a pickle step that doesn't exist"); - if (step.StepDefinitionIds != null && step.StepDefinitionIds.Count > 0) + if (step.StepDefinitionIds is { Count: > 0 }) { foreach (var stepDefinitionId in step.StepDefinitionIds) _actualElementsById.Should().ContainKey(stepDefinitionId, "a step references a step definition that doesn't exist"); @@ -374,19 +372,19 @@ public void ShouldPassBasicStructuralChecks() { throw new System.Exception($"{messageType} present in the expected but not in the actual."); } - if (messageType != typeof(Hook) && _actualElementsByType.ContainsKey(messageType)) + if (messageType != typeof(Hook) && _actualElementsByType.TryGetValue(messageType, out var nonHookElement)) { - _actualElementsByType[messageType].Should().HaveCount(_expectedElementsByType[messageType].Count()); + nonHookElement.Should().HaveCount(_expectedElementsByType[messageType].Count); } - if (messageType == typeof(Hook) && _actualElementsByType.ContainsKey(messageType)) - _actualElementsByType[messageType].Should().HaveCountGreaterThanOrEqualTo(_expectedElementsByType[messageType].Count()); + if (messageType == typeof(Hook) && _actualElementsByType.TryGetValue(messageType, out var hookElement)) + hookElement.Should().HaveCountGreaterThanOrEqualTo(_expectedElementsByType[messageType].Count); } actual.Should().HaveCountGreaterThanOrEqualTo(expected.Count(), "the total number of envelopes in the actual should be at least as many as in the expected"); } } - private bool GroupListIsEmpty(List groups) + private bool GroupListIsEmpty(List? groups) { if (groups == null || groups.Count == 0) return true; foreach (var group in groups) @@ -407,7 +405,7 @@ private EquivalencyAssertionOptions ArrangeFluentAssertionOptions(Equivale .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() - .ComparingByMembers() + .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() .ComparingByMembers() @@ -458,7 +456,7 @@ private EquivalencyAssertionOptions ArrangeFluentAssertionOptions(Equivale // Using a custom Property Selector so that we can ignore the properties that are not comparable .Using(_customCucumberMessagesPropertySelector) - // Using a custom string comparison that deals with ISO langauge codes when the property name ends with "Language" + // Using a custom string comparison that deals with ISO language codes when the property name ends with "Language" //.Using(ctx => //{ // var actual = ctx.Subject.Split("-")[0]; @@ -466,7 +464,7 @@ private EquivalencyAssertionOptions ArrangeFluentAssertionOptions(Equivale // actual.Should().Be(expected); //}) - // Using special logic to assert that suggestions must contain at least one snippets among those specified in the Expected set + // Using special logic to assert that suggestions must contain at least one snippet among those specified in the Expected set // We can't compare snippet content as the Language and Code properties won't match .Using(ctx => { @@ -547,17 +545,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.Formatters.Tests/MessagesCompatibilityTests.cs b/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTests.cs index 701afdec4..27312ba21 100644 --- a/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTests.cs +++ b/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTests.cs @@ -80,8 +80,8 @@ public void CCKScenarios(string testName) ConfirmAllTestsRan(numOfTests); } - [Ignore] [TestMethod] + [DataRow("tag-expressions")] // 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) diff --git a/Tests/Reqnroll.Formatters.Tests/Reqnroll.Formatters.Tests.csproj b/Tests/Reqnroll.Formatters.Tests/Reqnroll.Formatters.Tests.csproj index f7624cec5..557fe120b 100644 --- a/Tests/Reqnroll.Formatters.Tests/Reqnroll.Formatters.Tests.csproj +++ b/Tests/Reqnroll.Formatters.Tests/Reqnroll.Formatters.Tests.csproj @@ -32,6 +32,10 @@ + + + + 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..1f3b69a49 --- /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/BindingSourceProcessorStub.cs b/Tests/Reqnroll.RuntimeTests/BindingSourceProcessorStub.cs index d16a93041..c0db55ffe 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()) { } diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/BindingFactoryTests.cs index a5eba674b..ba50694e0 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(); 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/Discovery/BindingSourceProcessorTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs index f795a843a..4cd53b88a 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,36 @@ 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 InvalidScopeTagExpressionsOnStepDefBindingMethodErrors_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("Invalid scope") && 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("Invalid scope") && 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); diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs new file mode 100644 index 000000000..6814e4583 --- /dev/null +++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/ReqnrollTagExpressionParserTests.cs @@ -0,0 +1,123 @@ +using System; +using Xunit; +using FluentAssertions; +using Reqnroll.Bindings.Discovery; +using Cucumber.TagExpressions; + +namespace Reqnroll.RuntimeTests.Bindings.Discovery; + +public class ReqnrollTagExpressionParserTests +{ + private ReqnrollTagExpressionParser CreateParser() + { + return new ReqnrollTagExpressionParser(); + } + + [Theory] + [InlineData("tag1")] + [InlineData("myTag")] + [InlineData("feature")] + public void Single_term_expressions_without_at_prefix_are_correctly_prefixed(string tagName) + { + // Arrange + var parser = CreateParser(); + + // Act + var expression = parser.Parse(tagName); + + // Assert + expression.Should().NotBeNull(); + expression.Evaluate(["@" + tagName]).Should().BeTrue("tag with @ prefix should match"); + expression.Evaluate([tagName]).Should().BeFalse("tag without @ prefix should not match"); + } + + [Theory] + [InlineData("@tag1")] + [InlineData("@myTag")] + [InlineData("@feature")] + public void Single_term_expressions_with_at_prefix_remain_unchanged(string tagName) + { + // Arrange + var parser = CreateParser(); + + // Act + var expression = parser.Parse(tagName); + + // Assert + expression.Should().NotBeNull(); + expression.Evaluate([tagName]).Should().BeTrue("tag with @ prefix should match"); + } + + + [Theory] + [InlineData("tag1 and tag2")] + [InlineData("tag1 or tag2")] + [InlineData("not tag1")] + [InlineData("tag1 and @tag2")] + [InlineData("@tag1 or tag2")] + public void Multi_term_expressions_without_at_prefix_returns_InvalidTagExpression(string expression) + { + // Arrange + var parser = CreateParser(); + + // Act + var result = parser.Parse(expression); + + // Assert + result.Should().BeOfType(); + } + + + [Theory] + [InlineData("@tag1 and @tag2", new[] { "@tag1", "@tag2" }, true)] + [InlineData("@tag1 and @tag2", new[] { "@tag1" }, false)] + [InlineData("@tag1 or @tag2", new[] { "@tag1" }, true)] + [InlineData("@tag1 or @tag2", new[] { "@tag2" }, true)] + [InlineData("@tag1 or @tag2", new[] { "@tag3" }, false)] + [InlineData("not @tag1", new[] { "@tag1" }, false)] + [InlineData("not @tag1", new[] { "@tag2" }, true)] + public void Multi_term_expressions_with_at_prefix_work_correctly(string expression, string[] tags, bool expectedResult) + { + // Arrange + var parser = CreateParser(); + + // Act + var parsedExpression = parser.Parse(expression); + + // Assert + parsedExpression.Should().NotBeNull(); + parsedExpression.Evaluate(tags).Should().Be(expectedResult); + } + + + [Fact] + public void Empty_tag_expression_returns_true_for_any_tags() + { + // Arrange + var parser = CreateParser(); + + // Act + var expression = parser.Parse(""); + + // Assert + expression.Should().NotBeNull(); + expression.Evaluate(["@tag1"]).Should().BeTrue(); + expression.Evaluate(Array.Empty()).Should().BeTrue(); + } + + [Fact] + public void Null_or_empty_tag_names_are_handled_correctly() + { + // Arrange + var parser = CreateParser(); + + // Act & Assert - empty string should be handled + var expression1 = parser.Parse(""); + expression1.Should().NotBeNull(); + + // Null expression should be handled by underlying parser + Action act = () => parser.Parse(null); + act.Should().NotThrow(); + } + +} diff --git a/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs b/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs index 2f0986df8..ef4cffa42 100644 --- a/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs +++ b/Tests/Reqnroll.RuntimeTests/Infrastructure/StepDefinitionMatchServiceTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Globalization; using Moq; @@ -7,226 +8,248 @@ using Reqnroll.Infrastructure; using FluentAssertions; using Reqnroll.RuntimeTests.ErrorHandling; +using Reqnroll.Bindings.Discovery; -namespace Reqnroll.RuntimeTests.Infrastructure +namespace Reqnroll.RuntimeTests.Infrastructure; + +public class StepDefinitionMatchServiceTest { - - public class StepDefinitionMatchServiceTest - { - private Mock bindingRegistryMock; - private Mock stepArgumentTypeConverterMock; - private readonly CultureInfo bindingCulture = new CultureInfo("en-US", false); - private List whenStepDefinitions; + private readonly Mock _bindingRegistryMock; + private readonly Mock _stepArgumentTypeConverterMock; + private readonly CultureInfo _bindingCulture = new("en-US", false); + private readonly List _whenStepDefinitions; - public StepDefinitionMatchServiceTest() - { - whenStepDefinitions = new List(); - bindingRegistryMock = new Mock(); - bindingRegistryMock.Setup(r => r.GetConsideredStepDefinitions(StepDefinitionType.When, It.IsAny())) - .Returns(whenStepDefinitions); + public StepDefinitionMatchServiceTest() + { + _whenStepDefinitions = new List(); + _bindingRegistryMock = new Mock(); + _bindingRegistryMock.Setup(r => r.GetConsideredStepDefinitions(StepDefinitionType.When, It.IsAny())) + .Returns(_whenStepDefinitions); + + _stepArgumentTypeConverterMock = new Mock(); + } + + private StepDefinitionMatchService CreateSut() + { + return new StepDefinitionMatchService(_bindingRegistryMock.Object, _stepArgumentTypeConverterMock.Object, new StubErrorProvider(), new MatchArgumentCalculator()); + } + + private static BindingMethod CreateBindingMethod(string name = "dummy") + { + return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, Array.Empty(), null); + } + + private static BindingMethod CreateBindingMethodWithStringParam(string name = "dummy") + { + return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, [new BindingParameter(new RuntimeBindingType(typeof(string)), "param1")], null); + } - stepArgumentTypeConverterMock = new Mock(); - } + private static BindingMethod CreateBindingMethodWithDataTableParam(string name = "dummy") + { + return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, [new BindingParameter(new RuntimeBindingType(typeof(Table)), "param1")], null); + } - private StepDefinitionMatchService CreateSUT() - { - return new StepDefinitionMatchService(bindingRegistryMock.Object, stepArgumentTypeConverterMock.Object, new StubErrorProvider(), new MatchArgumentCalculator()); - } + private static BindingMethod CreateBindingMethodWithObjectParam(string name = "dummy") + { + return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, [new BindingParameter(new RuntimeBindingType(typeof(object)), "param1")], null); + } + + private StepInstance CreateSimpleWhen(string text = "I do something", IEnumerable tags = null) + { + tags ??= Array.Empty(); + var result = new StepInstance(StepDefinitionType.When, StepDefinitionKeyword.When, "When ", text, null, null, + 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(); + return new BindingScope(tagExpressionParser.Parse(tag), featureTitle, scenarioTitle); + } + + [Fact] + public void Should_GetBestMatch_succeed_when_proper_match() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); - private static BindingMethod CreateBindingMethod(string name = "dummy") - { - return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, new IBindingParameter[0], null); - } + var sut = CreateSut(); - private static BindingMethod CreateBindingMethodWithStringParam(string name = "dummy") - { - return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, new IBindingParameter[] { new BindingParameter(new RuntimeBindingType(typeof(string)), "param1") }, null); - } + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - private static BindingMethod CreateBindingMethodWithDataTableParam(string name = "dummy") - { - return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, new IBindingParameter[] { new BindingParameter(new RuntimeBindingType(typeof(Table)), "param1") }, null); - } + result.Success.Should().BeTrue(); + } - private static BindingMethod CreateBindingMethodWithObjectParam(string name = "dummy") - { - return new BindingMethod(new BindingType("dummy", "dummy", "dummy"), name, new IBindingParameter[] { new BindingParameter(new RuntimeBindingType(typeof(object)), "param1") }, null); - } + [Fact] + 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"), CreateBindingScope("non-matching-tag", null, null))); - private StepInstance CreateSimpleWhen(string text = "I do something") - { - var result = new StepInstance(StepDefinitionType.When, StepDefinitionKeyword.When, "When ", text, null, null, - new StepContext("MyFeature", "MyScenario", new string[0], new CultureInfo("en-US", false))); - return result; - } + var sut = CreateSut(); - [Fact] - public void Should_GetBestMatch_succeed_when_proper_match() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - var sut = CreateSUT(); + result.Success.Should().BeTrue(); + } - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + [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))); - result.Success.Should().BeTrue(); - } + var sut = CreateSut(); - [Fact] - 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))); + var result = sut.GetBestMatch(CreateSimpleWhen("I do something", ["alpha", "beta"]), _bindingCulture, out _, out _); - var sut = CreateSUT(); + result.Success.Should().BeTrue(); + result.StepBinding.Method.Name.Should().Be("m2"); + } - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } - [Fact] - public void Should_GetBestMatch_succeed_when_proper_match_with_parameters() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); + [Fact] + public void Should_GetBestMatch_succeed_when_proper_match_with_parameters() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [Fact] - public void Should_GetBestMatch_succeed_when_proper_match_with_parameters_even_if_there_is_a_DataTable_overload() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); + [Fact] + public void Should_GetBestMatch_succeed_when_proper_match_with_parameters_even_if_there_is_a_DataTable_overload() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithStringParam())); + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [Fact] - public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); + [Fact] + public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [Fact] - public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters_even_if_there_is_a_DataTable_overload() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); + [Fact] + public void Should_GetBestMatch_succeed_when_proper_match_with_object_parameters_even_if_there_is_a_DataTable_overload() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethodWithObjectParam())); + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethodWithDataTableParam())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [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))); + [Fact] + public void Should_GetBestMatch_fail_when_scope_errors_with_single_match() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod(), CreateBindingScope("non-matching-tag", null, null))); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out var ambiguityReason, out _); - result.Success.Should().BeFalse(); - ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); - } + result.Success.Should().BeFalse(); + ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); + } - [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))); + [Fact] + public void Should_GetBestMatch_fail_when_scope_errors_with_multiple_matches() + { + _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(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out var ambiguityReason, out _); - result.Success.Should().BeFalse(); - ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); - } + result.Success.Should().BeFalse(); + ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousScopes); + } - [Fact] // in case of single parameter error, we pretend success - the error will be displayed runtime - public void Should_GetBestMatch_succeed_when_parameter_errors_with_single_match() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod())); + [Fact] // in case of single parameter error, we pretend success - the error will be displayed runtime + public void Should_GetBestMatch_succeed_when_parameter_errors_with_single_match() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [Fact] - public void Should_GetBestMatch_fail_when_parameter_errors_with_multiple_matches() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy1"))); - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy2"))); + [Fact] + public void Should_GetBestMatch_fail_when_parameter_errors_with_multiple_matches() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy1"))); + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "(.*)", CreateBindingMethod("dummy2"))); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out var ambiguityReason, out _); - result.Success.Should().BeFalse(); - ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.ParameterErrors); - } + result.Success.Should().BeFalse(); + ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.ParameterErrors); + } - [Fact] - public void Should_GetBestMatch_fail_when_multiple_matches() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"))); - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"))); + [Fact] + public void Should_GetBestMatch_fail_when_multiple_matches() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy1"))); + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod("dummy2"))); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out var ambiguityReason, out _); - result.Success.Should().BeFalse(); - ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousSteps); - } + result.Success.Should().BeFalse(); + ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.AmbiguousSteps); + } - [Fact] - public void Should_GetBestMatch_succeed_when_multiple_matches_are_on_the_same_method() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); + [Fact] + public void Should_GetBestMatch_succeed_when_multiple_matches_are_on_the_same_method() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, ".*", CreateBindingMethod())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out _, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out _, out _); - result.Success.Should().BeTrue(); - } + result.Success.Should().BeTrue(); + } - [Fact] - public void Should_GetBestMatch_succeed_when_no_matching_step_definitions() - { - whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "non-maching-regex", CreateBindingMethod())); + [Fact] + public void Should_GetBestMatch_succeed_when_no_matching_step_definitions() + { + _whenStepDefinitions.Add(StepDefinitionHelper.CreateRegex(StepDefinitionType.When, "non-matching-regex", CreateBindingMethod())); - var sut = CreateSUT(); + var sut = CreateSut(); - var result = sut.GetBestMatch(CreateSimpleWhen(), bindingCulture, out var ambiguityReason, out _); + var result = sut.GetBestMatch(CreateSimpleWhen(), _bindingCulture, out var ambiguityReason, out _); - result.Success.Should().BeFalse(); - ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.None); - } + result.Success.Should().BeFalse(); + ambiguityReason.Should().Be(StepDefinitionAmbiguityReason.None); } -} +} \ No newline at end of file diff --git a/docs/automation/scoped-bindings.md b/docs/automation/scoped-bindings.md index 8401c1025..b49ae99e1 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 required within a tag expression, except for single-term expressions (such as "foo"). +``` +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. @@ -43,12 +60,12 @@ The following example combines the feature name and the tag scope with "AND": If multiple `[Scope]` attributes are defined for the same method or class, the attributes are combined with OR, i.e. at least one of the `[Scope]` attributes needs to match. -The following example combines the tag scopes with "OR": +The following example combines the 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 = "@anotherTag", Feature="thatFeature")] ``` ````{note} @@ -103,6 +120,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 +147,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`.