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/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.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..f2cebd02 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(); @@ -73,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/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/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/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..79222d1f --- /dev/null +++ b/src/TestStack.BDDfy/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// 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 = "We want most of the classes to sit in root namespace", Scope = "namespace", Target = "~N:TestStack.BDDfy")] +[assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Scope = "type", Target = "~T:TestStack.BDDfy.Annotations.ImplicitUseTargetFlags")] +[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Scope = "namespace", Target = "~N:TestStack.BDDfy.Annotations")] diff --git a/src/TestStack.BDDfy/IStepExecutor.cs b/src/TestStack.BDDfy/IStepExecutor.cs index c0979528..c339b52f 100644 --- a/src/TestStack.BDDfy/IStepExecutor.cs +++ b/src/TestStack.BDDfy/IStepExecutor.cs @@ -2,6 +2,6 @@ namespace TestStack.BDDfy { public interface IStepExecutor { - object Execute(Step step, object testObject); + object? Execute(Step step, object testObject); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/ITestContext.cs b/src/TestStack.BDDfy/ITestContext.cs index 2ad651f1..102d95d4 100644 --- a/src/TestStack.BDDfy/ITestContext.cs +++ b/src/TestStack.BDDfy/ITestContext.cs @@ -5,8 +5,8 @@ namespace TestStack.BDDfy public interface ITestContext { object TestObject { get; } - ExampleTable Examples { get; set; } - IFluentScanner FluentScanner { get; set; } + ExampleTable? Examples { get; set; } + IFluentScanner? FluentScanner { get; set; } List Tags { get; } } } \ No newline at end of file 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..adc471e3 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -26,25 +26,27 @@ 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) { 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/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/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 95b14c20..19a8a9b8 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -1,253 +1,13 @@ using System; using System.Diagnostics.CodeAnalysis; -#pragma warning disable CS1591 //Missing XML comment for publicly visible type or member -#pragma warning disable CS9113 // Parameter is unread. // ReSharper disable UnusedMember.Global -// ReSharper disable UnusedParameter.Local // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable IntroduceOptionalParameters.Global -// ReSharper disable MemberCanBeProtected.Global // ReSharper disable InconsistentNaming namespace TestStack.BDDfy.Annotations { - /// - /// Indicates that the value of the marked element could be null sometimes, - /// so the check for null is necessary before its usage - /// - /// - /// [CanBeNull] public object Test() { return null; } - /// public void UseTest() { - /// var p = Test(); - /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | - AttributeTargets.Property | AttributeTargets.Delegate | - AttributeTargets.Field, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class CanBeNullAttribute : Attribute { } - - /// - /// Indicates that the value of the marked element could never be null - /// - /// - /// [NotNull] public object Foo() { - /// return null; // Warning: Possible 'null' assignment - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | - AttributeTargets.Property | AttributeTargets.Delegate | - AttributeTargets.Field, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class NotNullAttribute : Attribute { } - - /// - /// Indicates that the marked method builds string by format pattern and (optional) arguments. - /// Parameter, which contains format string, should be given in constructor. The format string - /// should be in -like form - /// - /// - /// [StringFormatMethod("message")] - /// public void ShowError(string message, params object[] args) { /* do something */ } - /// public void Foo() { - /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string - /// } - /// - /// - /// Specifies which parameter of an annotated method should be treated as format-string - /// - [AttributeUsage( - AttributeTargets.Constructor | AttributeTargets.Method, - AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class StringFormatMethodAttribute(string formatParameterName) : Attribute - { - public string FormatParameterName { get; private set; } = formatParameterName; - } - - /// - /// Indicates that the function argument should be string literal and match one - /// of the parameters of the caller function. For example, ReSharper annotates - /// the parameter of - /// - /// - /// public void Foo(string param) { - /// if (param == null) - /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol - /// } - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class InvokerParameterNameAttribute : Attribute { } - - /// - /// Indicates that the method is contained in a type that implements - /// interface - /// and this method is used to notify that some property value changed - /// - /// - /// The method should be non-static and conform to one of the supported signatures: - /// - /// NotifyChanged(string) - /// NotifyChanged(params string[]) - /// NotifyChanged{T}(Expression{Func{T}}) - /// NotifyChanged{T,U}(Expression{Func{T,U}}) - /// SetProperty{T}(ref T, T, string) - /// - /// - /// - /// public class Foo : INotifyPropertyChanged { - /// public event PropertyChangedEventHandler PropertyChanged; - /// [NotifyPropertyChangedInvocator] - /// protected virtual void NotifyChanged(string propertyName) { ... } - /// - /// private string _name; - /// public string Name { - /// get { return _name; } - /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } - /// } - /// } - /// - /// Examples of generated notifications: - /// - /// NotifyChanged("Property") - /// NotifyChanged(() => Property) - /// NotifyChanged((VM x) => x.Property) - /// SetProperty(ref myField, value, "Property") - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute - { - public NotifyPropertyChangedInvocatorAttribute() { } - public NotifyPropertyChangedInvocatorAttribute(string parameterName) - { - ParameterName = parameterName; - } - - public string ParameterName { get; private set; } - } - - /// - /// Describes dependency between method input and output - /// - /// - ///

