From 7c006afed7c457dc9333d3de7e58e28e85eb0e43 Mon Sep 17 00:00:00 2001 From: jensakejohansson Date: Tue, 31 Mar 2026 23:25:54 +0200 Subject: [PATCH 1/5] Guards all StepRegistry mutations to make them thread-safe. Signed-off-by: jensakejohansson --- src/Gauge.Dotnet.csproj | 2 +- src/Loaders/StaticLoader.cs | 25 ++++++++++++++++++----- src/Processors/StepValidationProcessor.cs | 5 ++++- src/Registries/IStepRegistry.cs | 1 + src/Registries/StepRegistry.cs | 5 +++++ src/dotnet.json | 2 +- test/Processors/ValidateProcessorTests.cs | 12 +++++++++-- test/StepRegistryTests.cs | 13 ++++++++++++ 8 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/Gauge.Dotnet.csproj b/src/Gauge.Dotnet.csproj index d481a94..81d4042 100644 --- a/src/Gauge.Dotnet.csproj +++ b/src/Gauge.Dotnet.csproj @@ -6,7 +6,7 @@ enable Gauge.Runner The Gauge Team - 0.7.6 + 0.7.7 ThoughtWorks Inc. Gauge C# runner for Gauge. https://gauge.org diff --git a/src/Loaders/StaticLoader.cs b/src/Loaders/StaticLoader.cs index d8e221b..6e69a5a 100644 --- a/src/Loaders/StaticLoader.cs +++ b/src/Loaders/StaticLoader.cs @@ -24,6 +24,12 @@ public sealed class StaticLoader : IStaticLoader private readonly IConfiguration _config; private readonly ILogger _logger; + // Guards all StepRegistry mutations. gRPC dispatches CacheFileRequest messages concurrently on separate threads, + // all sharing this singleton. Without synchronization, concurrent ReloadSteps calls can interleave their + // RemoveSteps/AddStep operations — one thread's dictionary rebuild overwrites another's, leaving stale entries + // that cause false "duplicate step implementation" warnings in the IDE. + private readonly object _registryLock = new object(); + public StaticLoader(IAttributesLoader attributesLoader, IDirectoryWrapper directoryWrapper, IConfiguration config, ILogger logger) { @@ -44,20 +50,29 @@ public IStepRegistry GetStepRegistry() public void LoadStepsFromText(string content, string filepath) { - var steps = GetStepsFrom(content); - AddStepsToRegistry(filepath, steps); + lock (_registryLock) + { + var steps = GetStepsFrom(content); + AddStepsToRegistry(filepath, steps); + } } public void ReloadSteps(string content, string filepath) { if (IsFileRemoved(filepath)) return; - _stepRegistry.RemoveSteps(filepath); - LoadStepsFromText(content, filepath); + lock (_registryLock) + { + _stepRegistry.RemoveSteps(filepath); + LoadStepsFromText(content, filepath); + } } public void RemoveSteps(string file) { - _stepRegistry.RemoveSteps(file); + lock (_registryLock) + { + _stepRegistry.RemoveSteps(file); + } } private bool IsFileRemoved(string file) diff --git a/src/Processors/StepValidationProcessor.cs b/src/Processors/StepValidationProcessor.cs index 8112b2f..8a23491 100644 --- a/src/Processors/StepValidationProcessor.cs +++ b/src/Processors/StepValidationProcessor.cs @@ -37,7 +37,10 @@ public Task Process(int stream, StepValidateRequest reques { isValid = false; errorType = StepValidateResponse.Types.ErrorType.DuplicateStepImplementation; - errorMessage = string.Format("Multiple step implementations found for : {0}", stepToValidate); + var implementations = _stepRegistry.MethodsFor(stepToValidate); + var locations = string.Join("\n", implementations.Select(m => + $" {m.ClassName}.{m.Name} in {m.FileName}:{m.Span.StartLinePosition.Line + 1}")); + errorMessage = $"Step: {stepToValidate}\n{locations}"; } return Task.FromResult(GetStepValidateResponseMessage(isValid, errorType, errorMessage, suggestion)); } diff --git a/src/Registries/IStepRegistry.cs b/src/Registries/IStepRegistry.cs index f6f616e..a0e3287 100644 --- a/src/Registries/IStepRegistry.cs +++ b/src/Registries/IStepRegistry.cs @@ -13,6 +13,7 @@ public interface IStepRegistry { bool ContainsStep(string parsedStepText); GaugeMethod MethodFor(string parsedStepText); + IEnumerable MethodsFor(string parsedStepText); bool HasAlias(string stepText); string GetStepText(string parameterizedStepText); IEnumerable GetStepTexts(); diff --git a/src/Registries/StepRegistry.cs b/src/Registries/StepRegistry.cs index 751a9e4..2f89012 100644 --- a/src/Registries/StepRegistry.cs +++ b/src/Registries/StepRegistry.cs @@ -89,6 +89,11 @@ public GaugeMethod MethodFor(string parsedStepText) return _registry[parsedStepText][0]; } + public IEnumerable MethodsFor(string parsedStepText) + { + return _registry.TryGetValue(parsedStepText, out var methods) ? methods : Enumerable.Empty(); + } + public bool HasAlias(string stepValue) { return _registry.ContainsKey(stepValue) && _registry.GetValueOrDefault(stepValue).FirstOrDefault().HasAlias; diff --git a/src/dotnet.json b/src/dotnet.json index c161577..2561ba7 100644 --- a/src/dotnet.json +++ b/src/dotnet.json @@ -1,6 +1,6 @@ { "id": "dotnet", - "version": "0.7.6", + "version": "0.7.7", "description": "C# support for gauge + .NET", "run": { "windows": [ diff --git a/test/Processors/ValidateProcessorTests.cs b/test/Processors/ValidateProcessorTests.cs index b614110..f4ebcbd 100644 --- a/test/Processors/ValidateProcessorTests.cs +++ b/test/Processors/ValidateProcessorTests.cs @@ -4,10 +4,12 @@ * See LICENSE.txt in the project root for license information. *----------------------------------------------------------------*/ +using Gauge.Dotnet.Models; using Gauge.Dotnet.Processors; using Gauge.Dotnet.Registries; using Gauge.Messages; + namespace Gauge.Dotnet.UnitTests.Processors; [TestFixture] @@ -33,14 +35,20 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhenMultipleStepIm _mockStepRegistry.Setup(registry => registry.ContainsStep("step_text_1")).Returns(true); _mockStepRegistry.Setup(registry => registry.HasMultipleImplementations("step_text_1")).Returns(true); + _mockStepRegistry.Setup(registry => registry.MethodsFor("step_text_1")).Returns(new[] + { + new GaugeMethod { Name = "StepImpl", ClassName = "StepsA", FileName = "StepsA.cs", }, + new GaugeMethod { Name = "StepImpl", ClassName = "StepsB", FileName = "StepsB.cs", } + }); var processor = new StepValidationProcessor(_mockStepRegistry.Object); var response = await processor.Process(1, request); ClassicAssert.AreEqual(false, response.IsValid); ClassicAssert.AreEqual(StepValidateResponse.Types.ErrorType.DuplicateStepImplementation, response.ErrorType); - ClassicAssert.AreEqual("Multiple step implementations found for : step_text_1", - response.ErrorMessage); + StringAssert.Contains("Multiple step implementations found for : step_text_1", response.ErrorMessage); + StringAssert.Contains("StepsA.StepImpl in StepsA.cs:1", response.ErrorMessage); + StringAssert.Contains("StepsB.StepImpl in StepsB.cs:1", response.ErrorMessage); ClassicAssert.IsEmpty(response.Suggestion); } diff --git a/test/StepRegistryTests.cs b/test/StepRegistryTests.cs index 47c10ac..bf5796b 100644 --- a/test/StepRegistryTests.cs +++ b/test/StepRegistryTests.cs @@ -177,6 +177,19 @@ public void ShouldCheckIfFileIsCached() ClassicAssert.False(stepRegistry.IsFileCached("Bar.cs")); } + [Test] + public void ShouldDetectGenuineDuplicateSteps() + { + var stepRegistry = new StepRegistry(); + var method1 = new GaugeMethod { Name = "Click", ClassName = "ActionsA", StepText = "Click ", FileName = "ActionsA.cs" }; + var method2 = new GaugeMethod { Name = "Click", ClassName = "ActionsB", StepText = "Click ", FileName = "ActionsB.cs" }; + + stepRegistry.AddStep("Click {}", method1); + stepRegistry.AddStep("Click {}", method2); + + ClassicAssert.True(stepRegistry.HasMultipleImplementations("Click {}")); + } + [Test] public void ShouldNotContainStepPositionForExternalSteps() { From f79bd5007b415befa206b4c4b844dfe3ee033e8e Mon Sep 17 00:00:00 2001 From: jensakejohansson Date: Tue, 31 Mar 2026 23:55:11 +0200 Subject: [PATCH 2/5] Update test to match error msg. Signed-off-by: jensakejohansson --- test/Processors/ValidateProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Processors/ValidateProcessorTests.cs b/test/Processors/ValidateProcessorTests.cs index f4ebcbd..2fef270 100644 --- a/test/Processors/ValidateProcessorTests.cs +++ b/test/Processors/ValidateProcessorTests.cs @@ -46,7 +46,7 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhenMultipleStepIm ClassicAssert.AreEqual(false, response.IsValid); ClassicAssert.AreEqual(StepValidateResponse.Types.ErrorType.DuplicateStepImplementation, response.ErrorType); - StringAssert.Contains("Multiple step implementations found for : step_text_1", response.ErrorMessage); + StringAssert.Contains("Step: step_text_1", response.ErrorMessage); StringAssert.Contains("StepsA.StepImpl in StepsA.cs:1", response.ErrorMessage); StringAssert.Contains("StepsB.StepImpl in StepsB.cs:1", response.ErrorMessage); ClassicAssert.IsEmpty(response.Suggestion); From 0bc30496f548093d44f213ec2253356c53a34a9d Mon Sep 17 00:00:00 2001 From: jensakejohansson Date: Wed, 1 Apr 2026 11:23:25 +0200 Subject: [PATCH 3/5] Make StepRegistry thread-safe, new LookupStep-method contains all info an single atomic snapshot. Signed-off-by: jensakejohansson --- .../ExecutionOrchestratorTests.cs | 16 +- src/Loaders/AssemblyLoader.cs | 8 +- src/Loaders/StaticLoader.cs | 32 ++-- src/Processors/ExecuteStepProcessor.cs | 5 +- src/Processors/RefactorProcessor.cs | 8 +- src/Processors/StepNameProcessor.cs | 16 +- src/Processors/StepValidationProcessor.cs | 9 +- src/Registries/IStepRegistry.cs | 6 +- src/Registries/StepLookupResult.cs | 14 ++ src/Registries/StepRegistry.cs | 140 +++++++++++------- test/Loaders/StaticLoaderTests.cs | 32 ++-- test/Processors/ExecuteStepProcessorTests.cs | 15 +- test/Processors/StepNameProcessorTest.cs | 25 ++-- test/Processors/ValidateProcessorTests.cs | 19 +-- test/StepRegistryTests.cs | 10 +- 15 files changed, 194 insertions(+), 161 deletions(-) create mode 100644 src/Registries/StepLookupResult.cs diff --git a/integration-test/ExecutionOrchestratorTests.cs b/integration-test/ExecutionOrchestratorTests.cs index 1dc68dc..c6f4803 100644 --- a/integration-test/ExecutionOrchestratorTests.cs +++ b/integration-test/ExecutionOrchestratorTests.cs @@ -39,7 +39,7 @@ public async Task RecoverableIsTrueOnExceptionThrownWhenContinueOnFailure() new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); var gaugeMethod = assemblyLoader.GetStepRegistry() - .MethodFor("I throw a serializable exception and continue"); + .LookupStep("I throw a serializable exception and continue").Methods[0]; var executionResult = await orchestrator.ExecuteStep(gaugeMethod, 1); ClassicAssert.IsTrue(executionResult.Failed); ClassicAssert.IsTrue(executionResult.RecoverableError); @@ -63,7 +63,7 @@ public async Task ShouldCreateTableFromTargetType() new HookExecutor(assemblyLoader, executionInfoMapper, hookRegistry, _loggerFactory.CreateLogger()), new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); - var gaugeMethod = assemblyLoader.GetStepRegistry().MethodFor("Step that takes a table {}"); + var gaugeMethod = assemblyLoader.GetStepRegistry().LookupStep("Step that takes a table {}").Methods[0]; var table = new Table(new List { "foo", "bar" }); table.AddRow(new List { "foorow1", "barrow1" }); table.AddRow(new List { "foorow2", "barrow2" }); @@ -91,7 +91,7 @@ public async Task ShouldExecuteMethodAndReturnResult() new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); var gaugeMethod = assemblyLoader.GetStepRegistry() - .MethodFor("A context step which gets executed before every scenario"); + .LookupStep("A context step which gets executed before every scenario").Methods[0]; var executionResult = await orchestrator.ExecuteStep(gaugeMethod, 1); ClassicAssert.False(executionResult.Failed); @@ -117,7 +117,7 @@ public async Task ShouldGetPendingMessages() new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); - var gaugeMethod = assemblyLoader.GetStepRegistry().MethodFor("Say {} to {}"); + var gaugeMethod = assemblyLoader.GetStepRegistry().LookupStep("Say {} to {}").Methods[0]; var executionResult = await executionOrchestrator.ExecuteStep(gaugeMethod, 1, "hello", "world"); @@ -144,7 +144,7 @@ public async Task ShouldGetStacktraceForAggregateException() new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); - var gaugeMethod = assemblyLoader.GetStepRegistry().MethodFor("I throw an AggregateException"); + var gaugeMethod = assemblyLoader.GetStepRegistry().LookupStep("I throw an AggregateException").Methods[0]; var executionResult = await executionOrchestrator.ExecuteStep(gaugeMethod, 1); ClassicAssert.True(executionResult.Failed); @@ -163,7 +163,7 @@ public void ShouldGetStepTextsForMethod() var assemblyLoader = new AssemblyLoader(assemblyLocater, gaugeLoadContext, reflectionWrapper, activatorWrapper, new StepRegistry(), _loggerFactory.CreateLogger(), _configuration); var registry = assemblyLoader.GetStepRegistry(); - var gaugeMethod = registry.MethodFor("and an alias"); + var gaugeMethod = registry.LookupStep("and an alias").Methods[0]; var stepTexts = gaugeMethod.Aliases.ToList(); ClassicAssert.Contains("Step with text", stepTexts); @@ -189,7 +189,7 @@ public async Task SuccessIsFalseOnSerializableExceptionThrown() new HookExecutor(assemblyLoader, executionInfoMapper, hookRegistry, _loggerFactory.CreateLogger()), new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); - var gaugeMethod = assemblyLoader.GetStepRegistry().MethodFor("I throw a serializable exception"); + var gaugeMethod = assemblyLoader.GetStepRegistry().LookupStep("I throw a serializable exception").Methods[0]; var executionResult = await executionOrchestrator.ExecuteStep(gaugeMethod, 1); @@ -219,7 +219,7 @@ public async Task SuccessIsFalseOnUnserializableExceptionThrown() new StepExecutor(assemblyLoader, _loggerFactory.CreateLogger(), executionInfoMapper), _configuration, _loggerFactory.CreateLogger(), dataStoreFactory); - var gaugeMethod = assemblyLoader.GetStepRegistry().MethodFor("I throw an unserializable exception"); + var gaugeMethod = assemblyLoader.GetStepRegistry().LookupStep("I throw an unserializable exception").Methods[0]; var executionResult = await executionOrchestrator.ExecuteStep(gaugeMethod, 1); ClassicAssert.True(executionResult.Failed); ClassicAssert.AreEqual(expectedMessage, executionResult.ErrorMessage); diff --git a/src/Loaders/AssemblyLoader.cs b/src/Loaders/AssemblyLoader.cs index 717c4fd..14fe05d 100644 --- a/src/Loaders/AssemblyLoader.cs +++ b/src/Loaders/AssemblyLoader.cs @@ -87,11 +87,13 @@ public IStepRegistry GetStepRegistry() foreach (var stepText in stepTexts) { var stepValue = GetStepValue(stepText); - if (_registry.ContainsStep(stepValue)) + var lookup = _registry.LookupStep(stepValue); + if (lookup.Exists) { _logger.LogDebug("'{StepValue}': implementation found in StepRegistry, setting reflected methodInfo", stepValue); - _registry.MethodFor(stepValue).MethodInfo = info; - _registry.MethodFor(stepValue).ContinueOnFailure = info.IsRecoverableStep(this); + var existing = lookup.Methods[0]; + existing.MethodInfo = info; + existing.ContinueOnFailure = info.IsRecoverableStep(this); } else { diff --git a/src/Loaders/StaticLoader.cs b/src/Loaders/StaticLoader.cs index 6e69a5a..c7becbd 100644 --- a/src/Loaders/StaticLoader.cs +++ b/src/Loaders/StaticLoader.cs @@ -24,12 +24,6 @@ public sealed class StaticLoader : IStaticLoader private readonly IConfiguration _config; private readonly ILogger _logger; - // Guards all StepRegistry mutations. gRPC dispatches CacheFileRequest messages concurrently on separate threads, - // all sharing this singleton. Without synchronization, concurrent ReloadSteps calls can interleave their - // RemoveSteps/AddStep operations — one thread's dictionary rebuild overwrites another's, leaving stale entries - // that cause false "duplicate step implementation" warnings in the IDE. - private readonly object _registryLock = new object(); - public StaticLoader(IAttributesLoader attributesLoader, IDirectoryWrapper directoryWrapper, IConfiguration config, ILogger logger) { @@ -50,29 +44,20 @@ public IStepRegistry GetStepRegistry() public void LoadStepsFromText(string content, string filepath) { - lock (_registryLock) - { - var steps = GetStepsFrom(content); - AddStepsToRegistry(filepath, steps); - } + var steps = GetStepsFrom(content); + var entries = BuildStepEntries(filepath, steps); + _stepRegistry.ReplaceSteps(filepath, entries); } public void ReloadSteps(string content, string filepath) { if (IsFileRemoved(filepath)) return; - lock (_registryLock) - { - _stepRegistry.RemoveSteps(filepath); - LoadStepsFromText(content, filepath); - } + LoadStepsFromText(content, filepath); } public void RemoveSteps(string file) { - lock (_registryLock) - { - _stepRegistry.RemoveSteps(file); - } + _stepRegistry.RemoveSteps(file); } private bool IsFileRemoved(string file) @@ -109,8 +94,10 @@ public void LoadImplementations() } } - private void AddStepsToRegistry(string fileName, IEnumerable stepMethods) + private static IReadOnlyList<(string stepValue, GaugeMethod method)> BuildStepEntries( + string fileName, IEnumerable stepMethods) { + var entries = new List<(string, GaugeMethod)>(); foreach (var stepMethod in stepMethods) { var attributeListSyntax = stepMethod.AttributeLists.WithStepAttribute(); @@ -135,9 +122,10 @@ private void AddStepsToRegistry(string fileName, IEnumerable GetStepsFrom(string content) diff --git a/src/Processors/ExecuteStepProcessor.cs b/src/Processors/ExecuteStepProcessor.cs index 31ede7c..66d6462 100644 --- a/src/Processors/ExecuteStepProcessor.cs +++ b/src/Processors/ExecuteStepProcessor.cs @@ -29,10 +29,11 @@ public ExecuteStepProcessor(IStepRegistry registry, IExecutionOrchestrator execu [DebuggerHidden] public async Task Process(int streamId, ExecuteStepRequest request) { - if (!_stepRegistry.ContainsStep(request.ParsedStepText)) + var lookup = _stepRegistry.LookupStep(request.ParsedStepText); + if (!lookup.Exists) return ExecutionError("Step Implementation not found"); - var method = _stepRegistry.MethodFor(request.ParsedStepText); + var method = lookup.Methods[0]; var parameters = method.ParameterCount; var args = new string[parameters]; diff --git a/src/Processors/RefactorProcessor.cs b/src/Processors/RefactorProcessor.cs index f884480..db1b9aa 100644 --- a/src/Processors/RefactorProcessor.cs +++ b/src/Processors/RefactorProcessor.cs @@ -85,9 +85,13 @@ private static FileChanges ConvertToProtoFileChanges(RefactoringChange fileChang private GaugeMethod GetGaugeMethod(ProtoStepValue stepValue) { - if (_stepRegistry.HasMultipleImplementations(stepValue.StepValue)) + var lookup = _stepRegistry.LookupStep(stepValue.StepValue); + if (!lookup.Exists) + throw new Exception(string.Format("Step implementation not found for : {0}", + stepValue.ParameterizedStepValue)); + if (lookup.HasMultipleImplementations) throw new Exception(string.Format("Multiple step implementations found for : {0}", stepValue.ParameterizedStepValue)); - return _stepRegistry.MethodFor(stepValue.StepValue); + return lookup.Methods[0]; } } \ No newline at end of file diff --git a/src/Processors/StepNameProcessor.cs b/src/Processors/StepNameProcessor.cs index a52d965..ef43302 100644 --- a/src/Processors/StepNameProcessor.cs +++ b/src/Processors/StepNameProcessor.cs @@ -24,19 +24,17 @@ public Task Process(int stream, StepNameRequest request) { var parsedStepText = request.StepValue; - var isStepPresent = _stepRegistry.ContainsStep(parsedStepText); + var lookup = _stepRegistry.LookupStep(parsedStepText); var response = new StepNameResponse { - IsStepPresent = isStepPresent + IsStepPresent = lookup.Exists }; - if (!isStepPresent) return Task.FromResult(response); + if (!lookup.Exists) return Task.FromResult(response); - var stepText = _stepRegistry.GetStepText(parsedStepText); - var hasAlias = _stepRegistry.HasAlias(stepText); - var info = _stepRegistry.MethodFor(parsedStepText); + var info = lookup.Methods[0]; response.IsExternal = info.IsExternal; - response.HasAlias = hasAlias; + response.HasAlias = info.HasAlias; if (!response.IsExternal) { response.FileName = info.FileName; @@ -49,10 +47,10 @@ public Task Process(int stream, StepNameRequest request) }; } - if (hasAlias) + if (info.HasAlias) response.StepName.AddRange(info.Aliases); else - response.StepName.Add(stepText); + response.StepName.Add(info.StepText); return Task.FromResult(response); } diff --git a/src/Processors/StepValidationProcessor.cs b/src/Processors/StepValidationProcessor.cs index 8a23491..d31f40d 100644 --- a/src/Processors/StepValidationProcessor.cs +++ b/src/Processors/StepValidationProcessor.cs @@ -27,18 +27,19 @@ public Task Process(int stream, StepValidateRequest reques var errorMessage = ""; var suggestion = ""; var errorType = StepValidateResponse.Types.ErrorType.StepImplementationNotFound; - if (!_stepRegistry.ContainsStep(stepToValidate)) + + var lookup = _stepRegistry.LookupStep(stepToValidate); + if (!lookup.Exists) { isValid = false; errorMessage = string.Format("No implementation found for : {0}. Full Step Text :", stepToValidate); suggestion = GetSuggestion(request.StepValue); } - else if (_stepRegistry.HasMultipleImplementations(stepToValidate)) + else if (lookup.HasMultipleImplementations) { isValid = false; errorType = StepValidateResponse.Types.ErrorType.DuplicateStepImplementation; - var implementations = _stepRegistry.MethodsFor(stepToValidate); - var locations = string.Join("\n", implementations.Select(m => + var locations = string.Join("\n", lookup.Methods.Select(m => $" {m.ClassName}.{m.Name} in {m.FileName}:{m.Span.StartLinePosition.Line + 1}")); errorMessage = $"Step: {stepToValidate}\n{locations}"; } diff --git a/src/Registries/IStepRegistry.cs b/src/Registries/IStepRegistry.cs index a0e3287..dd32c7b 100644 --- a/src/Registries/IStepRegistry.cs +++ b/src/Registries/IStepRegistry.cs @@ -11,14 +11,12 @@ namespace Gauge.Dotnet.Registries; public interface IStepRegistry { - bool ContainsStep(string parsedStepText); - GaugeMethod MethodFor(string parsedStepText); - IEnumerable MethodsFor(string parsedStepText); bool HasAlias(string stepText); string GetStepText(string parameterizedStepText); IEnumerable GetStepTexts(); - bool HasMultipleImplementations(string parsedStepText); + StepLookupResult LookupStep(string parsedStepText); void AddStep(string stepValue, GaugeMethod stepMethod); + void ReplaceSteps(string filepath, IReadOnlyList<(string stepValue, GaugeMethod method)> newSteps); void RemoveSteps(string filepath); IEnumerable GetStepPositions(string filePath); bool IsFileCached(string file); diff --git a/src/Registries/StepLookupResult.cs b/src/Registries/StepLookupResult.cs new file mode 100644 index 0000000..6ddd10a --- /dev/null +++ b/src/Registries/StepLookupResult.cs @@ -0,0 +1,14 @@ +/*---------------------------------------------------------------- + * Copyright (c) ThoughtWorks, Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE.txt in the project root for license information. + *----------------------------------------------------------------*/ + +using Gauge.Dotnet.Models; + +namespace Gauge.Dotnet.Registries; + +public readonly record struct StepLookupResult( + bool Exists, + bool HasMultipleImplementations, + IReadOnlyList Methods); diff --git a/src/Registries/StepRegistry.cs b/src/Registries/StepRegistry.cs index 2f89012..ecc3967 100644 --- a/src/Registries/StepRegistry.cs +++ b/src/Registries/StepRegistry.cs @@ -13,6 +13,7 @@ namespace Gauge.Dotnet.Registries; [Serializable] public class StepRegistry : IStepRegistry { + private readonly object _lock = new(); private Dictionary> _registry; public StepRegistry() @@ -20,108 +21,137 @@ public StepRegistry() _registry = new Dictionary>(); } - public int Count => _registry.Count; + public int Count { get { lock (_lock) { return _registry.Count; } } } public IEnumerable GetStepTexts() { - return _registry.Values.SelectMany(methods => methods.Select(method => method.StepText)); + lock (_lock) + { + return _registry.Values.SelectMany(methods => methods.Select(method => method.StepText)).ToList(); + } } public void AddStep(string stepValue, GaugeMethod method) { - if (!_registry.ContainsKey(stepValue)) _registry.Add(stepValue, new List()); - _registry.GetValueOrDefault(stepValue).Add(method); + lock (_lock) + { + if (!_registry.ContainsKey(stepValue)) _registry.Add(stepValue, new List()); + _registry.GetValueOrDefault(stepValue).Add(method); + } } public void RemoveSteps(string filepath) { - var newRegistry = new Dictionary>(); - foreach (var (key, gaugeMethods) in _registry) + lock (_lock) { - var methods = gaugeMethods.Where(method => !filepath.Equals(method.FileName)).ToList(); - if (methods.Count > 0) newRegistry[key] = methods; + RemoveStepsInternal(filepath); } + } - _registry = newRegistry; + public void ReplaceSteps(string filepath, IReadOnlyList<(string stepValue, GaugeMethod method)> newSteps) + { + lock (_lock) + { + RemoveStepsInternal(filepath); + foreach (var (stepValue, method) in newSteps) + AddStepInternal(stepValue, method); + } + } + + public StepLookupResult LookupStep(string parsedStepText) + { + lock (_lock) + { + if (!_registry.TryGetValue(parsedStepText, out var methods)) + return new StepLookupResult(false, false, Array.Empty()); + return new StepLookupResult(true, methods.Count > 1, methods.ToList()); + } } public IEnumerable GetStepPositions(string filePath) { - var positions = new List(); - foreach (var (stepValue, gaugeMethods) in _registry) + lock (_lock) { - foreach (var m in gaugeMethods) + var positions = new List(); + foreach (var (stepValue, gaugeMethods) in _registry) { - if (!m.IsExternal && m.FileName.Equals(filePath)) + foreach (var m in gaugeMethods) { - var p = new StepPosition + if (!m.IsExternal && m.FileName.Equals(filePath)) { - StepValue = stepValue, - Span = new Span + var p = new StepPosition { - Start = m.Span.StartLinePosition.Line + 1, - StartChar = m.Span.StartLinePosition.Character, - End = m.Span.EndLinePosition.Line + 1, - EndChar = m.Span.EndLinePosition.Character - } - }; - positions.Add(p); + StepValue = stepValue, + Span = new Span + { + Start = m.Span.StartLinePosition.Line + 1, + StartChar = m.Span.StartLinePosition.Character, + End = m.Span.EndLinePosition.Line + 1, + EndChar = m.Span.EndLinePosition.Character + } + }; + positions.Add(p); + } } } + return positions; } - - return positions; - } - - - public bool ContainsStep(string parsedStepText) - { - return _registry.ContainsKey(parsedStepText); - } - - public bool HasMultipleImplementations(string parsedStepText) - { - return _registry[parsedStepText].Count > 1; - } - - public GaugeMethod MethodFor(string parsedStepText) - { - return _registry[parsedStepText][0]; - } - - public IEnumerable MethodsFor(string parsedStepText) - { - return _registry.TryGetValue(parsedStepText, out var methods) ? methods : Enumerable.Empty(); } public bool HasAlias(string stepValue) { - return _registry.ContainsKey(stepValue) && _registry.GetValueOrDefault(stepValue).FirstOrDefault().HasAlias; + lock (_lock) + { + return _registry.ContainsKey(stepValue) && _registry.GetValueOrDefault(stepValue).FirstOrDefault().HasAlias; + } } public string GetStepText(string stepValue) { - return _registry.ContainsKey(stepValue) ? _registry[stepValue][0].StepText : string.Empty; + lock (_lock) + { + return _registry.ContainsKey(stepValue) ? _registry[stepValue][0].StepText : string.Empty; + } } public void Clear() { - _registry = new Dictionary>(); + lock (_lock) { _registry = new Dictionary>(); } } - public IEnumerable AllSteps() { - return _registry.Keys; + lock (_lock) { return _registry.Keys.ToList(); } } public bool IsFileCached(string file) { - foreach (var gaugeMethods in _registry.Values) + lock (_lock) { - if (gaugeMethods.Any(method => file.Equals(method.FileName))) - return true; + foreach (var gaugeMethods in _registry.Values) + { + if (gaugeMethods.Any(method => file.Equals(method.FileName))) + return true; + } + return false; } - return false; + } + + private void RemoveStepsInternal(string filepath) + { + var newRegistry = new Dictionary>(); + foreach (var (key, gaugeMethods) in _registry) + { + var methods = gaugeMethods.Where(method => !filepath.Equals(method.FileName)).ToList(); + if (methods.Count > 0) newRegistry[key] = methods; + } + _registry = newRegistry; + } + + private void AddStepInternal(string stepValue, GaugeMethod method) + { + if (!_registry.ContainsKey(stepValue)) + _registry.Add(stepValue, new List()); + _registry.GetValueOrDefault(stepValue).Add(method); } } \ No newline at end of file diff --git a/test/Loaders/StaticLoaderTests.cs b/test/Loaders/StaticLoaderTests.cs index 22d8cde..22f1aff 100644 --- a/test/Loaders/StaticLoaderTests.cs +++ b/test/Loaders/StaticLoaderTests.cs @@ -51,9 +51,9 @@ public void ShouldAddAliasesSteps() loader.LoadStepsFromText(text, fileName); var registry = loader.GetStepRegistry(); - ClassicAssert.True(registry.ContainsStep("goodbye")); - ClassicAssert.True(registry.ContainsStep("adieu")); - ClassicAssert.True(registry.ContainsStep("sayonara")); + ClassicAssert.True(registry.LookupStep("goodbye").Exists); + ClassicAssert.True(registry.LookupStep("adieu").Exists); + ClassicAssert.True(registry.LookupStep("sayonara").Exists); ClassicAssert.AreEqual(3, registry.Count); } @@ -79,7 +79,7 @@ public void ShouldAddStepsFromGivenContent() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -156,7 +156,7 @@ public void ShouldNotReloadStepOfRemovedFile() const string fileName = @"foo.cs"; var filePath = Path.Combine(currentDirectory, fileName); loader.ReloadSteps(text, filePath); - ClassicAssert.False(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.False(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(0, loader.GetStepRegistry().Count); } @@ -181,7 +181,7 @@ public void ShouldReloadSteps() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); const string newText = "using Gauge.CSharp.Lib.Attributes;\n" + @@ -202,7 +202,7 @@ public void ShouldReloadSteps() loader.ReloadSteps(newText, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hola")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hola").Exists); ClassicAssert.AreEqual(2, loader.GetStepRegistry().Count); } @@ -245,13 +245,13 @@ public void ShouldRemoveSteps() loader.ReloadSteps(newText, file2); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hola")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hola").Exists); ClassicAssert.AreEqual(2, loader.GetStepRegistry().Count); loader.RemoveSteps(file2); - ClassicAssert.False(loader.GetStepRegistry().ContainsStep("hola")); + ClassicAssert.False(loader.GetStepRegistry().LookupStep("hola").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -278,7 +278,7 @@ public void ShouldFindStepAttribute_WhenUsingQualifiedName() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -304,7 +304,7 @@ public void ShouldFindStepAttribute_WhenUsingFullTypeName() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -332,7 +332,7 @@ public void ShouldFindStepAttribute_WhenStepIsNotFirst() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -361,7 +361,7 @@ public void ShouldFindStepAttribute_WhenStepIsAfterMultipleAttributes() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -392,7 +392,7 @@ public void ShouldIgnoreMethodsWithoutStepAttribute_InFileWithStepAttributes() "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } @@ -427,7 +427,7 @@ public void ShouldHandleMethodsWithOtherAttributesButNoStep_InFileWithStepAttrib "}\n"; const string fileName = @"foo.cs"; loader.LoadStepsFromText(text, fileName); - ClassicAssert.True(loader.GetStepRegistry().ContainsStep("hello")); + ClassicAssert.True(loader.GetStepRegistry().LookupStep("hello").Exists); ClassicAssert.AreEqual(1, loader.GetStepRegistry().Count); } diff --git a/test/Processors/ExecuteStepProcessorTests.cs b/test/Processors/ExecuteStepProcessorTests.cs index 1c5b3f1..990bce3 100644 --- a/test/Processors/ExecuteStepProcessorTests.cs +++ b/test/Processors/ExecuteStepProcessorTests.cs @@ -41,9 +41,9 @@ public async Task ShouldProcessExecuteStepRequest() } }; var mockStepRegistry = new Mock(); - mockStepRegistry.Setup(x => x.ContainsStep(parsedStepText)).Returns(true); var fooMethodInfo = new GaugeMethod { Name = "Foo", ParameterCount = 1 }; - mockStepRegistry.Setup(x => x.MethodFor(parsedStepText)).Returns(fooMethodInfo); + mockStepRegistry.Setup(x => x.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { fooMethodInfo })); var mockOrchestrator = new Mock(); mockOrchestrator.Setup(e => e.ExecuteStep(fooMethodInfo, It.IsAny(), It.IsAny())) .ReturnsAsync(() => new ProtoExecutionResult { ExecutionTime = 1, Failed = false }); @@ -79,9 +79,9 @@ public async Task ShouldProcessExecuteStepRequestForTableParam(Parameter.Types.P }; var mockStepRegistry = new Mock(); - mockStepRegistry.Setup(x => x.ContainsStep(parsedStepText)).Returns(true); var fooMethodInfo = new GaugeMethod { Name = "Foo", ParameterCount = 1 }; - mockStepRegistry.Setup(x => x.MethodFor(parsedStepText)).Returns(fooMethodInfo); + mockStepRegistry.Setup(x => x.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { fooMethodInfo })); var mockOrchestrator = new Mock(); mockOrchestrator.Setup(e => e.ExecuteStep(fooMethodInfo, It.IsAny(), It.IsAny())).ReturnsAsync(() => new ProtoExecutionResult @@ -113,9 +113,9 @@ public async Task ShouldReportArgumentMismatch() ParsedStepText = parsedStepText }; var mockStepRegistry = new Mock(); - mockStepRegistry.Setup(x => x.ContainsStep(parsedStepText)).Returns(true); var fooMethod = new GaugeMethod { Name = "Foo", ParameterCount = 1 }; - mockStepRegistry.Setup(x => x.MethodFor(parsedStepText)).Returns(fooMethod); + mockStepRegistry.Setup(x => x.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { fooMethod })); var mockOrchestrator = new Mock(); var mockTableFormatter = new Mock(); @@ -138,7 +138,8 @@ public async Task ShouldReportMissingStep() ParsedStepText = parsedStepText }; var mockStepRegistry = new Mock(); - mockStepRegistry.Setup(x => x.ContainsStep(parsedStepText)).Returns(false); + mockStepRegistry.Setup(x => x.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(false, false, Array.Empty())); var mockOrchestrator = new Mock(); var mockTableFormatter = new Mock(); diff --git a/test/Processors/StepNameProcessorTest.cs b/test/Processors/StepNameProcessorTest.cs index 3c0feb3..e660ec5 100644 --- a/test/Processors/StepNameProcessorTest.cs +++ b/test/Processors/StepNameProcessorTest.cs @@ -19,15 +19,13 @@ public async Task ShouldProcessStepNameRequest() }; var parsedStepText = request.StepValue; - const string stepText = "step1"; - mockStepRegistry.Setup(r => r.ContainsStep(parsedStepText)).Returns(true); - mockStepRegistry.Setup(r => r.GetStepText(parsedStepText)).Returns(stepText); var gaugeMethod = new GaugeMethod { - FileName = "foo" + FileName = "foo", + StepText = "step1" }; - mockStepRegistry.Setup(r => r.MethodFor(parsedStepText)).Returns(gaugeMethod); - mockStepRegistry.Setup(r => r.HasAlias(stepText)).Returns(false); + mockStepRegistry.Setup(r => r.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { gaugeMethod })); var stepNameProcessor = new StepNameProcessor(mockStepRegistry.Object); var response = await stepNameProcessor.Process(1, request); @@ -46,18 +44,16 @@ public async Task ShouldProcessStepNameWithAliasRequest() StepValue = "step1" }; var parsedStepText = request.StepValue; - const string stepText = "step1"; - mockStepRegistry.Setup(r => r.ContainsStep(parsedStepText)).Returns(true); - mockStepRegistry.Setup(r => r.GetStepText(parsedStepText)).Returns(stepText); var gaugeMethod = new GaugeMethod { FileName = "foo", + StepText = "step1", HasAlias = true, Aliases = new List { "step2", "step3" } }; - mockStepRegistry.Setup(r => r.MethodFor(parsedStepText)).Returns(gaugeMethod); - mockStepRegistry.Setup(r => r.HasAlias(stepText)).Returns(true); + mockStepRegistry.Setup(r => r.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { gaugeMethod })); var stepNameProcessor = new StepNameProcessor(mockStepRegistry.Object); var response = await stepNameProcessor.Process(1, request); @@ -77,16 +73,15 @@ public async Task ShouldProcessExternalSteps() StepValue = "step1" }; var parsedStepText = request.StepValue; - const string stepText = "step1"; - mockStepRegistry.Setup(r => r.ContainsStep(parsedStepText)).Returns(true); - mockStepRegistry.Setup(r => r.GetStepText(parsedStepText)).Returns(stepText); var gaugeMethod = new GaugeMethod { FileName = "foo", + StepText = "step1", IsExternal = true }; - mockStepRegistry.Setup(r => r.MethodFor(parsedStepText)).Returns(gaugeMethod); + mockStepRegistry.Setup(r => r.LookupStep(parsedStepText)) + .Returns(new StepLookupResult(true, false, new[] { gaugeMethod })); var stepNameProcessor = new StepNameProcessor(mockStepRegistry.Object); var response = await stepNameProcessor.Process(1, request); diff --git a/test/Processors/ValidateProcessorTests.cs b/test/Processors/ValidateProcessorTests.cs index 2fef270..3aaebb1 100644 --- a/test/Processors/ValidateProcessorTests.cs +++ b/test/Processors/ValidateProcessorTests.cs @@ -33,13 +33,12 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhenMultipleStepIm NumberOfParameters = 0 }; - _mockStepRegistry.Setup(registry => registry.ContainsStep("step_text_1")).Returns(true); - _mockStepRegistry.Setup(registry => registry.HasMultipleImplementations("step_text_1")).Returns(true); - _mockStepRegistry.Setup(registry => registry.MethodsFor("step_text_1")).Returns(new[] - { - new GaugeMethod { Name = "StepImpl", ClassName = "StepsA", FileName = "StepsA.cs", }, - new GaugeMethod { Name = "StepImpl", ClassName = "StepsB", FileName = "StepsB.cs", } - }); + _mockStepRegistry.Setup(registry => registry.LookupStep("step_text_1")).Returns( + new StepLookupResult(true, true, new[] + { + new GaugeMethod { Name = "StepImpl", ClassName = "StepsA", FileName = "StepsA.cs" }, + new GaugeMethod { Name = "StepImpl", ClassName = "StepsB", FileName = "StepsB.cs" } + })); var processor = new StepValidationProcessor(_mockStepRegistry.Object); var response = await processor.Process(1, request); @@ -65,6 +64,8 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhennNoImplFound() StepValue = "step_text_1" } }; + _mockStepRegistry.Setup(registry => registry.LookupStep("step_text_1")).Returns( + new StepLookupResult(false, false, Array.Empty())); var processor = new StepValidationProcessor(_mockStepRegistry.Object); var response = await processor.Process(1, request); @@ -86,8 +87,8 @@ public async Task ShouldGetVaildResponseForStepValidateRequest() NumberOfParameters = 0 }; - _mockStepRegistry.Setup(registry => registry.ContainsStep("step_text_1")).Returns(true); - _mockStepRegistry.Setup(registry => registry.HasMultipleImplementations("step_text_1")).Returns(false); + _mockStepRegistry.Setup(registry => registry.LookupStep("step_text_1")).Returns( + new StepLookupResult(true, false, new[] { new GaugeMethod { Name = "StepImpl" } })); var processor = new StepValidationProcessor(_mockStepRegistry.Object); var response = await processor.Process(1, request); diff --git a/test/StepRegistryTests.cs b/test/StepRegistryTests.cs index bf5796b..ff03d9c 100644 --- a/test/StepRegistryTests.cs +++ b/test/StepRegistryTests.cs @@ -25,8 +25,8 @@ public void ShouldContainMethodForStepDefined() foreach (var pair in methods) stepRegistry.AddStep(pair.Key, pair.Value); - ClassicAssert.True(stepRegistry.ContainsStep("Foo")); - ClassicAssert.True(stepRegistry.ContainsStep("Bar")); + ClassicAssert.True(stepRegistry.LookupStep("Foo").Exists); + ClassicAssert.True(stepRegistry.LookupStep("Bar").Exists); } [Test] @@ -104,7 +104,7 @@ public void ShouldGetMethodForStep() foreach (var pair in methods) stepRegistry.AddStep(pair.Key, pair.Value); - var method = stepRegistry.MethodFor("Foo"); + var method = stepRegistry.LookupStep("Foo").Methods[0]; ClassicAssert.AreEqual(method.Name, "Foo"); } @@ -164,7 +164,7 @@ public void ShouldRemoveStepsDefinedInAGivenFile() stepRegistry.AddStep(pair.Key, pair.Value); stepRegistry.RemoveSteps("Foo.cs"); - ClassicAssert.False(stepRegistry.ContainsStep("Foo")); + ClassicAssert.False(stepRegistry.LookupStep("Foo").Exists); } [Test] @@ -187,7 +187,7 @@ public void ShouldDetectGenuineDuplicateSteps() stepRegistry.AddStep("Click {}", method1); stepRegistry.AddStep("Click {}", method2); - ClassicAssert.True(stepRegistry.HasMultipleImplementations("Click {}")); + ClassicAssert.True(stepRegistry.LookupStep("Click {}").HasMultipleImplementations); } [Test] From 77165f73dbad20c65666289aeb2847a71ed233fb Mon Sep 17 00:00:00 2001 From: jensakejohansson Date: Wed, 1 Apr 2026 20:48:26 +0200 Subject: [PATCH 4/5] Adding logging so duplicate information is shown in gauge.log and lsp.log Signed-off-by: jensakejohansson --- src/Processors/StepValidationProcessor.cs | 11 ++++++++++- test/Processors/ValidateProcessorTests.cs | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Processors/StepValidationProcessor.cs b/src/Processors/StepValidationProcessor.cs index d31f40d..c9881ff 100644 --- a/src/Processors/StepValidationProcessor.cs +++ b/src/Processors/StepValidationProcessor.cs @@ -8,16 +8,19 @@ using Gauge.Dotnet.Extensions; using Gauge.Dotnet.Registries; using Gauge.Messages; +using Microsoft.Extensions.Logging; namespace Gauge.Dotnet.Processors; public class StepValidationProcessor : IGaugeProcessor { private readonly IStepRegistry _stepRegistry; + private readonly ILogger _logger; - public StepValidationProcessor(IStepRegistry stepRegistry) + public StepValidationProcessor(IStepRegistry stepRegistry, ILogger logger) { _stepRegistry = stepRegistry; + _logger = logger; } public Task Process(int stream, StepValidateRequest request) @@ -42,6 +45,12 @@ public Task Process(int stream, StepValidateRequest reques var locations = string.Join("\n", lookup.Methods.Select(m => $" {m.ClassName}.{m.Name} in {m.FileName}:{m.Span.StartLinePosition.Line + 1}")); errorMessage = $"Step: {stepToValidate}\n{locations}"; + _logger.LogDebug("Duplicate step implementation found for: {StepText}", stepToValidate); + foreach (var m in lookup.Methods) + { + _logger.LogDebug("Duplicate: {ClassName}.{MethodName} in {FileName}:{Line}", + m.ClassName, m.Name, m.FileName, m.Span.StartLinePosition.Line + 1); + } } return Task.FromResult(GetStepValidateResponseMessage(isValid, errorType, errorMessage, suggestion)); } diff --git a/test/Processors/ValidateProcessorTests.cs b/test/Processors/ValidateProcessorTests.cs index 3aaebb1..c9246fc 100644 --- a/test/Processors/ValidateProcessorTests.cs +++ b/test/Processors/ValidateProcessorTests.cs @@ -8,6 +8,7 @@ using Gauge.Dotnet.Processors; using Gauge.Dotnet.Registries; using Gauge.Messages; +using Microsoft.Extensions.Logging; namespace Gauge.Dotnet.UnitTests.Processors; @@ -39,7 +40,7 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhenMultipleStepIm new GaugeMethod { Name = "StepImpl", ClassName = "StepsA", FileName = "StepsA.cs" }, new GaugeMethod { Name = "StepImpl", ClassName = "StepsB", FileName = "StepsB.cs" } })); - var processor = new StepValidationProcessor(_mockStepRegistry.Object); + var processor = new StepValidationProcessor(_mockStepRegistry.Object, Mock.Of>()); var response = await processor.Process(1, request); ClassicAssert.AreEqual(false, response.IsValid); @@ -66,7 +67,7 @@ public async Task ShouldGetErrorResponseForStepValidateRequestWhennNoImplFound() }; _mockStepRegistry.Setup(registry => registry.LookupStep("step_text_1")).Returns( new StepLookupResult(false, false, Array.Empty())); - var processor = new StepValidationProcessor(_mockStepRegistry.Object); + var processor = new StepValidationProcessor(_mockStepRegistry.Object, Mock.Of>()); var response = await processor.Process(1, request); ClassicAssert.AreEqual(false, response.IsValid); @@ -90,7 +91,7 @@ public async Task ShouldGetVaildResponseForStepValidateRequest() _mockStepRegistry.Setup(registry => registry.LookupStep("step_text_1")).Returns( new StepLookupResult(true, false, new[] { new GaugeMethod { Name = "StepImpl" } })); - var processor = new StepValidationProcessor(_mockStepRegistry.Object); + var processor = new StepValidationProcessor(_mockStepRegistry.Object, Mock.Of>()); var response = await processor.Process(1, request); ClassicAssert.AreEqual(true, response.IsValid); From f48803753e23f46e3451a31c730a50cb1a5d5c1a Mon Sep 17 00:00:00 2001 From: jensakejohansson Date: Thu, 2 Apr 2026 08:37:51 +0200 Subject: [PATCH 5/5] Updated test to inject logger. Signed-off-by: jensakejohansson --- integration-test/ExternalReferenceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/ExternalReferenceTests.cs b/integration-test/ExternalReferenceTests.cs index fd65db2..78bcd0d 100644 --- a/integration-test/ExternalReferenceTests.cs +++ b/integration-test/ExternalReferenceTests.cs @@ -40,7 +40,7 @@ public async Task ShouldGetStepsFromReference(string referenceType, string stepT var assemblyLoader = new AssemblyLoader(assemblyLocater, gaugeLoadContext, new ReflectionWrapper(), new ActivatorWrapper(serviceProvider), new StepRegistry(), _loggerFactory.CreateLogger(), config); - var stepValidationProcessor = new StepValidationProcessor(assemblyLoader.GetStepRegistry()); + var stepValidationProcessor = new StepValidationProcessor(assemblyLoader.GetStepRegistry(), _loggerFactory.CreateLogger()); var message = new StepValidateRequest { StepText = stepText,