diff --git a/Reqnroll.MessagesLogger.TestLogger/FormatterLoggerBase.cs b/Reqnroll.MessagesLogger.TestLogger/FormatterLoggerBase.cs new file mode 100644 index 000000000..d42ad85d5 --- /dev/null +++ b/Reqnroll.MessagesLogger.TestLogger/FormatterLoggerBase.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; + +namespace Reqnroll.FormatterTestLogger; + +public abstract class FormatterLoggerBase : ITestLoggerWithParameters +{ + public bool IsInitialized { get; private set; } + public Dictionary Parameters { get; private set; } = new(); + public string TestRunDirectory { get; private set; } = string.Empty; + + private string _originalEnvValue; + private bool _envSetByLogger; + private bool _subscribed; + protected string FormatterName = null; + + // Meta-keys to ignore for formatter environment variable + // ReSharper disable once UnusedMember.Global + protected static readonly HashSet MetaKeys = new(StringComparer.OrdinalIgnoreCase) + { + "TestRunDirectory", "TargetFramework" + }; + + private void SetFormattersEnvironmentVariable(Dictionary parameters, string testRunDirectory) + { + string effectiveTestRunDirectory = null; + if (parameters != null && parameters.TryGetValue("testRunDirectory", out var paramTestRunDir) && !string.IsNullOrEmpty(paramTestRunDir)) + { + effectiveTestRunDirectory = paramTestRunDir; + } + else if (!string.IsNullOrEmpty(testRunDirectory)) + { + effectiveTestRunDirectory = testRunDirectory; + } + + if (parameters != null) + { + var outputFilePath = parameters.TryGetValue("outputFilePath", out string outputFilePathParameter) ? outputFilePathParameter : null; + var alternateFilePath = parameters.TryGetValue("LogFileName", out string alternateFilePathParameter) ? alternateFilePathParameter : null; + outputFilePath ??= alternateFilePath; + + if (string.IsNullOrEmpty(FormatterName)) + { + return; // No formatter name provided, nothing to do + } + if (!string.IsNullOrEmpty(effectiveTestRunDirectory) && !string.IsNullOrEmpty(outputFilePath)) + { + outputFilePath = Path.Combine(effectiveTestRunDirectory, outputFilePath); + } + else if (!string.IsNullOrEmpty(effectiveTestRunDirectory) && string.IsNullOrEmpty(outputFilePath)) + { + outputFilePath = effectiveTestRunDirectory; + } + + // Use proper JSON serialization to handle escaping correctly + var configObject = new + { + formatters = new Dictionary + { + [FormatterName] = new + { + outputFilePath + } + } + }; + + var json = JsonSerializer.Serialize(configObject); + + if (_originalEnvValue == null) + { + _originalEnvValue = Environment.GetEnvironmentVariable($"REQNROLL_FORMATTERS_LOGGER_{FormatterName}"); + } + Environment.SetEnvironmentVariable($"REQNROLL_FORMATTERS_LOGGER_{FormatterName}", json); + _envSetByLogger = true; + } + } + + private void SubscribeToTestRunComplete(TestLoggerEvents events) + { + if (!_subscribed && events != null) + { + events.TestRunComplete += (_, _) => + { + if (_envSetByLogger) + { + Environment.SetEnvironmentVariable($"REQNROLL_FORMATTERS_LOGGER_{FormatterName}", _originalEnvValue); + _envSetByLogger = false; + } + }; + _subscribed = true; + } + } + + public void Initialize(TestLoggerEvents events, Dictionary parameters) + { + SubscribeToTestRunComplete(events); + IsInitialized = true; + Parameters = parameters; + SetFormattersEnvironmentVariable(parameters, TestRunDirectory); + } + + public void Initialize(TestLoggerEvents events, string testRunDirectory) + { + SubscribeToTestRunComplete(events); + TestRunDirectory = testRunDirectory; + IsInitialized = true; + SetFormattersEnvironmentVariable(Parameters, testRunDirectory); + } +} diff --git a/Reqnroll.MessagesLogger.TestLogger/HtmlFormatterLogger.cs b/Reqnroll.MessagesLogger.TestLogger/HtmlFormatterLogger.cs new file mode 100644 index 000000000..79ffaa6c9 --- /dev/null +++ b/Reqnroll.MessagesLogger.TestLogger/HtmlFormatterLogger.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Reqnroll.FormatterTestLogger; + +[FriendlyName("html-formatter")] +[ExtensionUri("logger://formatterhtmllogger")] +public class HtmlFormatterLogger : FormatterLoggerBase +{ + public HtmlFormatterLogger() + { + FormatterName = "html"; + } +} diff --git a/Reqnroll.MessagesLogger.TestLogger/MessageFormatterLogger.cs b/Reqnroll.MessagesLogger.TestLogger/MessageFormatterLogger.cs new file mode 100644 index 000000000..4df2ef262 --- /dev/null +++ b/Reqnroll.MessagesLogger.TestLogger/MessageFormatterLogger.cs @@ -0,0 +1,13 @@ +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Reqnroll.FormatterTestLogger; + +[FriendlyName("message-formatter")] +[ExtensionUri("logger://formattermessagelogger")] +public class MessageFormatterLogger : FormatterLoggerBase +{ + public MessageFormatterLogger() + { + FormatterName = "message"; + } +} diff --git a/Reqnroll.MessagesLogger.TestLogger/Reqnroll.FormatterTestLogger.csproj b/Reqnroll.MessagesLogger.TestLogger/Reqnroll.FormatterTestLogger.csproj new file mode 100644 index 000000000..588f27a35 --- /dev/null +++ b/Reqnroll.MessagesLogger.TestLogger/Reqnroll.FormatterTestLogger.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + Reqnroll.FormatterTestLogger + Reqnroll.FormatterTestLogger + $(Reqnroll_KeyFile) + $(Reqnroll_EnableStrongNameSigning) + $(Reqnroll_PublicSign) + + + + + + + + diff --git a/Reqnroll.sln b/Reqnroll.sln index ce82a61e8..796050b9f 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reqnroll.TUnit.Generator.Re EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reqnroll.Formatters.Tests", "Tests\Reqnroll.Formatters.Tests\Reqnroll.Formatters.Tests.csproj", "{90C5D62C-CE31-2F54-BEF9-F0DA12C8CE19}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reqnroll.FormatterTestLogger", "Reqnroll.MessagesLogger.TestLogger\Reqnroll.FormatterTestLogger.csproj", "{DA774F90-8287-4364-A736-B3AD94404A5B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -269,6 +271,10 @@ Global {90C5D62C-CE31-2F54-BEF9-F0DA12C8CE19}.Debug|Any CPU.Build.0 = Debug|Any CPU {90C5D62C-CE31-2F54-BEF9-F0DA12C8CE19}.Release|Any CPU.ActiveCfg = Release|Any CPU {90C5D62C-CE31-2F54-BEF9-F0DA12C8CE19}.Release|Any CPU.Build.0 = Release|Any CPU + {DA774F90-8287-4364-A736-B3AD94404A5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA774F90-8287-4364-A736-B3AD94404A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA774F90-8287-4364-A736-B3AD94404A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA774F90-8287-4364-A736-B3AD94404A5B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs index 4ac9ebb9f..c05aabb71 100644 --- a/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/EnvironmentWrapper.cs @@ -1,5 +1,8 @@ -using System; using Reqnroll.CommonModels; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; namespace Reqnroll.EnvironmentAccess { @@ -36,5 +39,17 @@ public void SetEnvironmentVariable(string name, string value) } public string GetCurrentDirectory() => Environment.CurrentDirectory; + + public IDictionary GetEnvironmentVariables(string prefix) + { + if (string.IsNullOrEmpty(prefix)) + throw new ArgumentException("Argument cannot be null or empty", nameof(prefix)); + + return Environment.GetEnvironmentVariables() + .OfType() + .Select(e => (Key: e.Key?.ToString() ?? "", Value: e.Value?.ToString() ?? "")) + .Where(e => e.Key.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + .ToDictionary(e => e.Key, e => e.Value); + } } } diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs index 5e88ba8bc..590c69e0a 100644 --- a/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs +++ b/Reqnroll/EnvironmentAccess/IEnvironmentWrapper.cs @@ -1,4 +1,5 @@ using Reqnroll.CommonModels; +using System.Collections.Generic; namespace Reqnroll.EnvironmentAccess { @@ -10,6 +11,8 @@ public interface IEnvironmentWrapper IResult GetEnvironmentVariable(string name); + IDictionary GetEnvironmentVariables(string prefix); + void SetEnvironmentVariable(string name, string value); string GetCurrentDirectory(); diff --git a/Reqnroll/Formatters/Configuration/EnvironmentConfigurationResolver.cs b/Reqnroll/Formatters/Configuration/EnvironmentConfigurationResolver.cs index 9bf011eb5..bdfe6fac1 100644 --- a/Reqnroll/Formatters/Configuration/EnvironmentConfigurationResolver.cs +++ b/Reqnroll/Formatters/Configuration/EnvironmentConfigurationResolver.cs @@ -10,6 +10,7 @@ public class EnvironmentConfigurationResolver : FormattersConfigurationResolverB { private readonly IEnvironmentWrapper _environmentWrapper; private readonly IFormatterLog _log; + private readonly string _environmentVariableName; public EnvironmentConfigurationResolver( IEnvironmentWrapper environmentWrapper, @@ -17,19 +18,30 @@ public EnvironmentConfigurationResolver( { _environmentWrapper = environmentWrapper; _log = log; + _environmentVariableName = FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE; + } + + internal EnvironmentConfigurationResolver( + IEnvironmentWrapper environmentWrapper, + string environmentVariableName, + IFormatterLog log = null) + { + _environmentWrapper = environmentWrapper ?? throw new ArgumentNullException(nameof(environmentWrapper)); + _log = log; + _environmentVariableName = environmentVariableName ?? throw new ArgumentNullException(nameof(environmentVariableName)); } protected override JsonDocument GetJsonDocument() { try { - var formatters = _environmentWrapper.GetEnvironmentVariable(FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE); + var formatters = _environmentWrapper.GetEnvironmentVariable(_environmentVariableName); if (formatters is Success formattersSuccess) { if (string.IsNullOrWhiteSpace(formattersSuccess.Result)) { - _log?.WriteMessage($"Environment variable {FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE} is empty"); + _log?.WriteMessage($"Environment variable {_environmentVariableName} is empty"); return null; } @@ -43,12 +55,12 @@ protected override JsonDocument GetJsonDocument() } catch (JsonException ex) { - _log?.WriteMessage($"Failed to parse JSON from environment variable {FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE}: {ex.Message}"); + _log?.WriteMessage($"Failed to parse JSON from environment variable {_environmentVariableName}: {ex.Message}"); } } else if (formatters is Failure failure) { - _log?.WriteMessage($"Could not retrieve environment variable {FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE}: {failure.Description}"); + _log?.WriteMessage($"Could not retrieve environment variable {_environmentVariableName}: {failure.Description}"); } } catch (Exception ex) when (ex is not JsonException) diff --git a/Reqnroll/Formatters/Configuration/FormattersConfigurationConstants.cs b/Reqnroll/Formatters/Configuration/FormattersConfigurationConstants.cs index 1645d5202..392ebe3da 100644 --- a/Reqnroll/Formatters/Configuration/FormattersConfigurationConstants.cs +++ b/Reqnroll/Formatters/Configuration/FormattersConfigurationConstants.cs @@ -4,4 +4,5 @@ public static class FormattersConfigurationConstants { public const string REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE = "REQNROLL_FORMATTERS"; public const string REQNROLL_FORMATTERS_DISABLED_ENVIRONMENT_VARIABLE = "REQNROLL_FORMATTERS_DISABLED"; + public const string REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX = "REQNROLL_FORMATTERS_LOGGER_"; } \ No newline at end of file diff --git a/Reqnroll/Formatters/Configuration/FormattersConfigurationProvider.cs b/Reqnroll/Formatters/Configuration/FormattersConfigurationProvider.cs index e9cffe53f..07eb3be80 100644 --- a/Reqnroll/Formatters/Configuration/FormattersConfigurationProvider.cs +++ b/Reqnroll/Formatters/Configuration/FormattersConfigurationProvider.cs @@ -15,15 +15,16 @@ namespace Reqnroll.Formatters.Configuration; /// public class FormattersConfigurationProvider : IFormattersConfigurationProvider { - private readonly IList _resolvers; + private readonly List _resolvers; private readonly Lazy _resolvedConfiguration; private readonly IFormattersConfigurationDisableOverrideProvider _envVariableDisableFlagProvider; public bool Enabled => _resolvedConfiguration.Value.Enabled; - public FormattersConfigurationProvider(IDictionary resolvers, IFormattersEnvironmentOverrideConfigurationResolver environmentOverrideConfigurationResolver, IFormattersConfigurationDisableOverrideProvider envVariableDisableFlagProvider) + public FormattersConfigurationProvider(IDictionary resolvers, IFormattersEnvironmentOverrideConfigurationResolver environmentOverrideConfigurationResolver, IFormattersLoggerConfigurationProvider formattersLoggerConfigurationProvider, IFormattersConfigurationDisableOverrideProvider envVariableDisableFlagProvider) { var fileResolver = resolvers["fileBasedResolver"]; _resolvers = [fileResolver, environmentOverrideConfigurationResolver]; + _resolvers.AddRange(formattersLoggerConfigurationProvider.GetFormattersConfigurationResolvers()); _resolvedConfiguration = new Lazy(ResolveConfiguration); _envVariableDisableFlagProvider = envVariableDisableFlagProvider; } diff --git a/Reqnroll/Formatters/Configuration/FormattersConfigurationResolverBase.cs b/Reqnroll/Formatters/Configuration/FormattersConfigurationResolverBase.cs index b6b6a7a36..43f2752aa 100644 --- a/Reqnroll/Formatters/Configuration/FormattersConfigurationResolverBase.cs +++ b/Reqnroll/Formatters/Configuration/FormattersConfigurationResolverBase.cs @@ -29,19 +29,20 @@ protected virtual void ProcessJsonDocument(JsonDocument jsonDocument, Dictionary foreach(JsonProperty formatterProperty in formatters.EnumerateObject()) { var configValues = new Dictionary(); - + if (formatterProperty.Value.ValueKind == JsonValueKind.Object) { foreach (JsonProperty configProperty in formatterProperty.Value.EnumerateObject()) { - configValues.Add(configProperty.Name, GetConfigValue(configProperty.Value)); + configValues.Add(configProperty.Name, GetConfigValue(configProperty.Value)); } } - + result.Add(formatterProperty.Name, configValues); } } } + private object GetConfigValue(JsonElement valueElement) { switch (valueElement.ValueKind) diff --git a/Reqnroll/Formatters/Configuration/FormattersLoggerConfigurationProvider.cs b/Reqnroll/Formatters/Configuration/FormattersLoggerConfigurationProvider.cs new file mode 100644 index 000000000..2cd740115 --- /dev/null +++ b/Reqnroll/Formatters/Configuration/FormattersLoggerConfigurationProvider.cs @@ -0,0 +1,33 @@ +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Formatters.RuntimeSupport; +using System; +using System.Collections.Generic; + +namespace Reqnroll.Formatters.Configuration; + +public class FormattersLoggerConfigurationProvider : IFormattersLoggerConfigurationProvider +{ + private readonly IEnvironmentWrapper _environmentWrapper; + private readonly IFormatterLog _log; + + public FormattersLoggerConfigurationProvider(IEnvironmentWrapper environmentWrapper, IFormatterLog log = null) + { + _environmentWrapper = environmentWrapper ?? throw new ArgumentNullException(nameof(environmentWrapper)); + _log = log; + } + + public IEnumerable GetFormattersConfigurationResolvers() + { + var formattersConfigurationResolvers = new List(); + + var listOfFormattersResult = _environmentWrapper.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX); + foreach (var formatterEnvironmentVariable in listOfFormattersResult) + { + var resolver = new EnvironmentConfigurationResolver(_environmentWrapper, formatterEnvironmentVariable.Key, _log); + formattersConfigurationResolvers.Add(resolver); + } + + return formattersConfigurationResolvers; + } +} diff --git a/Reqnroll/Formatters/Configuration/IFormatterLoggerConfigurationProvider.cs b/Reqnroll/Formatters/Configuration/IFormatterLoggerConfigurationProvider.cs new file mode 100644 index 000000000..d8281389a --- /dev/null +++ b/Reqnroll/Formatters/Configuration/IFormatterLoggerConfigurationProvider.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Reqnroll.Formatters.Configuration; + +/// +/// Implementation of this interface provides a set of IFormattersConfigurationResolver instances each of which +/// is responsible for resolving the configuration of one formatter that has been configured via the --logger mechanism. +/// +public interface IFormattersLoggerConfigurationProvider +{ + IEnumerable GetFormattersConfigurationResolvers(); +} diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index bd82b5667..012b08ff4 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -115,6 +115,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs("fileBasedResolver"); container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs("message"); container.RegisterTypeAs("html"); diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 5a3229a23..737ff147d 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -49,6 +49,7 @@ + diff --git a/Reqnroll/Reqnroll.nuspec b/Reqnroll/Reqnroll.nuspec index 739b64284..06ab48465 100644 --- a/Reqnroll/Reqnroll.nuspec +++ b/Reqnroll/Reqnroll.nuspec @@ -37,6 +37,7 @@ + diff --git a/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTestBase.cs b/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTestBase.cs index 232c0e5a3..ccd065930 100644 --- a/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTestBase.cs +++ b/Tests/Reqnroll.Formatters.Tests/MessagesCompatibilityTestBase.cs @@ -166,7 +166,11 @@ protected static string ActualResultLocationDirectory() {"fileBasedResolver", configFileResolver } }; - FormattersConfigurationProvider configurationProvider = new FormattersConfigurationProvider(resolvers, configEnvResolver, new FormattersDisabledOverrideProvider(env)); + FormattersConfigurationProvider configurationProvider = new FormattersConfigurationProvider( + resolvers, + configEnvResolver, + new Mock().Object, + new FormattersDisabledOverrideProvider(env)); configurationProvider.GetFormatterConfigurationByName("message").TryGetValue("outputFilePath", out var outputFilePathElement); var outputFilePath = outputFilePathElement!.ToString(); diff --git a/Tests/Reqnroll.RuntimeTests/EnvironmentWrapperStub.cs b/Tests/Reqnroll.RuntimeTests/EnvironmentWrapperStub.cs index ca0c39af1..fc0681af8 100644 --- a/Tests/Reqnroll.RuntimeTests/EnvironmentWrapperStub.cs +++ b/Tests/Reqnroll.RuntimeTests/EnvironmentWrapperStub.cs @@ -25,6 +25,8 @@ public IResult GetEnvironmentVariable(string name) ? Result.Success(value) : Result.Failure($"Environment variable '{name}' not set in stub"); + public IDictionary GetEnvironmentVariables(string prefix) => throw new NotSupportedException(); + public bool IsEnvironmentVariableSet(string name) => EnvironmentVariables.ContainsKey(name); diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/CucumberConfigurationTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/CucumberConfigurationTests.cs index 495817ba7..160a59334 100644 --- a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/CucumberConfigurationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/CucumberConfigurationTests.cs @@ -23,7 +23,9 @@ public CucumberConfigurationTests() { "fileBasedResolver", _fileResolverMock.Object } }; - _sut = new FormattersConfigurationProvider(resolvers, _environmentResolverMock.Object, _disableOverrideProviderMock.Object); + _sut = new FormattersConfigurationProvider(resolvers, _environmentResolverMock.Object, + new Mock().Object, + _disableOverrideProviderMock.Object); } [Fact] diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/EnvironmentConfigurationResolverTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/EnvironmentConfigurationResolverTests.cs index c5b6e6409..d6b639ee4 100644 --- a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/EnvironmentConfigurationResolverTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/EnvironmentConfigurationResolverTests.cs @@ -81,4 +81,31 @@ public void Resolve_Should_Return_MultipleConfigurations_From_Environment_Variab Assert.Equal("forHtml", first["outputFilePath"]); Assert.Equal("forMessages", second["outputFilePath"]); } + + [Fact] + public void Resolve_Should_Parse_JSON_Format_Environment_Variable() + { + // Arrange + var expectedJson = """ + { + "formatters": { + "message": { + "outputFilePath": "foo.ndjson" + } + } + } + """; + + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariable(FormattersConfigurationConstants.REQNROLL_FORMATTERS_ENVIRONMENT_VARIABLE)) + .Returns(new Success(expectedJson)); + + // Act + var result = _sut.Resolve(); + + // Assert + result.Should().ContainKey("message"); + result["message"]["outputFilePath"].Should().Be("foo.ndjson"); + result.Should().HaveCount(1); + } } \ No newline at end of file diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormatterLoggerTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormatterLoggerTests.cs new file mode 100644 index 000000000..6e4949bfb Binary files /dev/null and b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormatterLoggerTests.cs differ diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormattersLoggerConfigurationProviderTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormattersLoggerConfigurationProviderTests.cs new file mode 100644 index 000000000..c15d53ebb --- /dev/null +++ b/Tests/Reqnroll.RuntimeTests/Formatters/Configuration/FormattersLoggerConfigurationProviderTests.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Moq; +using Reqnroll.CommonModels; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Formatters.Configuration; +using Reqnroll.Formatters.RuntimeSupport; +using Xunit; + +namespace Reqnroll.RuntimeTests.Formatters.Configuration; + +public class FormattersLoggerConfigurationProviderTests +{ + private readonly Mock _environmentWrapperMock; + private readonly Mock _formatterLogMock; + private readonly FormattersLoggerConfigurationProvider _sut; + + public FormattersLoggerConfigurationProviderTests() + { + _environmentWrapperMock = new Mock(); + _formatterLogMock = new Mock(); + _sut = new FormattersLoggerConfigurationProvider(_environmentWrapperMock.Object, _formatterLogMock.Object); + } + + [Fact] + public void Constructor_Should_Throw_ArgumentNullException_When_EnvironmentWrapper_Is_Null() + { + // Act & Assert + var act = () => new FormattersLoggerConfigurationProvider(null, _formatterLogMock.Object); + act.Should().Throw() + .WithParameterName("environmentWrapper"); + } + + [Fact] + public void Constructor_Should_Accept_Null_FormatterLog() + { + // Act & Assert + var act = () => new FormattersLoggerConfigurationProvider(_environmentWrapperMock.Object); + act.Should().NotThrow(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Return_Empty_When_Environment_Variables_List_Is_Empty() + { + // Arrange + // ReSharper disable once CollectionNeverUpdated.Local + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(new Dictionary()); + + // Act + var result = _sut.GetFormattersConfigurationResolvers(); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Create_Resolver_For_Single_Formatter() + { + // Arrange + var formatterNames = new[] { "REQNROLL_FORMATTERS_LOGGER_message" }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers().ToArray(); + + // Assert + result.Should().HaveCount(1); + result.Should().AllBeOfType(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Create_Resolvers_For_Multiple_Formatters() + { + // Arrange + var formatterNames = new List + { + "REQNROLL_FORMATTERS_LOGGER_message", + "REQNROLL_FORMATTERS_LOGGER_html", + "REQNROLL_FORMATTERS_LOGGER_json" + }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers().ToArray(); + + // Assert + result.Should().HaveCount(3); + result.Should().AllBeOfType(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Pass_EnvironmentWrapper_To_Created_Resolvers() + { + // Arrange + var formatterNames = new List { "REQNROLL_FORMATTERS_LOGGER_message" }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Mock the GetEnvironmentVariable call that will be made by the EnvironmentConfigurationResolver + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariable("REQNROLL_FORMATTERS_LOGGER_message")) + .Returns(new Success("{}")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers(); + var resolver = result.First(); + + // Trigger the resolver to use the environment wrapper + resolver.Resolve(); + + // Assert + _environmentWrapperMock.Verify(e => e.GetEnvironmentVariable("REQNROLL_FORMATTERS_LOGGER_message"), Times.Once); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Pass_FormatterLog_To_Created_Resolvers() + { + // Arrange + var formatterNames = new List { "REQNROLL_FORMATTERS_LOGGER_message" }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Mock the GetEnvironmentVariable call to return invalid JSON to trigger logging + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariable("REQNROLL_FORMATTERS_LOGGER_message")) + .Returns(new Success("invalid json")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers(); + var resolver = result.First(); + + // Trigger the resolver to use the formatter log + resolver.Resolve(); + + // Assert + _formatterLogMock.Verify(l => l.WriteMessage(It.Is(s => s.Contains("Failed to parse JSON"))), Times.Once); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Handle_Realistic_Environment_Variable_Names() + { + // Arrange - Simulate realistic environment variable names as they would appear + var formatterNames = new List + { + "REQNROLL_FORMATTERS_LOGGER_message", + "REQNROLL_FORMATTERS_LOGGER_html", + "REQNROLL_FORMATTERS_LOGGER_json", + "REQNROLL_FORMATTERS_LOGGER_junit" + }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers().ToArray(); + + // Assert + result.Should().HaveCount(4); + result.Should().AllBeOfType(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Return_Same_Results_On_Multiple_Calls() + { + // Arrange + var formatterNames = new List { "REQNROLL_FORMATTERS_LOGGER_message" }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Act + var result1 = _sut.GetFormattersConfigurationResolvers().ToArray(); + var result2 = _sut.GetFormattersConfigurationResolvers().ToArray(); + + // Assert + result1.Should().HaveCount(1); + result2.Should().HaveCount(1); + // Note: The results should be equivalent in structure, but they are new instances each time + result1.Should().AllBeOfType(); + result2.Should().AllBeOfType(); + } + + [Fact] + public void GetFormattersConfigurationResolvers_Should_Use_Correct_Environment_Variable_Prefix() + { + // Arrange + var formatterNames = new List { "REQNROLL_FORMATTERS_LOGGER_test" }; + _environmentWrapperMock + .Setup(e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX)) + .Returns(formatterNames.ToDictionary(n => n, _ => "")); + + // Act + var result = _sut.GetFormattersConfigurationResolvers(); + + // Assert + _environmentWrapperMock.Verify( + e => e.GetEnvironmentVariables(FormattersConfigurationConstants.REQNROLL_FORMATTERS_LOGGER_ENVIRONMENT_VARIABLE_PREFIX), + Times.Once); + result.Should().HaveCount(1); + } +} \ No newline at end of file diff --git a/docs/reporting/vstest-loggers.md b/docs/reporting/vstest-loggers.md index 676057407..2c459927a 100644 --- a/docs/reporting/vstest-loggers.md +++ b/docs/reporting/vstest-loggers.md @@ -40,4 +40,33 @@ A few common logger use cases can be found in the following table. The complete * - `html` - `--logger "html;logfilename=result.html"` - Produces a basic HTML report. -``` \ No newline at end of file +``` +## Reqnroll Formatters +[Reqnroll Formatters](./reqnroll-formatters.md) may also be configured via the .NET Test logger mechanism. This provides a convenient means to publish the formatter output to the `TestResult` folder. + +Reqnroll provides two built-in loggers which configure the built-in formatters and are invoked via the `dotnet test` command line like any other logger. +```{code-block} pwsh +:caption: Terminal +> dotnet test --logger "html-formatter;outputFilePath=result.html" +[...] +Results File: C:\MySolution\MyReqnrollProject\TestResults\result.html +[...] +``` +The output file name may also be set using `logfilename` in place of using `outputFilePath`. + +The built-in formatter loggers can be found in the following table. +```{list-table} +:header-rows: 1 + +* - Logger + - Option + - Description +* - `html-formatter` + - `--logger "html-formatter;outputFilePath=result.html"` + - Can be used to produce an HTML report. +* - `message-formatter` + - `--logger "message-formatter;outputFilePath=myresult.ndjson"` + - Produces an ndjson file containing Cucumber Messages. +``` + +These formatter loggers override any previous configuration of the formatter (i.e, formatters configured via Environment Variables overrides the `reqnroll.json` and the command-line loggers override both the Environment Variable and the `reqnroll.json`). \ No newline at end of file