Function Definition Table syntax:

- /// - /// FDT ::= FDTRow [;FDTRow]* - /// FDTRow ::= Input => Output | Output <= Input - /// Input ::= ParameterName: Value [, Input]* - /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} - /// Value ::= true | false | null | notnull | canbenull - /// - /// If method has single input parameter, it's name could be omitted.
- /// Using halt (or void/nothing, which is the same) - /// for method output means that the methos doesn't return normally.
- /// canbenull annotation is only applicable for output parameters.
- /// You can use multiple [ContractAnnotation] for each FDT row, - /// or use single attribute with rows separated by semicolon.
- ///
- /// - /// - /// [ContractAnnotation("=> halt")] - /// public void TerminationMethod() - /// - /// - /// [ContractAnnotation("halt <= condition: false")] - /// public void Assert(bool condition, string text) // regular assertion method - /// - /// - /// [ContractAnnotation("s:null => true")] - /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() - /// - /// - /// // A method that returns null if the parameter is null, and not null if the parameter is not null - /// [ContractAnnotation("null => null; notnull => notnull")] - /// public object Transform(object data) - /// - /// - /// [ContractAnnotation("s:null=>false; =>true,result:notnull; =>false, result:null")] - /// public bool TryParse(string s, out Person result) - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) : Attribute - { - public ContractAnnotationAttribute([NotNull] string contract) - : this(contract, false) { } - - public string Contract { get; private set; } = contract; - public bool ForceFullStates { get; private set; } = forceFullStates; - } - - /// - /// Indicates that marked element should be localized or not - /// - /// - /// [LocalizationRequiredAttribute(true)] - /// public class Foo { - /// private string str = "my string"; // Warning: Localizable string - /// } - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class LocalizationRequiredAttribute(bool required) : Attribute - { - public LocalizationRequiredAttribute() : this(true) { } - - public bool Required { get; private set; } = required; - } - - /// - /// Indicates that the value of the marked type (or its derivatives) - /// cannot be compared using '==' or '!=' operators and Equals() - /// should be used instead. However, using '==' or '!=' for comparison - /// with null is always permitted. - /// - /// - /// [CannotApplyEqualityOperator] - /// class NoEquality { } - /// class UsesNoEquality { - /// public void Test() { - /// var ca1 = new NoEquality(); - /// var ca2 = new NoEquality(); - /// if (ca1 != null) { // OK - /// bool condition = ca1 == ca2; // Warning - /// } - /// } - /// } - /// - [AttributeUsage( - AttributeTargets.Interface | AttributeTargets.Class | - AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } - - /// - /// When applied to a target attribute, specifies a requirement for any type marked - /// with the target attribute to implement or inherit specific type or types. - /// - /// - /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement - /// public class ComponentAttribute : Attribute { } - /// [Component] // ComponentAttribute requires implementing IComponent interface - /// public class MyComponent : IComponent { } - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] - [BaseTypeRequired(typeof(Attribute))] - [ExcludeFromCodeCoverage] - public sealed class BaseTypeRequiredAttribute([NotNull] Type baseType) : Attribute - { - [NotNull] public Type BaseType { get; private set; } = baseType; - } - /// /// Indicates that the marked symbol is used implicitly /// (e.g. via reflection, in external library), so this symbol @@ -326,268 +86,4 @@ public enum ImplicitUseTargetFlags /// Entity marked with attribute and all its members considered used WithMembers = Itself | Members } - - /// - /// This attribute is intended to mark publicly available API - /// which should not be removed and so is treated as used - /// - [ExcludeFromCodeCoverage] - public sealed class PublicAPIAttribute : MeansImplicitUseAttribute - { - public PublicAPIAttribute() { } - public PublicAPIAttribute([NotNull] string comment) - { - Comment = comment; - } - - [NotNull] public string Comment { get; private set; } - } - - /// - /// Tells code analysis engine if the parameter is completely handled - /// when the invoked method is on stack. If the parameter is a delegate, - /// indicates that delegate is executed while the method is executed. - /// If the parameter is an enumerable, indicates that it is enumerated - /// while the method is executed - /// - [AttributeUsage(AttributeTargets.Parameter, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class InstantHandleAttribute : Attribute { } - - /// - /// Indicates that a parameter is a path to a file or a folder - /// within a web project. Path can be relative or absolute, - /// starting from web root (~) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public class PathReferenceAttribute : Attribute - { - public PathReferenceAttribute() { } - public PathReferenceAttribute([PathReference] string basePath) - { - BasePath = basePath; - } - - [NotNull] public string BasePath { get; private set; } - } - - // ASP.NET MVC attributes - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcAreaMasterLocationFormatAttribute(string format) : Attribute - { - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcAreaPartialViewLocationFormatAttribute(string format) : Attribute - { - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcAreaViewLocationFormatAttribute(string format) : Attribute - { - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcMasterLocationFormatAttribute(string format) : Attribute - { - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcPartialViewLocationFormatAttribute(string format) : Attribute - { - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcViewLocationFormatAttribute(string format) : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC action. If applied to a method, the MVC action name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcActionAttribute : Attribute - { - public AspMvcActionAttribute() { } - public AspMvcActionAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [NotNull] public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcAreaAttribute : PathReferenceAttribute - { - public AspMvcAreaAttribute() { } - public AspMvcAreaAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [NotNull] public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that - /// the parameter is an MVC controller. If applied to a method, - /// the MVC controller name is calculated implicitly from the context. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcControllerAttribute : Attribute - { - public AspMvcControllerAttribute() { } - public AspMvcControllerAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [NotNull] public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(String, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcMasterAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(String, Object) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcModelTypeAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that - /// the parameter is an MVC partial view. If applied to a method, - /// the MVC partial view name is calculated implicitly from the context. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcPartialViewAttribute : PathReferenceAttribute { } - - /// - /// ASP.NET MVC attribute. Allows disabling all inspections - /// for MVC views within a class or a method. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcSupressViewErrorAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcDisplayTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcEditorTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. - /// Use this attribute for custom wrappers similar to - /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view. If applied to a method, the MVC view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(Object) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcViewAttribute : PathReferenceAttribute { } - - /// - /// ASP.NET MVC attribute. When applied to a parameter of an attribute, - /// indicates that this parameter is an MVC action name - /// - /// - /// [ActionName("Foo")] - /// public ActionResult Login(string returnUrl) { - /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK - /// return RedirectToAction("Bar"); // Error: Cannot resolve action - /// } - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] - [ExcludeFromCodeCoverage] - public sealed class AspMvcActionSelectorAttribute : Attribute { } - - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Field, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class HtmlElementAttributesAttribute : Attribute - { - public HtmlElementAttributesAttribute() { } - public HtmlElementAttributesAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] public string Name { get; private set; } - } - - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Field | - AttributeTargets.Property, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class HtmlAttributeValueAttribute([NotNull] string name) : Attribute - { - [NotNull] public string Name { get; private set; } = name; - } - - // Razor attributes - - /// - /// Razor attribute. Indicates that a parameter or a method is a Razor section. - /// Use this attribute for custom wrappers similar to - /// System.Web.WebPages.WebPageBase.RenderSection(String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, Inherited = true)] - [ExcludeFromCodeCoverage] - public sealed class RazorSectionAttribute : Attribute { } } \ No newline at end of file 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..71a6a1fa 100644 --- a/src/TestStack.BDDfy/Reporters/FileReportModel.cs +++ b/src/TestStack.BDDfy/Reporters/FileReportModel.cs @@ -15,15 +15,15 @@ public IEnumerable Stories get { var groupedByNamespace = from story in _stories - where story.Metadata == null + where story.Metadata is null orderby story.Namespace group story by story.Namespace into g select g; var groupedByStories = from story in _stories - where story.Metadata != null - orderby story.Metadata.Title - group story by story.Metadata.Type.Name into g + where story.Metadata is not null + orderby story.Metadata!.Title + group story by story.Metadata!.Type.Name into g select g; var aggregatedStories = @@ -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..12b2c0a9 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..2575eed4 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,9 @@ 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) ?? []; + if (titleLines.Length == 0) continue; + var title = titleLines[0]; AddLine(string.Format("{0}", title)); @@ -210,14 +203,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 +238,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 +264,25 @@ 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) ?? []; + if (titleLines.Length == 0) continue; 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 +295,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 +360,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("