From 52a5e118e541245779c32b820e63995693d44bd2 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 30 May 2026 20:51:06 +0100 Subject: [PATCH 01/11] Fixed warning for nullable types --- src/.editorconfig | 19 +++++ .../BuyingTrainFareWithExamples.cs | 3 +- .../{Startup.cs => TestModuleStartup.cs} | 3 +- .../XUnitOutputReporter.cs | 2 +- .../Abstractions/DefaultStepTitleFactory.cs | 5 +- src/TestStack.BDDfy/BDDfyExtensions.cs | 24 +++---- .../Configuration/ComponentFactory.cs | 2 +- .../Configuration/IHumanizer.cs | 2 +- src/TestStack.BDDfy/DefaultHumanizer.cs | 4 +- src/TestStack.BDDfy/Engine.cs | 16 +++-- src/TestStack.BDDfy/GlobalSuppressions.cs | 8 +++ .../Processors/AsyncTestRunner.cs | 12 ++-- .../Processors/AsyncTestSyncContext.cs | 8 +-- .../Processors/ExceptionProcessor.cs | 5 +- .../Processors/UnusedExampleException.cs | 9 +-- src/TestStack.BDDfy/Reporters/ReportModel.cs | 70 ++++++------------- .../Scanners/DefaultScanner.cs | 13 ++-- .../Scanners/IStoryMetaDataScanner.cs | 2 +- .../ScenarioScanners/FluentScenarioScanner.cs | 11 ++- .../ReflectiveScenarioScanner.cs | 24 ++----- .../Scanners/StepScanners/Examples/Example.cs | 19 ++--- .../StepScanners/Examples/ExampleTable.cs | 68 +++++------------- .../StepScanners/Examples/ExampleValue.cs | 45 ++++-------- .../StepScanners/Fluent/FluentScanner.cs | 2 +- .../StepScanners/Fluent/IFluentScanner.cs | 2 +- .../StepScanners/RunStepWithArgsAttribute.cs | 9 +-- .../Scanners/StoryAttributeMetaDataScanner.cs | 30 ++++---- src/TestStack.BDDfy/Scenario.cs | 8 +-- 28 files changed, 170 insertions(+), 255 deletions(-) rename src/TestStack.BDDfy.Tests/{Startup.cs => TestModuleStartup.cs} (84%) create mode 100644 src/TestStack.BDDfy/GlobalSuppressions.cs diff --git a/src/.editorconfig b/src/.editorconfig index e3d80304..e8bdc478 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,5 +1,24 @@ [*.cs] +# Static readonly fields should be PascalCase +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Constants should be PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.applicable_accessibilities = * +dotnet_naming_symbols.constants.required_modifiers = const # SYSLIB1045: Convert to 'GeneratedRegexAttribute'. dotnet_diagnostic.SYSLIB1045.severity = none diff --git a/src/Samples/TestStack.BDDfy.Samples/BuyingTrainFareWithExamples.cs b/src/Samples/TestStack.BDDfy.Samples/BuyingTrainFareWithExamples.cs index 9af74060..117b2969 100644 --- a/src/Samples/TestStack.BDDfy.Samples/BuyingTrainFareWithExamples.cs +++ b/src/Samples/TestStack.BDDfy.Samples/BuyingTrainFareWithExamples.cs @@ -19,8 +19,7 @@ public void SuccessfulRailCardPurchases() .And(_ => TheBuyerSelectsA(fare)) .When(_ => TheBuyerPays()) .Then(_ => ASaleOccursWithAnAmountOf(Price)) - .WithExamples(new ExampleTable( - "Buyer Category", "Fare", "Price") + .WithExamples(new ExampleTable("Buyer Category", "Fare", "Price") { {BuyerCategory.Student, new MonthlyPass(), new Money(76)}, {BuyerCategory.Senior, new MonthlyPass(), new Money(98)}, diff --git a/src/TestStack.BDDfy.Tests/Startup.cs b/src/TestStack.BDDfy.Tests/TestModuleStartup.cs similarity index 84% rename from src/TestStack.BDDfy.Tests/Startup.cs rename to src/TestStack.BDDfy.Tests/TestModuleStartup.cs index e4c67152..5e5f3987 100644 --- a/src/TestStack.BDDfy.Tests/Startup.cs +++ b/src/TestStack.BDDfy.Tests/TestModuleStartup.cs @@ -1,8 +1,7 @@ using System.Runtime.CompilerServices; using TestStack.BDDfy.Configuration; -using TestStack.BDDfy.Tests; -namespace TestStack.BDDfy.Samples +namespace TestStack.BDDfy.Tests { public class Startup { diff --git a/src/TestStack.BDDfy.Tests/XUnitOutputReporter.cs b/src/TestStack.BDDfy.Tests/XUnitOutputReporter.cs index 4e02a043..870b0d1d 100644 --- a/src/TestStack.BDDfy.Tests/XUnitOutputReporter.cs +++ b/src/TestStack.BDDfy.Tests/XUnitOutputReporter.cs @@ -5,7 +5,7 @@ namespace TestStack.BDDfy.Tests { public class XUnitOutputReporter(ITestOutputHelper testOutputHelper = null): TextReporter { - private ITestOutputHelper _outputHelper = testOutputHelper ?? Xunit.TestContext.Current.TestOutputHelper; + private readonly ITestOutputHelper _outputHelper = testOutputHelper ?? Xunit.TestContext.Current.TestOutputHelper; protected override void WriteLine(string text = null) { diff --git a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs index 374ffde8..28e5ef3d 100644 --- a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs @@ -47,8 +47,7 @@ string createTitle() if (testContext.Examples != null) { var matchingHeaders = testContext.Examples.Headers - .Where(header => ExampleTable.HeaderMatches(header, i.ParameterName) || - ExampleTable.HeaderMatches(header, i.Value.Name)) + .Where(header => ExampleTable.HeaderMatches(header, i.ParameterName) || ExampleTable.HeaderMatches(header, i.Value.Name)) .ToList(); if (matchingHeaders.Count > 1) @@ -58,7 +57,7 @@ string createTitle() if (matchingHeader != null) return string.Format("<{0}>", matchingHeader); } - return i.Value.Value.FlattenArray(); + return i.Value.Value?.FlattenArray() ?? Array.Empty(); }) .ToArray(); diff --git a/src/TestStack.BDDfy/BDDfyExtensions.cs b/src/TestStack.BDDfy/BDDfyExtensions.cs index f75c5ee7..c1f33ee1 100644 --- a/src/TestStack.BDDfy/BDDfyExtensions.cs +++ b/src/TestStack.BDDfy/BDDfyExtensions.cs @@ -15,9 +15,9 @@ public static class BDDfyExtensions /// public static Story BDDfy( this object testObject, - string scenarioTitle = null, + string? scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] - string caller = null) + string? caller = null) { var callerName = testObject.GetActualCallerName(caller); return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(callerName)).Run(); @@ -25,9 +25,9 @@ public static Story BDDfy( public static Engine LazyBDDfy( this object testObject, - string scenarioTitle = null, + string? scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] - string caller = null) + string? caller = null) { var callerName = testObject.GetActualCallerName(caller); return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(callerName)); @@ -43,9 +43,9 @@ public static Engine LazyBDDfy( /// public static Story BDDfy( this object testObject, - string scenarioTitle = null, + string? scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] - string caller = null) + string? caller = null) where TStory : class { var callerName = testObject.GetActualCallerName(caller); @@ -54,9 +54,9 @@ public static Story BDDfy( public static Engine LazyBDDfy( this object testObject, - string scenarioTitle = null, + string? scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] - string caller = null) + string? caller = null) where TStory : class { var callerName = testObject.GetActualCallerName(caller); @@ -65,8 +65,8 @@ public static Engine LazyBDDfy( static Engine InternalLazyBDDfy( object testObject, - string scenarioTitle, - Type explicitStoryType = null) + string? scenarioTitle, + Type? explicitStoryType = null) { var testContext = TestContext.GetContext(testObject); @@ -77,7 +77,7 @@ static Engine InternalLazyBDDfy( return new (storyScanner); } - static DefaultScanner GetReflectiveScanner(ITestContext testContext, string scenarioTitle = null, Type explicitStoryType = null) + static DefaultScanner GetReflectiveScanner(ITestContext testContext, string? scenarioTitle = null, Type? explicitStoryType = null) { var stepScanners = Configurator.Scanners.GetStepScanners(testContext).ToArray(); var reflectiveScenarioScanner = new ReflectiveScenarioScanner(scenarioTitle, stepScanners); @@ -85,7 +85,7 @@ static DefaultScanner GetReflectiveScanner(ITestContext testContext, string scen return new (testContext, reflectiveScenarioScanner, explicitStoryType); } - static string GetActualCallerName(this object testObject, string inferedCallerName) + static string? GetActualCallerName(this object testObject, string? inferedCallerName) => inferedCallerName == ".ctor" ? testObject.GetType().Name : inferedCallerName; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Configuration/ComponentFactory.cs b/src/TestStack.BDDfy/Configuration/ComponentFactory.cs index 6f99ca68..3f5969b6 100644 --- a/src/TestStack.BDDfy/Configuration/ComponentFactory.cs +++ b/src/TestStack.BDDfy/Configuration/ComponentFactory.cs @@ -26,7 +26,7 @@ public void Enable() _active = true; } - public TComponent ConstructFor(TMaterial material) + public TComponent? ConstructFor(TMaterial material) { if (_active && _runsOn(material)) return _factory(); diff --git a/src/TestStack.BDDfy/Configuration/IHumanizer.cs b/src/TestStack.BDDfy/Configuration/IHumanizer.cs index 02c4583c..95833378 100644 --- a/src/TestStack.BDDfy/Configuration/IHumanizer.cs +++ b/src/TestStack.BDDfy/Configuration/IHumanizer.cs @@ -2,6 +2,6 @@ namespace TestStack.BDDfy.Configuration { public interface IHumanizer { - string Humanize(string input); + string? Humanize(string? input); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/DefaultHumanizer.cs b/src/TestStack.BDDfy/DefaultHumanizer.cs index e88d2908..4352fd45 100644 --- a/src/TestStack.BDDfy/DefaultHumanizer.cs +++ b/src/TestStack.BDDfy/DefaultHumanizer.cs @@ -15,7 +15,7 @@ internal partial class DefaultHumanizer: IHumanizer private static readonly Regex UnicodeMatchPattern = new(@"[^\u0000-\u007F]"); private static readonly Regex LoneIReplacePattern = new(@"(?<=^|\s)i(?=\s|$)"); - public string Humanize(string input) + public string? Humanize(string? input) { if (string.IsNullOrWhiteSpace(input)) return input; @@ -23,7 +23,7 @@ public string Humanize(string input) input = TokensPattern.Replace(input, "-#$1#-"); - var words = input.Split(['_','-']); + var words = input.Split(['_', '-'], StringSplitOptions.RemoveEmptyEntries); var finalWords = words.Select(x => TokenReplacePattern.Replace(x, "<$1>")); diff --git a/src/TestStack.BDDfy/Engine.cs b/src/TestStack.BDDfy/Engine.cs index 4c1083a1..5a487107 100644 --- a/src/TestStack.BDDfy/Engine.cs +++ b/src/TestStack.BDDfy/Engine.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using TestStack.BDDfy.Configuration; using TestStack.BDDfy.Processors; @@ -7,6 +8,7 @@ namespace TestStack.BDDfy public class Engine(IScanner scanner) { private readonly IScanner _scanner = scanner; + private Story? _story; static Engine() { @@ -23,25 +25,25 @@ static void InvokeBatchProcessors() public Story Run() { - Story = _scanner.Scan(); + _story = _scanner.Scan(); - var processors = Configurator.Processors.GetProcessors(Story).ToList(); + var processors = Configurator.Processors.GetProcessors(_story).ToList(); try { //run processors in the right order regardless of the order they are provided to the Bddfier foreach (var processor in processors.Where(p => p.ProcessType < ProcessType.Disposal).OrderBy(p => (int)p.ProcessType)) - processor.Process(Story); + processor.Process(_story); } finally { foreach (var finallyProcessor in processors.Where(p => p.ProcessType >= ProcessType.Disposal).OrderBy(p => (int)p.ProcessType)) - finallyProcessor.Process(Story); + finallyProcessor.Process(_story); } - return Story; + return _story; } - public Story Story { get; private set; } + public Story Story { get { return _story ?? throw new InvalidOperationException("Story has not been run yet"); } } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/GlobalSuppressions.cs b/src/TestStack.BDDfy/GlobalSuppressions.cs new file mode 100644 index 00000000..31448155 --- /dev/null +++ b/src/TestStack.BDDfy/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "", Scope = "namespace", Target = "~N:TestStack.BDDfy")] diff --git a/src/TestStack.BDDfy/Processors/AsyncTestRunner.cs b/src/TestStack.BDDfy/Processors/AsyncTestRunner.cs index 9b525329..9e6c5ed8 100644 --- a/src/TestStack.BDDfy/Processors/AsyncTestRunner.cs +++ b/src/TestStack.BDDfy/Processors/AsyncTestRunner.cs @@ -23,9 +23,13 @@ public static void Run(Func performStep) } catch (AggregateException ae) { - var innerException = ae.InnerException; - ExceptionProcessor.PreserveStackTrace(innerException); - throw innerException; + if(ae.InnerException is Exception innerException) + { + ExceptionProcessor.PreserveStackTrace(innerException); + throw innerException; + } + + throw; } } else @@ -45,7 +49,7 @@ public static void Run(Func performStep) } [SecuritySafeCritical] - private static void SetSynchronizationContext(SynchronizationContext context) + private static void SetSynchronizationContext(SynchronizationContext? context) { SynchronizationContext.SetSynchronizationContext(context); } diff --git a/src/TestStack.BDDfy/Processors/AsyncTestSyncContext.cs b/src/TestStack.BDDfy/Processors/AsyncTestSyncContext.cs index bab669be..34859601 100644 --- a/src/TestStack.BDDfy/Processors/AsyncTestSyncContext.cs +++ b/src/TestStack.BDDfy/Processors/AsyncTestSyncContext.cs @@ -9,7 +9,7 @@ namespace TestStack.BDDfy internal class AsyncTestSyncContext : SynchronizationContext { readonly object _lock = new(); - Exception _exception; + Exception? _exception; int _operationCount; public override void OperationCompleted() @@ -30,7 +30,7 @@ public override void OperationStarted() } } - public override void Post(SendOrPostCallback d, object state) + public override void Post(SendOrPostCallback d, object? state) { // The call to Post() may be the state machine signaling that an exception is // about to be thrown, so we make sure the operation count gets incremented @@ -50,7 +50,7 @@ public override void Post(SendOrPostCallback d, object state) }); } - public override void Send(SendOrPostCallback d, object state) + public override void Send(SendOrPostCallback d, object? state) { try { @@ -62,7 +62,7 @@ public override void Send(SendOrPostCallback d, object state) } } - public Exception WaitForCompletion() + public Exception? WaitForCompletion() { lock (_lock) { diff --git a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs index f9e04ca2..2b937dc6 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -26,9 +26,8 @@ public ProcessType ProcessType // http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx internal static void PreserveStackTrace(Exception exception) { - MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", - BindingFlags.Instance | BindingFlags.NonPublic); - preserveStackTrace.Invoke(exception, null); + var preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); + preserveStackTrace?.Invoke(exception, null); } public void Process(Story story) diff --git a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs index 27471b6e..c0d76f50 100644 --- a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs +++ b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs @@ -3,12 +3,9 @@ namespace TestStack.BDDfy.Processors { [Serializable] - public class UnusedExampleException : Exception - { - public UnusedExampleException(ExampleValue unusedValue) - : base(string.Format("Example Column '{0}' is unused, all examples should be consumed by the test (have you misspelt a field or property?)\r\n\r\n" + public class UnusedExampleException(ExampleValue unusedValue): Exception( + string.Format("Example Column '{0}' is unused, all examples should be consumed by the test (have you misspelt a field or property?)\r\n\r\n" + "If this is not the case, raise an issue at https://github.com/TestStack/TestStack.BDDfy/issues.", unusedValue.Header)) - { - } + { } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Reporters/ReportModel.cs b/src/TestStack.BDDfy/Reporters/ReportModel.cs index 1bc3c1ea..e156ef54 100644 --- a/src/TestStack.BDDfy/Reporters/ReportModel.cs +++ b/src/TestStack.BDDfy/Reporters/ReportModel.cs @@ -6,95 +6,71 @@ namespace TestStack.BDDfy.Reporters { public class ReportModel { - public List Stories { get; set; } - - public ReportModel() - { - Stories = []; - } + public List Stories { get; set; } = []; public class Story { - public Story() - { - Scenarios = []; - } - - public string Namespace { get; set; } + public string? Namespace { get; set; } public Result Result { get; set; } - public List Scenarios { get; set; } - public StoryMetadata Metadata { get; set; } + public List Scenarios { get; set; } = []; + public StoryMetadata? Metadata { get; set; } } public class StoryMetadata { - public Type Type { get; set; } - public string Title { get; set; } - public string TitlePrefix { get; set; } - public string Narrative1 { get; set; } - public string Narrative2 { get; set; } - public string Narrative3 { get; set; } - public string ImageUri { get; set; } - public string StoryUri { get; set; } + public Type? Type { get; set; } + public string? Title { get; set; } + public string? TitlePrefix { get; set; } + public string? Narrative1 { get; set; } + public string? Narrative2 { get; set; } + public string? Narrative3 { get; set; } + public string? ImageUri { get; set; } + public string? StoryUri { get; set; } } public class Scenario { - public Scenario() - { - Tags = []; - Steps = []; - } - - public string Id { get; set; } + public string Id { get; set; } = null!; - public string Title { get; set; } + public string? Title { get; set; } - public List Tags { get; set; } + public List Tags { get; set; } = []; - public Example Example { get; set; } + public Example? Example { get; set; } public TimeSpan Duration { get; set; } - public List Steps { get; set; } + public List Steps { get; set; } = []; public Result Result { get; set; } } public class Step { - public string Id { get; set; } + public string Id { get; set; } = null!; public bool Asserts { get; set; } public bool ShouldReport { get; set; } - public string Title { get; set; } + public string? Title { get; set; } public ExecutionOrder ExecutionOrder { get; set; } public Result Result { get; set; } - public Exception Exception { get; set; } + public Exception? Exception { get; set; } public TimeSpan Duration { get; set; } } public class Example { - public Example() - { - Values = new List(); - } - - public string[] Headers { get; set; } + public string[] Headers { get; set; } = []; - public IEnumerable Values { get; set; } + public IEnumerable Values { get; set; } = []; - public override string ToString() - { - return string.Join(", ", Values.Select(i => i.ToString())); - } + public override string ToString() => string.Join(", ", Values.Select(i => i.ToString())); } } } diff --git a/src/TestStack.BDDfy/Scanners/DefaultScanner.cs b/src/TestStack.BDDfy/Scanners/DefaultScanner.cs index ec039963..8b31ffc8 100644 --- a/src/TestStack.BDDfy/Scanners/DefaultScanner.cs +++ b/src/TestStack.BDDfy/Scanners/DefaultScanner.cs @@ -4,18 +4,13 @@ namespace TestStack.BDDfy { - public class DefaultScanner(ITestContext testContext, IScenarioScanner scenarioScanner, Type explicitStoryType = null): IScanner + public class DefaultScanner(ITestContext testContext, IScenarioScanner scenarioScanner, Type? explicitStoryType = null): IScanner { - private readonly ITestContext _testContext = testContext; - private readonly Type _explicitStoryType = explicitStoryType; - private readonly IScenarioScanner _scenarioScanner = scenarioScanner; - public Story Scan() { - var scenarios = _scenarioScanner.Scan(_testContext); - var metaData = Configurator.Scanners.StoryMetadataScanner().Scan(_testContext.TestObject, _explicitStoryType); - - return new Story(metaData, scenarios.ToArray()); + var scenarios = scenarioScanner.Scan(testContext); + var metaData = Configurator.Scanners.StoryMetadataScanner().Scan(testContext.TestObject, explicitStoryType); + return new Story(metaData, [.. scenarios]); } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/IStoryMetaDataScanner.cs b/src/TestStack.BDDfy/Scanners/IStoryMetaDataScanner.cs index bda156ea..d8c13622 100644 --- a/src/TestStack.BDDfy/Scanners/IStoryMetaDataScanner.cs +++ b/src/TestStack.BDDfy/Scanners/IStoryMetaDataScanner.cs @@ -4,6 +4,6 @@ namespace TestStack.BDDfy { public interface IStoryMetadataScanner { - StoryMetadata Scan(object testObject, Type explicitStoryType = null); + StoryMetadata? Scan(object testObject, Type? explicitStoryType = null); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/ScenarioScanners/FluentScenarioScanner.cs b/src/TestStack.BDDfy/Scanners/ScenarioScanners/FluentScenarioScanner.cs index 23ba87d0..a883b8aa 100644 --- a/src/TestStack.BDDfy/Scanners/ScenarioScanners/FluentScenarioScanner.cs +++ b/src/TestStack.BDDfy/Scanners/ScenarioScanners/FluentScenarioScanner.cs @@ -4,22 +4,19 @@ namespace TestStack.BDDfy.Scanners.ScenarioScanners; -internal class FluentScenarioScanner(List steps, string title): IScenarioScanner +internal class FluentScenarioScanner(List steps, string? title): IScenarioScanner { - private readonly string _title = title; - private readonly List _steps = steps; - public IEnumerable Scan(ITestContext testContext) { - var scenarioText = _title ?? testContext.TestObject.GetType().Name; + var scenarioText = title ?? testContext.TestObject.GetType().Name; if (testContext.Examples != null) { var scenarioId = Configurator.IdGenerator.GetScenarioId(); return testContext.Examples.Select(example => - new Scenario(scenarioId, testContext.TestObject, CloneSteps(_steps), scenarioText, example, testContext.Tags)); + new Scenario(scenarioId, testContext.TestObject, CloneSteps(steps), scenarioText, example, testContext.Tags)); } - return [new Scenario(testContext.TestObject, _steps, scenarioText, testContext.Tags)]; + return [new Scenario(testContext.TestObject, steps, scenarioText, testContext.Tags)]; } private static List CloneSteps(IEnumerable steps) => [.. steps.Select(static step => new Step(step))]; diff --git a/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs b/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs index adcca8a5..906d9172 100644 --- a/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs +++ b/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs @@ -6,26 +6,19 @@ namespace TestStack.BDDfy { - public class ReflectiveScenarioScanner(string scenarioTitle, params IStepScanner[] stepScanners): IScenarioScanner + public class ReflectiveScenarioScanner(string? scenarioTitle, params IStepScanner[] stepScanners): IScenarioScanner { - private readonly IEnumerable _stepScanners = stepScanners; - private readonly string _scenarioTitle = scenarioTitle; - - public ReflectiveScenarioScanner(params IStepScanner[] stepScanners) - : this(null, stepScanners) - { - } + public ReflectiveScenarioScanner(params IStepScanner[] stepScanners) : this(null, stepScanners) { } public virtual IEnumerable Scan(ITestContext testContext) { Type scenarioType; - string scenarioTitle; if (testContext.Examples == null) { var steps = ScanScenarioForSteps(testContext); scenarioType = testContext.TestObject.GetType(); - scenarioTitle = _scenarioTitle ?? GetScenarioText(scenarioType); + scenarioTitle ??= GetScenarioText(scenarioType); var orderedSteps = steps.OrderBy(o => o.ExecutionOrder).ThenBy(o => o.ExecutionSubOrder).ToList(); yield return new Scenario(testContext.TestObject, orderedSteps, scenarioTitle, testContext.Tags); @@ -33,7 +26,7 @@ public virtual IEnumerable Scan(ITestContext testContext) } scenarioType = testContext.TestObject.GetType(); - scenarioTitle = _scenarioTitle ?? GetScenarioText(scenarioType); + scenarioTitle ??= GetScenarioText(scenarioType); var scenarioId = Configurator.IdGenerator.GetScenarioId(); @@ -45,10 +38,7 @@ public virtual IEnumerable Scan(ITestContext testContext) } } - static string GetScenarioText(Type scenarioType) - { - return Configurator.Humanizer.Humanize(scenarioType.Name); - } + static string? GetScenarioText(Type scenarioType) => Configurator.Humanizer.Humanize(scenarioType.Name); protected virtual IEnumerable ScanScenarioForSteps(ITestContext testContext) { @@ -58,7 +48,7 @@ protected virtual IEnumerable ScanScenarioForSteps(ITestContext testContex foreach (var methodInfo in GetMethodsOfInterest(scenarioType)) { // chain of responsibility of step scanners - foreach (var scanner in _stepScanners) + foreach (var scanner in stepScanners) { var steps = scanner.Scan(testContext, methodInfo); if (steps.Any()) @@ -80,7 +70,7 @@ protected virtual IEnumerable ScanScenarioForSteps(ITestContext testContex foreach (var methodInfo in GetMethodsOfInterest(scenarioType)) { // chain of responsibility of step scanners - foreach (var scanner in _stepScanners) + foreach (var scanner in stepScanners) { var steps = scanner.Scan(testContext, methodInfo, example); if (steps.Any()) diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/Example.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/Example.cs index b5241d54..bfca1bcf 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/Example.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/Example.cs @@ -6,23 +6,12 @@ namespace TestStack.BDDfy { public class Example(params ExampleValue[] items) { - private readonly ExampleValue[] _items = items; + public string[] Headers { get { return [.. Values.Select(i => i.Header)]; } } - public string[] Headers { get { return Values.Select(i => i.Header).ToArray(); } } + public IEnumerable Values { get; } = items; - public IEnumerable Values - { - get { return _items; } - } + public object? GetValueOf(int index, Type targetType) => Values.ElementAt(index).GetValue(targetType); - public object GetValueOf(int index, Type targetType) - { - return _items[index].GetValue(targetType); - } - - public override string ToString() - { - return string.Join(", ", Values.Select(i => i.ToString())); - } + public override string ToString() => string.Join(", ", Values.Select(i => i.ToString())); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleTable.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleTable.cs index 113227f6..295c2391 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleTable.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleTable.cs @@ -16,51 +16,29 @@ public class ExampleTable(params string[] headers): ICollection public bool IsReadOnly { get { return false; } } - public void Add(params object[] items) + public void Add(params object?[] items) { if (items.Length != Headers.Length) throw new ArgumentException(string.Format("Number of column values does not match number of headers, got {0}, expected {1}", items.Length, Headers.Length)); - Example example = null; - // ReSharper disable once AccessToModifiedClosure - example = new Example(items.Select((o, i) => new ExampleValue(Headers[i], o, () => _rows.IndexOf(example))).ToArray()); + Example? example = null; + example = new Example([.. items.Select((o, i) => new ExampleValue(Headers[i], o, () => _rows.IndexOf(example!)))]); Add(example); } - public void Add(Example example) - { - _rows.Add(example); - } + public void Add(Example example) => _rows.Add(example); - public void Clear() - { - _rows.Clear(); - } + public void Clear() => _rows.Clear(); - public bool Contains(Example item) - { - return _rows.Contains(item); - } + public bool Contains(Example item) => _rows.Contains(item); - public void CopyTo(Example[] array, int arrayIndex) - { - _rows.CopyTo(array, arrayIndex); - } + public void CopyTo(Example[] array, int arrayIndex) => _rows.CopyTo(array, arrayIndex); - public bool Remove(Example item) - { - return _rows.Remove(item); - } + public bool Remove(Example item) => _rows.Remove(item); - public IEnumerator GetEnumerator() - { - return _rows.GetEnumerator(); - } + public IEnumerator GetEnumerator() => _rows.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public static ExampleTable Parse(string table) { @@ -76,23 +54,16 @@ public static ExampleTable Parse(string table) return exampleTable; } - static object[] GetValues(string row) - { - return row.Split('|').Select(h => h.Trim()).Select(s=>string.IsNullOrEmpty(s) ? null : (object)s).ToArray(); - } + static object?[] GetValues(string row) => [.. row.Split('|').Select(static h => h.Trim()).Select(s => string.IsNullOrEmpty(s) ? null : (object)s)]; - public static bool HeaderMatches(string header, string name) + public static bool HeaderMatches(string header, string? name) { - if (name == null) - return false; + if (name is null) return false; return Sanitise(name).ToLower().Equals(Sanitise(header).ToLower()); } - private static string Sanitise(string value) - { - return value.Replace(" ", string.Empty).Replace("_", string.Empty); - } + private static string Sanitise(string value) => value.Replace(" ", string.Empty).Replace("_", string.Empty); public string ToString(string[] additionalHeaders, string[][] additionalData) { @@ -101,7 +72,7 @@ public string ToString(string[] additionalHeaders, string[][] additionalData) var rows = new List(); - Action> addRow = cells => + void addRow(IEnumerable cells) { var row = new string[numberColumns]; var index = 0; @@ -121,7 +92,7 @@ public string ToString(string[] additionalHeaders, string[][] additionalData) } rows.Add(row); - }; + } addRow(Headers.Concat(additionalHeaders)); var rowIndex = 0; @@ -141,12 +112,9 @@ public string ToString(string[] additionalHeaders, string[][] additionalData) return stringBuilder.ToString(); } - public override string ToString() - { - return ToString(new string[0], new string[0][]); - } + public override string ToString() => ToString([], []); - private void WriteExampleRow(string[] row, int[] maxWidth, StringBuilder stringBuilder) + private static void WriteExampleRow(string[] row, int[] maxWidth, StringBuilder stringBuilder) { for (var index = 0; index < row.Length; index++) { diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs index b4eb2fee..66c480cd 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs @@ -2,50 +2,41 @@ namespace TestStack.BDDfy { - public class ExampleValue(string header, object underlyingValue, Func getRowIndex) + public class ExampleValue(string header, object? underlyingValue, Func getRowIndex) { - private readonly object _underlyingValue = underlyingValue; + private readonly object? _underlyingValue = underlyingValue; private readonly Func _getRowIndex = getRowIndex; public string Header { get; private set; } = header; - public bool MatchesName(string name) - { - return ExampleTable.HeaderMatches(Header, name); - } + public bool MatchesName(string name) => ExampleTable.HeaderMatches(Header, name); - public int Row - { - get - { - return _getRowIndex() + 1; - } - } + public int Row => _getRowIndex() + 1; - public object GetValue(Type targetType) + public object? GetValue(Type targetType) { - var stringValue = _underlyingValue as string; - if (_underlyingValue == null) + if (_underlyingValue is null) { if (targetType.IsValueType() && !(targetType.IsGenericType() && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))) { - var valueAsString = string.IsNullOrEmpty(stringValue) ? "" : string.Format("\"{0}\"", _underlyingValue); - throw new ArgumentException(string.Format("Cannot convert {0} to {1} (Column: '{2}', Row: {3})", valueAsString, targetType.Name, Header, Row)); + throw new ArgumentException(string.Format("Cannot convert to {0} (Column: '{1}', Row: {2})", targetType.Name, Header, Row)); } ValueHasBeenUsed = true; return null; } + var valueIsString = _underlyingValue is string; + ValueHasBeenUsed = true; if (targetType.IsInstanceOfType(_underlyingValue)) return _underlyingValue; - if (targetType.IsEnum() && _underlyingValue is string) + if (targetType.IsEnum() && valueIsString) return Enum.Parse(targetType, (string)_underlyingValue); - if (targetType == typeof(DateTime)) - return DateTime.Parse(stringValue); + if (targetType == typeof(DateTime) && valueIsString) + return DateTime.Parse((string)_underlyingValue); try { @@ -55,21 +46,15 @@ public object GetValue(Type targetType) { throw new UnassignableExampleException(string.Format( "{0} cannot be assigned to {1} (Column: '{2}', Row: {3})", - _underlyingValue == null ? "" : _underlyingValue.ToString(), + _underlyingValue?.ToString() ?? "", targetType.Name, Header, Row), ex, this); } } public bool ValueHasBeenUsed { get; private set; } - public override string ToString() - { - return string.Join("{0}: {1}", Header, _underlyingValue); - } + public override string ToString() => string.Join("{0}: {1}", Header, _underlyingValue); - public string GetValueAsString() - { - return _underlyingValue.FlattenArray().ToString(); - } + public string? GetValueAsString() => _underlyingValue?.FlattenArray().ToString(); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs index e18159c2..178d9c88 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs @@ -26,7 +26,7 @@ internal FluentScanner(TScenario testObject) ?? throw new InvalidOperationException("Failed to retrieve method info for ExecuteAction."); } - IScanner IFluentScanner.GetScanner(string scenarioTitle, Type explicitStoryType) + public IScanner GetScanner(string? scenarioTitle, Type? explicitStoryType) { return new DefaultScanner(_testContext, new FluentScenarioScanner(_steps, scenarioTitle), explicitStoryType); } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/IFluentScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/IFluentScanner.cs index 06c021e0..e61cb16b 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/IFluentScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/IFluentScanner.cs @@ -4,6 +4,6 @@ namespace TestStack.BDDfy { public interface IFluentScanner { - IScanner GetScanner(string scenarioTitle, Type explicitStoryType); + IScanner GetScanner(string? scenarioTitle, Type? explicitStoryType); } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/RunStepWithArgsAttribute.cs b/src/TestStack.BDDfy/Scanners/StepScanners/RunStepWithArgsAttribute.cs index 776c6587..c1a903fe 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/RunStepWithArgsAttribute.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/RunStepWithArgsAttribute.cs @@ -5,13 +5,8 @@ namespace TestStack.BDDfy [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class RunStepWithArgsAttribute(params object[] inputArguments): Attribute { - private readonly object[] _inputArguments = inputArguments; + public string? StepTextTemplate { get; set; } - public string StepTextTemplate { get; set; } - - public object[] InputArguments - { - get { return _inputArguments; } - } + public object[] InputArguments { get; } = inputArguments; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs b/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs index c0219adf..84a9bc68 100644 --- a/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs @@ -1,39 +1,35 @@ using System; using System.Linq; +using System.Reflection; namespace TestStack.BDDfy.Scanners { internal class StoryAttributeMetadataScanner : IStoryMetadataScanner { - public virtual StoryMetadata Scan(object testObject, Type explicitStoryType = null) - { - return GetStoryMetadata(testObject, explicitStoryType) ?? GetStoryMetadataFromScenario(testObject); - } + public virtual StoryMetadata? Scan(object testObject, Type? explicitStoryType = null) + => GetStoryMetadata(testObject, explicitStoryType) ?? GetStoryMetadataFromScenario(testObject); - static StoryMetadata GetStoryMetadataFromScenario(object testObject) + static StoryMetadata? GetStoryMetadataFromScenario(object testObject) { var scenarioType = testObject.GetType(); var storyAttribute = GetStoryAttribute(scenarioType); - if (storyAttribute == null) - return null; + if (storyAttribute is null) return null; return new StoryMetadata(scenarioType, storyAttribute); } - StoryMetadata GetStoryMetadata(object testObject, Type explicityStoryType) + StoryMetadata? GetStoryMetadata(object testObject, Type? explicitStoryType) { - var candidateStoryType = GetCandidateStory(testObject, explicityStoryType); - if (candidateStoryType == null) - return null; + var candidateStoryType = GetCandidateStory(testObject, explicitStoryType); + if (candidateStoryType is null) return null; var storyAttribute = GetStoryAttribute(candidateStoryType); - if (storyAttribute == null) - return null; + if (storyAttribute is null) return null; return new StoryMetadata(candidateStoryType, storyAttribute); } - protected virtual Type GetCandidateStory(object testObject, Type explicitStoryType) + protected virtual Type? GetCandidateStory(object testObject, Type? explicitStoryType) { if (explicitStoryType != null) return explicitStoryType; @@ -43,9 +39,7 @@ protected virtual Type GetCandidateStory(object testObject, Type explicitStoryTy return declaringType ?? testObjectType; } - static StoryNarrativeAttribute GetStoryAttribute(Type candidateStoryType) - { - return (StoryNarrativeAttribute)candidateStoryType.GetCustomAttributes(typeof(StoryNarrativeAttribute), true).FirstOrDefault(); - } + static StoryNarrativeAttribute? GetStoryAttribute(Type candidateStoryType) + => candidateStoryType.GetCustomAttribute(true); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scenario.cs b/src/TestStack.BDDfy/Scenario.cs index e3222836..f7860a54 100644 --- a/src/TestStack.BDDfy/Scenario.cs +++ b/src/TestStack.BDDfy/Scenario.cs @@ -7,7 +7,7 @@ namespace TestStack.BDDfy { public class Scenario { - public Scenario(object testObject, List steps, string scenarioText, List tags) + public Scenario(object testObject, List steps, string? scenarioText, List tags) { TestObject = testObject; Steps = steps; @@ -16,7 +16,7 @@ public Scenario(object testObject, List steps, string scenarioText, List steps, string scenarioText, Example example, List tags) + public Scenario(string id, object testObject, List steps, string? scenarioText, Example? example, List tags) { Id = id; TestObject = testObject; @@ -27,9 +27,9 @@ public Scenario(string id, object testObject, List steps, string scenarioT } public string Id { get; set; } - public string Title { get; private set; } + public string? Title { get; private set; } public List Tags { get; private set; } - public Example Example { get; set; } + public Example? Example { get; set; } public TimeSpan Duration { get { return new TimeSpan(Steps.Sum(x => x.Duration.Ticks)); } } public object TestObject { get; internal set; } public List Steps { get; private set; } From 77d9649657cee51ec018ea305d6d99f33f935986 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 30 May 2026 21:00:10 +0100 Subject: [PATCH 02/11] fix broken test --- .../Scanners/StepScanners/ArgumentCleaningExtensions.cs | 4 ++-- .../Scanners/StepScanners/Examples/ExampleValue.cs | 2 +- src/TestStack.BDDfy/Step.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/ArgumentCleaningExtensions.cs b/src/TestStack.BDDfy/Scanners/StepScanners/ArgumentCleaningExtensions.cs index 0727ae47..ac9966ae 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/ArgumentCleaningExtensions.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/ArgumentCleaningExtensions.cs @@ -11,7 +11,7 @@ internal static object[] FlattenArrays(this IEnumerable inputs) return [.. inputs.Select(FlattenArray)]; } - public static object FlattenArray(this object input) + public static object FlattenArray(this object? input) { if (input is Array inputArray) { @@ -22,7 +22,7 @@ public static object FlattenArray(this object input) return GetSafeValue(input); } - static object GetSafeValue(object input) => input switch + static object GetSafeValue(object? input) => input switch { null => "", string s => s == "" ? "" : (s.Trim() == "" ? $"'{s}'" : s), diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs index 66c480cd..8b872308 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/ExampleValue.cs @@ -55,6 +55,6 @@ public class ExampleValue(string header, object? underlyingValue, Func getR public override string ToString() => string.Join("{0}: {1}", Header, _underlyingValue); - public string? GetValueAsString() => _underlyingValue?.FlattenArray().ToString(); + public string? GetValueAsString() => _underlyingValue.FlattenArray().ToString(); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Step.cs b/src/TestStack.BDDfy/Step.cs index 444af43c..07a9d570 100644 --- a/src/TestStack.BDDfy/Step.cs +++ b/src/TestStack.BDDfy/Step.cs @@ -7,7 +7,7 @@ namespace TestStack.BDDfy public class Step { private readonly StepTitle _stepTitle; - private string _title; + private string? _title; public Step( Func action, StepTitle stepTitle, @@ -45,7 +45,7 @@ public Step(Step step) public string Title => _title??= _stepTitle; public ExecutionOrder ExecutionOrder { get; private set; } public Result Result { get; set; } - public Exception Exception { get; set; } + public Exception? Exception { get; set; } public int ExecutionSubOrder { get; set; } public TimeSpan Duration { get; set; } public List Arguments { get; private set; } From c9d9ec00ddd2ec58c09a69ae57d2e8a7336c0966 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sun, 31 May 2026 00:40:51 +0100 Subject: [PATCH 03/11] nullable cleanup --- .../WhenBuildingReportDiagnostics.cs | 2 +- ...xamplesUsingReflectiveScanner.approved.txt | 10 +++ .../Examples/ReflectiveWithExamples.cs | 14 ++-- ...EmptyOrWhitespaceWillResultInMethodName.cs | 4 ++ .../Abstractions/DefaultStepTitleFactory.cs | 13 ++-- src/TestStack.BDDfy/Configuration/Scanners.cs | 2 +- .../Processors/ExceptionProcessor.cs | 13 ++-- src/TestStack.BDDfy/Processors/StoryCache.cs | 22 ++----- src/TestStack.BDDfy/Properties/Annotations.cs | 2 +- .../Reporters/ConsoleReporter.cs | 2 +- .../Diagnostics/DiagnosticsReport.cs | 2 +- .../Diagnostics/DiagnosticsReportBuilder.cs | 20 +++--- .../Reporters/Diagnostics/StoryDiagnostic.cs | 10 +-- .../Reporters/FileReportModel.cs | 4 +- .../Reporters/FileReportSummaryModel.cs | 64 +++++-------------- .../Reporters/Html/ClassicReportBuilder.cs | 64 ++++++++----------- .../Reporters/Html/HtmlReportModel.cs | 7 +- .../Reporters/Html/HtmlReportResources.cs | 4 +- .../Reporters/Html/HtmlReporter.cs | 12 ++-- .../Reporters/Html/MetroReportBuilder.cs | 59 ++++++++--------- .../MarkDown/MarkDownReportBuilder.cs | 6 +- src/TestStack.BDDfy/Reporters/ReportModel.cs | 6 +- .../Reporters/ReportModelMappers.cs | 35 ++++------ src/TestStack.BDDfy/Reporters/TextReporter.cs | 19 +++--- .../ArgumentCleaningExtensions.cs | 9 +-- .../StepScanners/Examples/ExampleValue.cs | 4 +- .../ExecutableAttributeStepScanner.cs | 12 ++-- .../StepScanners/Fluent/StepArgument.cs | 10 +-- .../MethodName/MethodNameStepScanner.cs | 60 ++++++++--------- .../StepScanners/StepActionFactory.cs | 23 ++----- src/TestStack.BDDfy/Scanners/StoryMetadata.cs | 35 +++++++--- src/TestStack.BDDfy/Scenario.cs | 2 +- src/TestStack.BDDfy/Step.cs | 4 +- src/TestStack.BDDfy/Story.cs | 25 +++----- .../StoryNarrativeAttribute.cs | 19 +++--- 35 files changed, 270 insertions(+), 329 deletions(-) create mode 100644 src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.RunExamplesUsingReflectiveScanner.approved.txt diff --git a/src/TestStack.BDDfy.Tests/Reporters/Diagnostics/WhenBuildingReportDiagnostics.cs b/src/TestStack.BDDfy.Tests/Reporters/Diagnostics/WhenBuildingReportDiagnostics.cs index 0f82ef98..ea665961 100644 --- a/src/TestStack.BDDfy.Tests/Reporters/Diagnostics/WhenBuildingReportDiagnostics.cs +++ b/src/TestStack.BDDfy.Tests/Reporters/Diagnostics/WhenBuildingReportDiagnostics.cs @@ -25,7 +25,7 @@ internal void AndGivenTwoStoriesEachWithTwoScenariosWithThreeStepsOfFiveMillisec internal void WhenTheDiagnosticDataIsCalculated() { - _result = _sut.GetDiagnosticData(new FileReportModel(_stories.ToReportModel())); + _result = DiagnosticsReportBuilder.GetDiagnosticData(new FileReportModel(_stories.ToReportModel())); } internal void ThenTwoStoriesShouldBeReturned() diff --git a/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.RunExamplesUsingReflectiveScanner.approved.txt b/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.RunExamplesUsingReflectiveScanner.approved.txt new file mode 100644 index 00000000..5c79a453 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.RunExamplesUsingReflectiveScanner.approved.txt @@ -0,0 +1,10 @@ + +Scenario: Reflective with examples + Given step with passed as parameter + And step with accessed via property + +Examples: +| First Example | Second Example | +| 1 | foo | +| 2 | bar | + diff --git a/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.cs b/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.cs index af27339d..ce47cc66 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/Examples/ReflectiveWithExamples.cs @@ -1,9 +1,11 @@ using Shouldly; +using System.Diagnostics.CodeAnalysis; using TestStack.BDDfy.Reporters; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.Examples { + [SuppressMessage("Performance", "CA1822:Mark members as static")] public class ReflectiveWithExamples { private readonly Story _story; @@ -20,18 +22,12 @@ public ReflectiveWithExamples() .BDDfy(); } - internal void GivenStepWith__FirstExample__PassedAsParameter(int firstExample) - { - firstExample.ShouldBeOneOf(1, 2); - } + internal void GivenStepWith__FirstExample__PassedAsParameter(int firstExample) => firstExample.ShouldBeOneOf(1, 2); - internal void AndGivenStepWith__SecondExample__AccessedViaProperty() - { - SecondExample.ShouldBeOneOf("foo", "bar"); - } + internal void AndGivenStepWith__SecondExample__AccessedViaProperty() => SecondExample.ShouldBeOneOf("foo", "bar"); [Fact] - public void Run() + public void RunExamplesUsingReflectiveScanner() { var reporter = new TextReporter(); reporter.Process(_story); diff --git a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs index c9e26a34..70ee13c0 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs @@ -1,10 +1,12 @@ using Shouldly; +using System.Diagnostics.CodeAnalysis; using System.Linq; using TestStack.BDDfy.Configuration; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.ReflectiveScanner { + [SuppressMessage("Performance", "CA1822:Mark members as static")] public class AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName { private class TypeWithDecoratedMethods @@ -20,8 +22,10 @@ public TypeWithDecoratedMethods() [Given("")] public void GivenWithEmptyString() { } + [When(" ")] public void WhenWithWhitespace() { } + [Then(null)] public void ThenWithNull() { } diff --git a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs index 28e5ef3d..f2cebd02 100644 --- a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs @@ -72,14 +72,17 @@ string createTitle() public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new(AppendPrefix(title, stepPrefix)); - private static string AppendPrefix(string title, string stepPrefix) + private static string AppendPrefix(string? title, string stepPrefix) { - if (!title.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase)) + var stepTitle = (title ?? string.Empty).Trim(); + + if (!stepTitle.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase)) { - if (title.Length == 0) return string.Format("{0} ", stepPrefix); - return string.Format("{0} {1}{2}", stepPrefix, title[..1].ToLower(), title[1..]); + if (stepTitle.Length == 0) return string.Format("{0} ", stepPrefix); + + return string.Format("{0} {1}{2}", stepPrefix, stepTitle[..1].ToLower(), stepTitle[1..]); } - return title; + return stepTitle; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Configuration/Scanners.cs b/src/TestStack.BDDfy/Configuration/Scanners.cs index a0b0775f..a4060e7c 100644 --- a/src/TestStack.BDDfy/Configuration/Scanners.cs +++ b/src/TestStack.BDDfy/Configuration/Scanners.cs @@ -38,6 +38,6 @@ public IEnumerable GetStepScanners(object objectUnderTest) public Func StoryMetadataScanner = () => new StoryAttributeMetadataScanner(); [Obsolete("This will be removed soon. Use Configurator.Humanizer.Humanize")] - public static Func Humanize = Configurator.Humanizer.Humanize; + public static Func Humanize = Configurator.Humanizer.Humanize; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs index 2b937dc6..adc471e3 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -33,17 +33,20 @@ internal static void PreserveStackTrace(Exception exception) public void Process(Story story) { var allSteps = story.Scenarios.SelectMany(s => s.Steps); - if (!allSteps.Any()) - return; + + if (!allSteps.Any()) return; var worseResult = story.Result; var stepWithWorseResult = allSteps.First(s => s.Result == worseResult); - + if (worseResult == Result.Failed || worseResult == Result.Inconclusive) { - PreserveStackTrace(stepWithWorseResult.Exception); - throw stepWithWorseResult.Exception; + if(stepWithWorseResult.Exception is not null) + { + PreserveStackTrace(stepWithWorseResult.Exception!); + throw stepWithWorseResult.Exception; + } } if (worseResult == Result.NotImplemented) diff --git a/src/TestStack.BDDfy/Processors/StoryCache.cs b/src/TestStack.BDDfy/Processors/StoryCache.cs index 20437f5d..61c3d64f 100644 --- a/src/TestStack.BDDfy/Processors/StoryCache.cs +++ b/src/TestStack.BDDfy/Processors/StoryCache.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace TestStack.BDDfy.Processors { @@ -6,30 +7,19 @@ public class StoryCache : IProcessor { private static readonly IList Cache = []; - public ProcessType ProcessType - { - get { return ProcessType.Finally; } - } + public ProcessType ProcessType => ProcessType.Finally; public void Process(Story story) { - foreach (var scenario in story.Scenarios) + foreach (var scenario in story.Scenarios.Where(s=>s.TestObject is not null)) { - TestContext.ClearContextFor(scenario.TestObject); - scenario.TestObject = null; - foreach (var step in scenario.Steps) - step.Action = null; + TestContext.ClearContextFor(scenario.TestObject!); + foreach (var step in scenario.Steps) step.Action = null; } Cache.Add(story); } - public static IEnumerable Stories - { - get - { - return Cache; - } - } + public static IEnumerable Stories => Cache; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 95b14c20..1c4bb94c 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -131,7 +131,7 @@ public NotifyPropertyChangedInvocatorAttribute(string parameterName) ParameterName = parameterName; } - public string ParameterName { get; private set; } + public string? ParameterName { get; private set; } } /// diff --git a/src/TestStack.BDDfy/Reporters/ConsoleReporter.cs b/src/TestStack.BDDfy/Reporters/ConsoleReporter.cs index 1ef9c97c..ec5f8c93 100644 --- a/src/TestStack.BDDfy/Reporters/ConsoleReporter.cs +++ b/src/TestStack.BDDfy/Reporters/ConsoleReporter.cs @@ -9,7 +9,7 @@ protected override void Write(string text, params object[] args) Console.Write(text, args); } - protected override void WriteLine(string text = null) + protected override void WriteLine(string? text = null) { Console.WriteLine(text); } diff --git a/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReport.cs b/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReport.cs index eff9673a..7f45196d 100644 --- a/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReport.cs +++ b/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReport.cs @@ -4,6 +4,6 @@ namespace TestStack.BDDfy.Reporters.Diagnostics { public class DiagnosticsReport { - public IList Stories { get; set; } + public IList Stories { get; set; } = []; } } diff --git a/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReportBuilder.cs b/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReportBuilder.cs index 765f34ad..ee687155 100644 --- a/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReportBuilder.cs +++ b/src/TestStack.BDDfy/Reporters/Diagnostics/DiagnosticsReportBuilder.cs @@ -17,29 +17,27 @@ public string CreateReport(FileReportModel model) return _serializer.Serialize(rootObject); } - public IList GetDiagnosticData(FileReportModel viewModel) + internal static IList GetDiagnosticData(FileReportModel viewModel) { var graph = new List(); foreach (var story in viewModel.Stories) { - var name = story.Namespace; - if (story.Metadata != null) - name = story.Metadata.Title; + var name = story.Metadata?.Title ?? story.Namespace; graph.Add(new StoryDiagnostic { - Name = name, + Name = name ?? "Story", Duration = story.Scenarios.Sum(x => x.Duration.Milliseconds), - Scenarios = story.Scenarios.Select(scenario => new StoryDiagnostic.Scenario() + Scenarios = [.. story.Scenarios.Select(scenario => new StoryDiagnostic.Scenario() { - Name = scenario.Title, + Name = scenario.Title ?? "Scenario", Duration = scenario.Duration.Milliseconds, - Steps = scenario.Steps.Select(step => new StoryDiagnostic.Step() + Steps = [.. scenario.Steps.Select(step => new StoryDiagnostic.Step() { - Name = step.Title, + Name = step.Title ?? "Step", Duration = step.Duration.Milliseconds - }).ToList() - }).ToList() + })] + })] }); } diff --git a/src/TestStack.BDDfy/Reporters/Diagnostics/StoryDiagnostic.cs b/src/TestStack.BDDfy/Reporters/Diagnostics/StoryDiagnostic.cs index 95eebfc5..fe28da7e 100644 --- a/src/TestStack.BDDfy/Reporters/Diagnostics/StoryDiagnostic.cs +++ b/src/TestStack.BDDfy/Reporters/Diagnostics/StoryDiagnostic.cs @@ -4,20 +4,20 @@ namespace TestStack.BDDfy.Reporters.Diagnostics { public class StoryDiagnostic { - public string Name { get; set; } + public string Name { get; set; } = null!; public int Duration { get; set; } - public List Scenarios { get; set; } + public List Scenarios { get; set; } = []; public class Scenario { - public string Name { get; set; } + public string Name { get; set; } = null!; public int Duration { get; set; } - public List Steps { get; set; } + public List Steps { get; set; } = []; } public class Step { - public string Name { get; set; } + public string Name { get; set; } = null!; public int Duration { get; set; } } } diff --git a/src/TestStack.BDDfy/Reporters/FileReportModel.cs b/src/TestStack.BDDfy/Reporters/FileReportModel.cs index 8b50e30c..9356b29a 100644 --- a/src/TestStack.BDDfy/Reporters/FileReportModel.cs +++ b/src/TestStack.BDDfy/Reporters/FileReportModel.cs @@ -33,8 +33,8 @@ from story in groupedByStories.Union(groupedByNamespace) Metadata = story.First().Metadata, Namespace = story.Key, Result = story.First().Result, - Scenarios = story.SelectMany(s => s.Scenarios).OrderBy(s => s.Title).ToList() // order scenarios by title, - }; + Scenarios = [.. story.SelectMany(s => s.Scenarios).OrderBy(s => s.Title)], + }; return aggregatedStories; } diff --git a/src/TestStack.BDDfy/Reporters/FileReportSummaryModel.cs b/src/TestStack.BDDfy/Reporters/FileReportSummaryModel.cs index 80d0e91a..d66a417d 100644 --- a/src/TestStack.BDDfy/Reporters/FileReportSummaryModel.cs +++ b/src/TestStack.BDDfy/Reporters/FileReportSummaryModel.cs @@ -3,53 +3,23 @@ namespace TestStack.BDDfy.Reporters { - public class FileReportSummaryModel + public class FileReportSummaryModel(ReportModel reportModel) { - readonly IEnumerable _stories; - readonly IEnumerable _scenarios; - - public FileReportSummaryModel(ReportModel reportModel) - { - _stories = reportModel.Stories; - _scenarios = _stories.SelectMany(s => s.Scenarios).ToList(); - } - - public int Namespaces - { - get - { - return _stories.Where(b => b.Metadata == null).GroupBy(s => s.Namespace).Count(); - } - } - - public int Scenarios - { - get { return _stories.SelectMany(s => s.Scenarios).Count(); } - } - - public int Stories - { - get { return _stories.Where(b => b.Metadata != null).GroupBy(b => b.Metadata.Type).Count(); } - } - - public int Passed - { - get { return _scenarios.Count(b => b.Result == Result.Passed); } - } - - public int Failed - { - get { return _scenarios.Count(b => b.Result == Result.Failed); } - } - - public int Inconclusive - { - get { return _scenarios.Count(b => b.Result == Result.Inconclusive); } - } - - public int NotImplemented - { - get { return _scenarios.Count(b => b.Result == Result.NotImplemented); } - } + readonly IEnumerable _stories = reportModel.Stories; + readonly IEnumerable _scenarios = [.. reportModel.Stories.SelectMany(s => s.Scenarios)]; + + public int Namespaces => _stories.Where(b => b.Metadata == null).GroupBy(s => s.Namespace).Count(); + + public int Scenarios => _stories.SelectMany(s => s.Scenarios).Count(); + + public int Stories => _stories.Where(b => b.Metadata is not null).GroupBy(b => b.Metadata.Type).Count(); + + public int Passed => _scenarios.Count(b => b.Result is Result.Passed); + + public int Failed => _scenarios.Count(b => b.Result is Result.Failed); + + public int Inconclusive => _scenarios.Count(b => b.Result is Result.Inconclusive); + + public int NotImplemented => _scenarios.Count(b => b.Result is Result.NotImplemented); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Reporters/Html/ClassicReportBuilder.cs b/src/TestStack.BDDfy/Reporters/Html/ClassicReportBuilder.cs index 32504488..3abbab85 100644 --- a/src/TestStack.BDDfy/Reporters/Html/ClassicReportBuilder.cs +++ b/src/TestStack.BDDfy/Reporters/Html/ClassicReportBuilder.cs @@ -9,24 +9,16 @@ namespace TestStack.BDDfy.Reporters.Html { public class ClassicReportBuilder : IReportBuilder { - private HtmlReportModel _model; + private HtmlReportModel _model = null!; readonly StringBuilder _html; const int TabIndentation = 2; int _tabCount; - public ClassicReportBuilder() - { - _html = new StringBuilder(); - } - - string IReportBuilder.CreateReport(FileReportModel model) - { - return CreateReport(model as HtmlReportModel); - } + public ClassicReportBuilder() => _html = new StringBuilder(); - public string CreateReport(HtmlReportModel model) + public string CreateReport(FileReportModel model) { - _model = model; + _model = model as HtmlReportModel ?? throw new InvalidCastException("Model must be of type HtmlReportModel"); AddLine(""); using (OpenTag(HtmlTag.html)) { @@ -164,7 +156,7 @@ private void AddStory(ReportModel.Story story) { foreach (var scenario in scenariosGroupedById) { - AddScenario(scenario.ToArray()); + AddScenario([.. scenario]); } } } @@ -187,7 +179,7 @@ private void AddScenarioWithExamples(ReportModel.Scenario[] scenarioGroup) var firstScenario = scenarioGroup.First(); var scenarioResult = (Result)scenarioGroup.Max(s => (int)s.Result); - AddLine(string.Format("
{2}{3}
", scenarioResult, firstScenario.Id, WebUtility.HtmlEncode(firstScenario.Title), FormatTags(firstScenario.Tags))); + AddLine(string.Format("
{2}{3}
", scenarioResult, firstScenario.Id, WebUtility.HtmlEncode(firstScenario.Title), ClassicReportBuilder.FormatTags(firstScenario.Tags))); using (OpenTag(string.Format("
    ", firstScenario.Id), HtmlTag.ul)) { @@ -195,8 +187,8 @@ private void AddScenarioWithExamples(ReportModel.Scenario[] scenarioGroup) { using (OpenTag(string.Format("
  • ", step.ExecutionOrder), HtmlTag.li)) { - var titleLines = WebUtility.HtmlEncode(step.Title) - .Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var titleLines = WebUtility.HtmlEncode(step.Title).Split([Environment.NewLine], StringSplitOptions.None); + var title = titleLines[0]; AddLine(string.Format("{0}", title)); @@ -210,14 +202,13 @@ private void AddScenarioWithExamples(ReportModel.Scenario[] scenarioGroup) } } - private string FormatTags(List tags) - { - return string.Join(string.Empty, tags.Select(t => string.Format("
    {0}
    ", t))); - } + private static string FormatTags(List tags) => string.Join(string.Empty, tags.Select(t => string.Format("
    {0}
    ", t))); private void AddExamples(ReportModel.Scenario[] scenarioGroup) { var firstScenario = scenarioGroup.First(); + if(firstScenario.Example is null) throw new InvalidOperationException("First scenario in the group must have an example"); + var scenarioResult = (Result)scenarioGroup.Max(s => (int)s.Result); using (OpenTag("
  • ", HtmlTag.li)) @@ -246,25 +237,25 @@ private void AddExampleRow(ReportModel.Scenario scenario, Result scenarioResult) using (OpenTag("", HtmlTag.tr)) { AddLine(string.Format("", scenario.Result)); - foreach (var exampleValue in scenario.Example.Values) + foreach (var exampleValue in scenario.Example?.Values ?? []) AddLine(string.Format("{0}", WebUtility.HtmlEncode(exampleValue.GetValueAsString()))); - if (scenarioResult != Result.Failed) + if (scenarioResult is not Result.Failed) return; using (OpenTag("", HtmlTag.td)) { - var failingStep = scenario.Steps.FirstOrDefault(s => s.Result == Result.Failed); + var failingStep = scenario.Steps.FirstOrDefault(s => s.Result is Result.Failed); - if (failingStep == null) - return; + if (failingStep is null) return; var exceptionId = Configurator.IdGenerator.GetStepId(); - var encodedExceptionMessage = WebUtility.HtmlEncode(failingStep.Exception.Message); + var encodedExceptionMessage = WebUtility.HtmlEncode(failingStep.Exception?.Message); AddLine(string.Format("{1}", exceptionId, encodedExceptionMessage)); using (OpenTag(string.Format("
    ", exceptionId), HtmlTag.div)) { - AddLine(string.Format("{0}", failingStep.Exception.StackTrace)); + if(failingStep.Exception is not null) + AddLine(string.Format("{0}", failingStep.Exception.StackTrace)); } } } @@ -272,24 +263,24 @@ private void AddExampleRow(ReportModel.Scenario scenario, Result scenarioResult) private void AddScenario(ReportModel.Scenario scenario) { - AddLine(string.Format("
    {2}{3}
    ", scenario.Result, scenario.Id, scenario.Title, FormatTags(scenario.Tags))); + AddLine(string.Format("
    {2}{3}
    ", scenario.Result, scenario.Id, scenario.Title, ClassicReportBuilder.FormatTags(scenario.Tags))); using (OpenTag(string.Format("
      ", scenario.Id), HtmlTag.ul)) { foreach (var step in scenario.Steps.Where(s => s.ShouldReport)) { string stepClass = string.Empty; - var reportException = step.Exception != null && step.Result == Result.Failed; + var reportException = step.Exception is not null && step.Result == Result.Failed; string canToggle = reportException ? "canToggle" : string.Empty; using (OpenTag(string.Format("
    • ", step.Result, stepClass, step.ExecutionOrder, canToggle, step.Id), HtmlTag.li)) { - var titleLines = step.Title.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var titleLines = step.Title.Split([Environment.NewLine], StringSplitOptions.None); var title = titleLines[0]; if (reportException) { stepClass = step.Result + "Exception"; - if (!string.IsNullOrEmpty(step.Exception.Message)) + if (!string.IsNullOrEmpty(step.Exception!.Message)) title += " [Exception Message: '" + step.Exception.Message + "']"; } @@ -302,7 +293,7 @@ private void AddScenario(ReportModel.Scenario scenario) { using (OpenTag(string.Format("
      ", stepClass, step.Id), HtmlTag.div)) { - AddLine(string.Format("{0}", step.Exception.StackTrace)); + AddLine(string.Format("{0}", step.Exception!.StackTrace)); } } } @@ -367,7 +358,7 @@ private void AddLine(string line) _html.AppendLine(string.Empty.PadLeft(tabWidth) + line); } - private void EmbedCssFile(string cssContent, string htmlComment = null) + private void EmbedCssFile(string? cssContent, string? htmlComment = null) { using (OpenTag("