From a02693518719b3e5a684e96c095fb937746e0a3c Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Wed, 29 Oct 2025 20:13:15 +0000 Subject: [PATCH 01/34] clean up appveyor related files some random refactoring added IStepTitleFactory and IFluentScannerFactory --- appveyor.deploy.yml | 14 -- appveyor.yml | 17 -- build.cake | 83 ---------- build.ps1 | 145 ------------------ deploy.cake | 89 ----------- deploy.ps1 | 145 ------------------ .../UseExamplesWithFluentApi.cs | 6 +- ...ultHumanizerTests.cs => HumanizerTests.cs} | 13 +- .../Abstractions/DefaultStepTitleFactory.cs | 80 ++++++++++ .../Abstractions/IStepTitleFactory.cs | 17 ++ .../Configuration/Configurator.cs | 9 ++ .../Configuration/IHumanizer.cs | 3 + src/TestStack.BDDfy/DefaultHumanizer.cs | 105 ++++++------- src/TestStack.BDDfy/Engine.cs | 6 - .../Factories/DefaultFluentScannerFactory.cs | 6 + .../Factories/IFluentScannerFactory.cs | 12 ++ .../Processors/ExceptionProcessor.cs | 24 --- .../Processors/UnusedExampleException.cs | 26 +--- .../Properties/AssemblyInfo.cs | 4 +- src/TestStack.BDDfy/Reporters/FileHelpers.cs | 7 - .../Examples/UnassignableExampleException.cs | 15 +- .../Scanners/StepScanners/Fluent/FluentApi.cs | 10 +- .../StepScanners/Fluent/FluentScanner.cs | 132 +++++++--------- src/TestStack.BDDfy/SerializableAttribute.cs | 10 -- src/TestStack.BDDfy/TestContext.cs | 6 +- src/TestStack.BDDfy/TypeExtensions.cs | 39 +---- 26 files changed, 267 insertions(+), 756 deletions(-) delete mode 100644 appveyor.deploy.yml delete mode 100644 appveyor.yml delete mode 100644 build.cake delete mode 100644 build.ps1 delete mode 100644 deploy.cake delete mode 100644 deploy.ps1 rename src/TestStack.BDDfy.Tests/{DefaultHumanizerTests.cs => HumanizerTests.cs} (89%) create mode 100644 src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs create mode 100644 src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs create mode 100644 src/TestStack.BDDfy/Factories/DefaultFluentScannerFactory.cs create mode 100644 src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs delete mode 100644 src/TestStack.BDDfy/SerializableAttribute.cs diff --git a/appveyor.deploy.yml b/appveyor.deploy.yml deleted file mode 100644 index b9766709..00000000 --- a/appveyor.deploy.yml +++ /dev/null @@ -1,14 +0,0 @@ -assembly_info: - patch: false - -platform: - - Any CPU - -configuration: - - Debug - -build_script: - - ps: ./deploy.ps1 - -test: off -skip_non_tags: true \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index aaaa0545..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -assembly_info: - patch: false - -platform: - - Any CPU - -configuration: - - Release - -build_script: - - powershell .\build.ps1 - -test: off -skip_tags: true - -cache: - - src\packages -> **\packages.config # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified diff --git a/build.cake b/build.cake deleted file mode 100644 index 27e2811d..00000000 --- a/build.cake +++ /dev/null @@ -1,83 +0,0 @@ -#tool "nuget:?package=GitReleaseNotes" -#tool "nuget:?package=GitVersion.CommandLine" - -var target = Argument("target", "Default"); -var bddfyProj = "./src/TestStack.BDDfy/TestStack.BDDfy.csproj"; -var outputDir = "./artifacts/"; - -Task("Clean") - .Does(() => { - if (DirectoryExists(outputDir)) - { - DeleteDirectory(outputDir, recursive:true); - } - }); - -Task("Restore") - .Does(() => { - DotNetCoreRestore("src"); - }); - -GitVersion versionInfo = null; -Task("Version") - .Does(() => { - GitVersion(new GitVersionSettings{ - UpdateAssemblyInfo = true, - OutputType = GitVersionOutput.BuildServer - }); - versionInfo = GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); - }); - -Task("Build") - .IsDependentOn("Clean") - .IsDependentOn("Version") - .IsDependentOn("Restore") - .Does(() => { - MSBuild("./src/TestStack.BDDfy.sln"); - }); - -Task("Test") - .IsDependentOn("Build") - .Does(() => { - DotNetCoreTest("./src/TestStack.BDDfy.Tests"); - DotNetCoreTest("./src/Samples/TestStack.BDDfy.Samples"); - }); - -Task("Package") - .IsDependentOn("Test") - .Does(() => { - var settings = new DotNetCorePackSettings - { - ArgumentCustomization = args=> args.Append(" --include-symbols /p:PackageVersion=" + versionInfo.NuGetVersion), - OutputDirectory = outputDir, - NoBuild = true - }; - - DotNetCorePack(bddfyProj, settings); - - var releaseNotesExitCode = StartProcess( - @"tools\GitReleaseNotes\tools\gitreleasenotes.exe", - new ProcessSettings { Arguments = ". /o artifacts/releasenotes.md" }); - - if (string.IsNullOrEmpty(System.IO.File.ReadAllText("./artifacts/releasenotes.md"))) - System.IO.File.WriteAllText("./artifacts/releasenotes.md", "No issues closed since last release"); - - if (releaseNotesExitCode != 0) throw new Exception("Failed to generate release notes"); - - System.IO.File.WriteAllLines(outputDir + "artifacts", new[]{ - "nuget:TestStack.BDDfy." + versionInfo.NuGetVersion + ".nupkg", - "nugetSymbols:TestStack.BDDfy." + versionInfo.NuGetVersion + ".symbols.nupkg", - "releaseNotes:releasenotes.md" - }); - - if (AppVeyor.IsRunningOnAppVeyor) - { - foreach (var file in GetFiles(outputDir + "**/*")) - AppVeyor.UploadArtifact(file.FullPath); - } - }); - -Task("Default") - .IsDependentOn("Package"); - -RunTarget(target); \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 18b85605..00000000 --- a/build.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER Experimental -Tells Cake to use the latest Roslyn release. -.PARAMETER WhatIf -Performs a dry run of the build script. -No tasks will be executed. -.PARAMETER Mono -Tells Cake to use the Mono scripting engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -http://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target = "Default", - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [Alias("DryRun","Noop")] - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -Write-Host "Preparing to run build script..." - -$PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition; -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$NUGET_URL = "http://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" - -# Should we use mono? -$UseMono = ""; -if($Mono.IsPresent) { - Write-Verbose -Message "Using the Mono based scripting engine." - $UseMono = "-mono" -} - -# Should we use the new Roslyn? -$UseExperimental = ""; -if($Experimental.IsPresent -and !($Mono.IsPresent)) { - Write-Verbose -Message "Using experimental version of Roslyn." - $UseExperimental = "-experimental" -} - -# Is this a dry run? -$UseDryRun = ""; -if($WhatIf.IsPresent) { - $UseDryRun = "-dryrun" -} - -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null -} - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { Invoke-WebRequest -Uri http://cakebuild.net/download/bootstrapper/packages -OutFile $PACKAGES_CONFIG } catch { - Throw "Could not download packages.config." - } -} - -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } -} - -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." - } - Write-Verbose -Message ($NuGetOutput | out-string) - Pop-Location -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} - -# Start Cake -Write-Host "Running build script..." -Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE diff --git a/deploy.cake b/deploy.cake deleted file mode 100644 index f3718c66..00000000 --- a/deploy.cake +++ /dev/null @@ -1,89 +0,0 @@ -#addin "Cake.Json" - -using System.Net; -using System.Linq; - -var target = Argument("target", "Default"); - -string Get(string url) -{ - var assetsRequest = WebRequest.CreateHttp(url); - assetsRequest.Method = "GET"; - assetsRequest.Accept = "application/vnd.github.v3+json"; - assetsRequest.UserAgent = "BuildScript"; - - using (var assetsResponse = assetsRequest.GetResponse()) - { - var assetsStream = assetsResponse.GetResponseStream(); - var assetsReader = new StreamReader(assetsStream); - var assetsBody = assetsReader.ReadToEnd(); - return assetsBody; - } -} - -Task("EnsureRequirements") - .Does(() => - { - if (!AppVeyor.IsRunningOnAppVeyor) - throw new Exception("Deployment should happen via appveyor"); - - var isTag = - AppVeyor.Environment.Repository.Tag.IsTag && - !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); - if (!isTag) - throw new Exception("Deployment should happen from a published GitHub release"); - }); - -var tag = ""; - -Task("UpdateVersionInfo") - .IsDependentOn("EnsureRequirements") - .Does(() => - { - tag = AppVeyor.Environment.Repository.Tag.Name; - AppVeyor.UpdateBuildVersion(tag); - }); - -Task("DownloadGitHubReleaseArtifacts") - .IsDependentOn("UpdateVersionInfo") - .Does(() => - { - var assets_url = ParseJson(Get("https://api.github.com/repos/teststack/teststack.bddfy/releases/tags/" + tag)) - .GetValue("assets_url").Value(); - EnsureDirectoryExists("./releaseArtifacts"); - foreach(var asset in DeserializeJson(Get(assets_url))) - { - DownloadFile(asset.Value("browser_download_url"), "./releaseArtifacts/" + asset.Value("name")); - } - }); - -Task("DeployNuget") - .IsDependentOn("DownloadGitHubReleaseArtifacts") - .Does(() => - { - // Turns .artifacts file into a lookup - var fileLookup = System.IO.File - .ReadAllLines("./releaseArtifacts/artifacts") - .Select(l => l.Split(':')) - .ToDictionary(v => v[0], v => v[1]); - - NuGetPush( - "./releaseArtifacts/" + fileLookup["nuget"], - new NuGetPushSettings { - ApiKey = EnvironmentVariable("NuGetApiKey"), - Source = "https://www.nuget.org/api/v2/package" - }); - }); - -Task("Deploy") - .IsDependentOn("DeployNuget"); - -Task("Default") - .IsDependentOn("Deploy"); - -Task("Verify") - .Does(() => { - // Nothing, used to make sure the script compiles - }); - -RunTarget(target); \ No newline at end of file diff --git a/deploy.ps1 b/deploy.ps1 deleted file mode 100644 index eaf1d62d..00000000 --- a/deploy.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER Experimental -Tells Cake to use the latest Roslyn release. -.PARAMETER WhatIf -Performs a dry run of the build script. -No tasks will be executed. -.PARAMETER Mono -Tells Cake to use the Mono scripting engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -http://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "deploy.cake", - [string]$Target = "Default", - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [Alias("DryRun","Noop")] - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -Write-Host "Preparing to run build script..." - -$PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition; -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$NUGET_URL = "http://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" - -# Should we use mono? -$UseMono = ""; -if($Mono.IsPresent) { - Write-Verbose -Message "Using the Mono based scripting engine." - $UseMono = "-mono" -} - -# Should we use the new Roslyn? -$UseExperimental = ""; -if($Experimental.IsPresent -and !($Mono.IsPresent)) { - Write-Verbose -Message "Using experimental version of Roslyn." - $UseExperimental = "-experimental" -} - -# Is this a dry run? -$UseDryRun = ""; -if($WhatIf.IsPresent) { - $UseDryRun = "-dryrun" -} - -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null -} - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { Invoke-WebRequest -Uri http://cakebuild.net/download/bootstrapper/packages -OutFile $PACKAGES_CONFIG } catch { - Throw "Could not download packages.config." - } -} - -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } -} - -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." - } - Write-Verbose -Message ($NuGetOutput | out-string) - Pop-Location -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} - -# Start Cake -Write-Host "Running build script..." -Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE diff --git a/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs b/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs index 19cd6603..b2b10b6f 100644 --- a/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs +++ b/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs @@ -1,4 +1,8 @@ -using Shouldly; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using Shouldly; +using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Reporters; +using TestStack.BDDfy.Reporters.Html; using Xunit; namespace TestStack.BDDfy.Samples diff --git a/src/TestStack.BDDfy.Tests/DefaultHumanizerTests.cs b/src/TestStack.BDDfy.Tests/HumanizerTests.cs similarity index 89% rename from src/TestStack.BDDfy.Tests/DefaultHumanizerTests.cs rename to src/TestStack.BDDfy.Tests/HumanizerTests.cs index 7a5d7baf..6898f5d6 100644 --- a/src/TestStack.BDDfy.Tests/DefaultHumanizerTests.cs +++ b/src/TestStack.BDDfy.Tests/HumanizerTests.cs @@ -5,9 +5,9 @@ namespace TestStack.BDDfy.Tests { - public class DefaultHumanizerTests + public sealed class HumanizerTests { - private static DefaultHumanizer Humanizer => new(); + private static readonly DefaultHumanizer Humanizer = new(); [Fact] public void PascalCaseInputStringIsTurnedIntoSentence() @@ -34,6 +34,13 @@ public void WhenInputStringEndWithANumber_ThenNumberIsDealtWithLikeAWord() Humanizer.Humanize("NumberIsAtTheEnd100").ShouldBe("Number is at the end 100"); } + [Fact] + public void WhenInputStringHasANumberInTheMiddleAndEnd_ThenNumberIsDealtWithLikeAWord() + { + Humanizer.Humanize("Number15InTheMiddleAndEndingWith100") + .ShouldBe("Number 15 in the middle and ending with 100"); + } + [Fact] public void UnderscoredInputStringIsTurnedIntoSentence() { @@ -57,7 +64,7 @@ public void OneLetterWordInTheBeginningOfStringIsTurnedIntoAWord() [Theory] [InlineData("GivenThereAre__start__Cucumbers", "Given there are cucumbers")] [InlineData("Given_there_are__start__cucumbers", "Given there are cucumbers")] - [InlineData("GivenThereAre__count1__Cucumbers", "Given there are cucumbers")] + [InlineData("GivenThereAre__count1__Cucumbers", "Given there are cucumbers")] [InlineData("Given_there_are__count2__cucumbers", "Given there are cucumbers")] // The spacing rules for numbers are not consequential [InlineData("GivenMethodTaking__ExampleInt__", "Given method taking ")] [InlineData("Given_method_taking__ExampleInt__", "Given method taking ")] diff --git a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs new file mode 100644 index 00000000..51c7c433 --- /dev/null +++ b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Reflection; +using TestStack.BDDfy.Configuration; + +namespace TestStack.BDDfy.Abstractions; + +internal class DefaultStepTitleFactory : IStepTitleFactory +{ + public StepTitle Create( + string stepTextTemplate, + bool includeInputsInStepTitle, + MethodInfo methodInfo, + StepArgument[] inputArguments, + ITestContext testContext, + string stepPrefix) + { + Func createTitle = () => + { + var flatInputArray = inputArguments.Select(o => o.Value).FlattenArrays(); + var name = methodInfo.Name; + var stepTitleAttribute = methodInfo.GetCustomAttributes(typeof(StepTitleAttribute), true).SingleOrDefault(); + if (stepTitleAttribute != null) + { + var titleAttribute = ((StepTitleAttribute)stepTitleAttribute); + name = string.Format(titleAttribute.StepTitle, flatInputArray); + if (titleAttribute.IncludeInputsInStepTitle != null) + includeInputsInStepTitle = titleAttribute.IncludeInputsInStepTitle.Value; + } + + var stepTitle = AppendPrefix(Configurator.Humanizer.Humanize(name), stepPrefix); + + if (!string.IsNullOrEmpty(stepTextTemplate)) stepTitle = string.Format(stepTextTemplate, flatInputArray); + else if (includeInputsInStepTitle) + { + var parameters = methodInfo.GetParameters(); + var stringFlatInputs = + inputArguments + .Select((a, i) => new { ParameterName = parameters[i].Name, Value = a }) + .Select(i => + { + if (testContext.Examples != null) + { + var matchingHeaders = testContext.Examples.Headers + .Where(header => ExampleTable.HeaderMatches(header, i.ParameterName) || + ExampleTable.HeaderMatches(header, i.Value.Name)) + .ToList(); + + if (matchingHeaders.Count > 1) + throw new AmbiguousMatchException($"More than one headers for examples, match the parameter '{i.ParameterName}' provided for '{methodInfo.Name}'"); + + var matchingHeader = matchingHeaders.SingleOrDefault(); + if (matchingHeader != null) + return string.Format("<{0}>", matchingHeader); + } + return i.Value.Value.FlattenArray(); + }) + .ToArray(); + stepTitle = stepTitle + " " + string.Join(", ", stringFlatInputs); + } + + return stepTitle.Trim(); + }; + + return new StepTitle(createTitle); + } + + public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new StepTitle(AppendPrefix(title, stepPrefix)); + + private static string AppendPrefix(string title, string stepPrefix) + { + if (!title.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase)) + { + if (title.Length == 0) return string.Format("{0} ", stepPrefix); + return string.Format("{0} {1}{2}", stepPrefix, title.Substring(0, 1).ToLower(), title.Substring(1)); + } + + return title; + } +} \ No newline at end of file diff --git a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs new file mode 100644 index 00000000..ed8c9cf1 --- /dev/null +++ b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using TestStack.BDDfy; + +namespace TestStack.BDDfy.Abstractions; + +public interface IStepTitleFactory +{ + public StepTitle Create( + string stepTextTemplate, + bool includeInputsInStepTitle, + MethodInfo methodInfo, + StepArgument[] inputArguments, + ITestContext testContext, + string stepPrefix); + + StepTitle Create(string title, string stepPrefix, ITestContext testContext); +} \ No newline at end of file diff --git a/src/TestStack.BDDfy/Configuration/Configurator.cs b/src/TestStack.BDDfy/Configuration/Configurator.cs index 7ff99aef..a33b2566 100644 --- a/src/TestStack.BDDfy/Configuration/Configurator.cs +++ b/src/TestStack.BDDfy/Configuration/Configurator.cs @@ -1,3 +1,8 @@ +using System; +using TestStack.BDDfy; +using TestStack.BDDfy.Abstractions; +using TestStack.BDDfy.Factories; + namespace TestStack.BDDfy.Configuration { public static class Configurator @@ -15,5 +20,9 @@ public static class Configurator public static IStepExecutor StepExecutor { get; set; } = new StepExecutor(); public static IHumanizer Humanizer { get; set; } = new DefaultHumanizer(); + + public static IFluentScannerFactory FluentScannerFactory { get; set; } = new DefaultFluentScannerFactory(); + + public static IStepTitleFactory StepTitleFactory { get; set; } = new DefaultStepTitleFactory(); } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Configuration/IHumanizer.cs b/src/TestStack.BDDfy/Configuration/IHumanizer.cs index 02c4583c..aafb27ae 100644 --- a/src/TestStack.BDDfy/Configuration/IHumanizer.cs +++ b/src/TestStack.BDDfy/Configuration/IHumanizer.cs @@ -1,7 +1,10 @@ +using System; + namespace TestStack.BDDfy.Configuration { public interface IHumanizer { + Func PreserveCasingWhen { get; set; } 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 576ea423..34336101 100644 --- a/src/TestStack.BDDfy/DefaultHumanizer.cs +++ b/src/TestStack.BDDfy/DefaultHumanizer.cs @@ -1,81 +1,76 @@ +using Microsoft.VisualBasic; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; -using System.Xml.Linq; using TestStack.BDDfy.Configuration; namespace TestStack.BDDfy { - public partial class DefaultHumanizer: IHumanizer + internal partial class DefaultHumanizer: IHumanizer { - static readonly Func FromUnderscoreSeparatedWords = methodName => string.Join(" ", methodName.Split(new[] { '_' })); - static string FromPascalCase(string name) - { - var chars = name.Aggregate( - new List(), - (list, currentChar) => - { - if (currentChar == ' ' || list.Count == 0) - list.Add(currentChar); - else - { - if(ShouldAddSpace(list[list.Count - 1], currentChar)) - list.Add(' '); - list.Add(char.ToLower(currentChar)); - } - - return list; - }); - - var result = new string(chars.ToArray()); - return result.Replace(" i ", " I "); // I is an exception - } + private static readonly TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; - private static bool ShouldAddSpace(char lastChar, char currentChar) + public Func PreserveCasingWhen { get; set; } = input + => input.Replace("__", "-").Contains('_'); + + public string Humanize(string input) { - if (lastChar == ' ') - return false; + var shouldPreserveCasing = PreserveCasingWhen(input); + input = TokensPattern().Replace(input, "-#$1#-"); - if (char.IsDigit(lastChar)) - { - if (char.IsLetter(currentChar)) - return true; + var words = input.Split(['_','-']); - return false; + var finalWords = words.Select(x => TokenReplacePattern().Replace(x, "<$1>")); + + var sentence = string.Join(" ", finalWords); + + if (!shouldPreserveCasing) + { + sentence = ConsecutiveCapitalLetters().Replace(sentence, "$1 $2"); + sentence = PascalToSentence(sentence); + sentence = SentenceWithNumerals().Replace(sentence, " "); + if (UnicodeMatchPattern().IsMatch(input)) + throw new ArgumentException("Non ascii characters detected"); } + + + sentence = sentence.Trim().Replace(" "," "); + sentence = LoneIReplacePattern().Replace(sentence, "I"); + return sentence; + } - if (!char.IsLower(currentChar) && currentChar != '>' && lastChar != '<') - return true; + public static string PascalToSentence(string input) + { + if (string.IsNullOrEmpty(input)) return input; - return false; + var sentence = PascalCaseRegex().Replace(input, " $1"); + sentence = sentence.Replace("< ", "<").Replace(" >",">"); + var final = char.ToUpper(sentence[0]) + sentence.Substring(1).ToLower(); + return final; } - private static readonly Func ConvertNonExample = name => { - if (name.Contains('_')) - return FromUnderscoreSeparatedWords(name); - return FromPascalCase(name); - }; + [GeneratedRegex("([A-Z]+)([A-Z][a-z])")] + private static partial Regex ConsecutiveCapitalLetters(); - private string ExampleTitle(string name) - { - // Compare contains("__") with a regex match - string newName = TitleCleanerRegex().Replace(name, " <$1> "); + [GeneratedRegex("__([a-zA-Z0-9]+)__")] + private static partial Regex TokensPattern(); - if (newName == name) { - throw new ArgumentException("Illegal example title in name '" + name + "'!"); - } + [GeneratedRegex(@"(?]*>)")] + private static partial Regex SentenceWithNumerals(); - public string Humanize(string input) - => input.Contains("__") ? ExampleTitle(input) : ConvertNonExample(input); + [GeneratedRegex(@"[^\u0000-\u007F]")] + private static partial Regex UnicodeMatchPattern(); - [GeneratedRegex("__([a-zA-Z][a-zA-Z0-9]*)__")] - private static partial Regex TitleCleanerRegex(); + [GeneratedRegex(@"(?<=^|\s)i(?=\s|$)")] + private static partial Regex LoneIReplacePattern(); } } diff --git a/src/TestStack.BDDfy/Engine.cs b/src/TestStack.BDDfy/Engine.cs index d29d4cf8..4c1083a1 100644 --- a/src/TestStack.BDDfy/Engine.cs +++ b/src/TestStack.BDDfy/Engine.cs @@ -10,13 +10,7 @@ public class Engine(IScanner scanner) static Engine() { -#if APPDOMAIN - System.AppDomain.CurrentDomain.DomainUnload += (sender, e) => { - InvokeBatchProcessors(); - }; -#else System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += context => InvokeBatchProcessors(); -#endif } static void InvokeBatchProcessors() diff --git a/src/TestStack.BDDfy/Factories/DefaultFluentScannerFactory.cs b/src/TestStack.BDDfy/Factories/DefaultFluentScannerFactory.cs new file mode 100644 index 00000000..e6ef0643 --- /dev/null +++ b/src/TestStack.BDDfy/Factories/DefaultFluentScannerFactory.cs @@ -0,0 +1,6 @@ +namespace TestStack.BDDfy.Factories; + +internal class DefaultFluentScannerFactory : IFluentScannerFactory +{ + public IFluentScanner Create(TScenario testObject) where TScenario : class => new FluentScanner(testObject); +} diff --git a/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs b/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs new file mode 100644 index 00000000..44a1acfa --- /dev/null +++ b/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TestStack.BDDfy.Factories; + +public interface IFluentScannerFactory +{ + IFluentScanner Create(TScenario testObject) where TScenario : class; +} diff --git a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs index 1df3d22a..04259d4d 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -16,30 +16,6 @@ public class ExceptionProcessor(Action assertInconclusive): IProcessor static ExceptionProcessor() { var exceptionType = typeof(Exception); -// No best guess for CORE Clr -#if APPDOMAIN - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if(ExcludedAssemblies.Any(ex => assembly.GetName().FullName.StartsWith(ex))) - continue; - - foreach (var inconclusiveExceptionType in GetTypesSafely(assembly)) - { - if (inconclusiveExceptionType.Name.Contains("Inconclusive") && - inconclusiveExceptionType.Name.Contains("Exception") && - exceptionType.IsAssignableFrom(inconclusiveExceptionType)) - { - var constructors = inconclusiveExceptionType.GetConstructors(); - var shortestCtor = constructors.Min(c => c.GetParameters().Length); - var ctor = constructors.First(c => c.GetParameters().Length == shortestCtor); - var argList = new List(); - argList.AddRange(ctor.GetParameters().Select(p => DefaultValue(p.ParameterType))); - BestGuessInconclusiveAssertion = () => { throw (Exception)ctor.Invoke(argList.ToArray()); }; - return; - } - } - } -#endif BestGuessInconclusiveAssertion = () => { throw new InconclusiveException(); }; } diff --git a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs index 4661af29..62d01f09 100644 --- a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs +++ b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs @@ -2,22 +2,12 @@ namespace TestStack.BDDfy.Processors { -#if NET40 - [System.Serializable] -#else - [System.Runtime.Serialization.Serializable] -#endif - 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)) - { -#if NET40 - - protected UnusedExampleException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) - : base(info, context) - { - } -#endif - } + [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" + + "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/AssemblyInfo.cs b/src/TestStack.BDDfy/Properties/AssemblyInfo.cs index 46b351b7..c03b4d14 100644 --- a/src/TestStack.BDDfy/Properties/AssemblyInfo.cs +++ b/src/TestStack.BDDfy/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -16,4 +17,5 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("20482e5c-b996-4cc8-b30b-fc7b16268b29")] \ No newline at end of file +[assembly: Guid("20482e5c-b996-4cc8-b30b-fc7b16268b29")] +[assembly: InternalsVisibleTo("TestStack.BDDfy.Tests")] \ No newline at end of file diff --git a/src/TestStack.BDDfy/Reporters/FileHelpers.cs b/src/TestStack.BDDfy/Reporters/FileHelpers.cs index 58c55259..60803d9e 100644 --- a/src/TestStack.BDDfy/Reporters/FileHelpers.cs +++ b/src/TestStack.BDDfy/Reporters/FileHelpers.cs @@ -8,15 +8,8 @@ public class FileHelpers // http://stackoverflow.com/questions/52797/c-how-do-i-get-the-path-of-the-assembly-the-code-is-in#answer-283917 internal static string AssemblyDirectory() { -#if NET40 - string codeBase = typeof(Engine).Assembly().CodeBase; - var uri = new UriBuilder(codeBase); - string path = Uri.UnescapeDataString(uri.Path); - return Path.GetDirectoryName(path); -#else var basePath = AppContext.BaseDirectory; return Path.GetFullPath(basePath); -#endif } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/UnassignableExampleException.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/UnassignableExampleException.cs index e1909791..c58d7a29 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Examples/UnassignableExampleException.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Examples/UnassignableExampleException.cs @@ -2,22 +2,9 @@ namespace TestStack.BDDfy { -#if NET40 - [System.Serializable] -#else - [System.Runtime.Serialization.Serializable] -#endif + [Serializable] public class UnassignableExampleException(string message, Exception inner, ExampleValue exampleValue): Exception(message, inner) { -#if NET40 - - protected UnassignableExampleException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) - { - } - -#endif public ExampleValue ExampleValue { get; private set; } = exampleValue; } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs index 4beb5884..c1fb10f1 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs @@ -1,7 +1,8 @@ using System; using System.Linq.Expressions; using System.Threading.Tasks; - +using TestStack.BDDfy.Configuration; + // ReSharper disable CheckNamespace // This is in BDDfy namespace to make its usage simpler namespace TestStack.BDDfy @@ -308,8 +309,9 @@ interface IFluentStepBuilder object TestObject { get; } } - public class FluentStepBuilder : IFluentStepBuilder, IFluentStepBuilder - where TScenario : class + public class FluentStepBuilder + : IFluentStepBuilder, IFluentStepBuilder + where TScenario : class { readonly FluentScanner scanner; @@ -318,7 +320,7 @@ public FluentStepBuilder(TScenario testObject) TestObject = testObject; var existingContext = TestContext.GetContext(TestObject); if (existingContext.FluentScanner == null) - existingContext.FluentScanner = new FluentScanner(testObject); + existingContext.FluentScanner = Configurator.FluentScannerFactory.Create(testObject); scanner = (FluentScanner) existingContext.FluentScanner; } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs index 50510085..5087dcf9 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Linq; using System.Threading.Tasks; using TestStack.BDDfy.Annotations; using TestStack.BDDfy.Configuration; @@ -59,32 +59,23 @@ IScanner IFluentScanner.GetScanner(string scenarioTitle, Type explicitStoryType) public void AddStep(Action stepAction, string title, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) { var action = StepActionFactory.GetStepAction(o => stepAction()); - _steps.Add(new Step(action, new StepTitle(AppendPrefix(title, stepPrefix)), FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, new List())); - } - - private string AppendPrefix(string title, string stepPrefix) - { - if (!title.ToLower().StartsWith(stepPrefix.ToLower())) - { - if (title.Length == 0) - return string.Format("{0} ", stepPrefix); - return string.Format("{0} {1}{2}", stepPrefix, title.Substring(0, 1).ToLower(), title.Substring(1)); - } - - return title; + var stepTitle = CreateTitle(title, stepPrefix); + _steps.Add(new Step(action, stepTitle, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, new List())); } public void AddStep(Func stepAction, string title, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) { var action = StepActionFactory.GetStepAction(o => stepAction()); - _steps.Add(new Step(action, new StepTitle(AppendPrefix(title, stepPrefix)), FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, new List())); + var stepTitle = CreateTitle(title, stepPrefix); + _steps.Add(new Step(action, stepTitle, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, new List())); } + private StepTitle CreateTitle(string title, string stepPrefix) => Configurator.StepTitleFactory.Create(title, stepPrefix, _testContext); + public void AddStep(Expression> stepAction, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) { var compiledAction = stepAction.Compile(); - var call = Expression.Call(Expression.Constant(this), - _fakeExecuteActionMethod, stepAction.Body); + var call = Expression.Call(Expression.Constant(this), _fakeExecuteActionMethod, stepAction.Body); var expression = Expression.Lambda>(call, Expression.Parameter(typeof(TScenario))); AddStep(_ => compiledAction().Action(), expression, null, true, reports, executionOrder, asserts, stepPrefix); } @@ -96,21 +87,45 @@ private void ExecuteAction(ExampleAction action) } - public void AddStep(Expression> stepAction, string stepTextTemplate, bool includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) + public void AddStep( + Expression> stepAction, + string stepTextTemplate, + bool includeInputsInStepTitle, + bool reports, + ExecutionOrder executionOrder, + bool asserts, + string stepPrefix) { var action = stepAction.Compile(); - var inputArguments = new StepArgument[0]; + var inputArguments = Array.Empty(); if (includeInputsInStepTitle) { inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); } - var title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + var title = CreateTitle( + stepTextTemplate, + includeInputsInStepTitle, + GetMethodInfo(stepAction), + inputArguments, + stepPrefix); + var args = inputArguments.Where(s => !string.IsNullOrEmpty(s.Name)).ToList(); - _steps.Add(new Step(StepActionFactory.GetStepAction(action), title, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, args)); + var stepDelegate = StepActionFactory.GetStepAction(action); + var shouldFixAsserts = FixAsserts(asserts, executionOrder); + var shouldFixConsecutiveStep = FixConsecutiveStep(executionOrder); + + _steps.Add(new Step(stepDelegate, title, shouldFixAsserts, shouldFixConsecutiveStep, reports, args)); } - public void AddStep(Expression> stepAction, string stepTextTemplate, bool includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) + public void AddStep( + Expression> stepAction, + string stepTextTemplate, + bool includeInputsInStepTitle, + bool reports, + ExecutionOrder executionOrder, + bool asserts, + string stepPrefix) { var action = stepAction.Compile(); @@ -120,19 +135,33 @@ public void AddStep(Expression> stepAction, string stepTextTem private void AddStep(Action action, LambdaExpression stepAction, string stepTextTemplate, bool includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) { - var inputArguments = new StepArgument[0]; + var inputArguments = Array.Empty(); if (includeInputsInStepTitle) { inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); } - var title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, - stepPrefix); + var title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + var args = inputArguments.Where(s => !string.IsNullOrEmpty(s.Name)).ToList(); _steps.Add(new Step(StepActionFactory.GetStepAction(action), title, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, args)); } + private StepTitle CreateTitle( + string stepTextTemplate, + bool includeInputsInStepTitle, + MethodInfo methodInfo, + StepArgument[] inputArguments, + string stepPrefix) + => Configurator.StepTitleFactory.Create( + stepTextTemplate, + includeInputsInStepTitle, + methodInfo, + inputArguments, + _testContext, + stepPrefix); + private bool FixAsserts(bool asserts, ExecutionOrder executionOrder) { if (executionOrder == ExecutionOrder.ConsecutiveStep) @@ -174,59 +203,6 @@ private ExecutionOrder FixConsecutiveStep(ExecutionOrder executionOrder) return executionOrder; } - private StepTitle CreateTitle(string stepTextTemplate, bool includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, string stepPrefix) - { - Func createTitle = () => - { - - var flatInputArray = inputArguments.Select(o => o.Value).FlattenArrays(); - var name = methodInfo.Name; - var stepTitleAttribute = methodInfo.GetCustomAttributes(typeof(StepTitleAttribute), true).SingleOrDefault(); - if (stepTitleAttribute != null) - { - var titleAttribute = ((StepTitleAttribute)stepTitleAttribute); - name = string.Format(titleAttribute.StepTitle, flatInputArray); - if (titleAttribute.IncludeInputsInStepTitle != null) - includeInputsInStepTitle = titleAttribute.IncludeInputsInStepTitle.Value; - } - - var stepTitle = AppendPrefix(Configurator.Humanizer.Humanize(name), stepPrefix); - - if (!string.IsNullOrEmpty(stepTextTemplate)) stepTitle = string.Format(stepTextTemplate, flatInputArray); - else if (includeInputsInStepTitle) - { - var parameters = methodInfo.GetParameters(); - var stringFlatInputs = - inputArguments - .Select((a, i) => new { ParameterName = parameters[i].Name, Value = a }) - .Select(i => - { - if (_testContext.Examples != null) - { - var matchingHeaders = _testContext.Examples.Headers - .Where(header => ExampleTable.HeaderMatches(header, i.ParameterName) || - ExampleTable.HeaderMatches(header, i.Value.Name)) - .ToList(); - - if (matchingHeaders.Count > 1) - throw new AmbiguousMatchException ($"More than one headers for examples, match the parameter '{i.ParameterName}' provided for '{methodInfo.Name}'"); - - var matchingHeader = matchingHeaders.SingleOrDefault(); - if (matchingHeader != null) - return string.Format("<{0}>", matchingHeader); - } - return i.Value.Value.FlattenArray(); - }) - .ToArray(); - stepTitle = stepTitle + " " + string.Join(", ", stringFlatInputs); - } - - return stepTitle.Trim(); - }; - - return new StepTitle(createTitle); - } - private static MethodInfo GetMethodInfo(LambdaExpression stepAction) { var methodCall = (MethodCallExpression)stepAction.Body; diff --git a/src/TestStack.BDDfy/SerializableAttribute.cs b/src/TestStack.BDDfy/SerializableAttribute.cs deleted file mode 100644 index 85100866..00000000 --- a/src/TestStack.BDDfy/SerializableAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -#if NET40 - -#else -namespace System.Runtime.Serialization -{ - public class SerializableAttribute : Attribute - { - } -} -#endif \ No newline at end of file diff --git a/src/TestStack.BDDfy/TestContext.cs b/src/TestStack.BDDfy/TestContext.cs index 35a4613a..4c9c6e37 100644 --- a/src/TestStack.BDDfy/TestContext.cs +++ b/src/TestStack.BDDfy/TestContext.cs @@ -20,9 +20,8 @@ public static void SetContext(object testObject, ITestContext context) lock (_dictionaryLock) { - if (ContextLookup.ContainsKey(testObject)) + if (ContextLookup.TryGetValue(testObject, out ITestContext oldContext)) { - var oldContext = ContextLookup[testObject]; context.Examples = oldContext.Examples; ContextLookup[testObject] = new TestContext(testObject); } @@ -51,8 +50,7 @@ public static void ClearContextFor(object testObject) { lock (_dictionaryLock) { - if (ContextLookup.ContainsKey(testObject)) - ContextLookup.Remove(testObject); + ContextLookup.Remove(testObject); } } diff --git a/src/TestStack.BDDfy/TypeExtensions.cs b/src/TestStack.BDDfy/TypeExtensions.cs index d20d7329..1e599c02 100644 --- a/src/TestStack.BDDfy/TypeExtensions.cs +++ b/src/TestStack.BDDfy/TypeExtensions.cs @@ -1,44 +1,9 @@ using System; using System.Reflection; +using System.Linq; namespace TestStack.BDDfy { -#if NET40 - public static class TypeExtensions - { - public static Assembly Assembly(this Type type) - { - return type.Assembly; - } - - public static object[] GetCustomAttributes(this Type type, Type attributeType, bool inherit) - { - return type.GetCustomAttributes(attributeType, inherit); - } - - public static bool IsEnum(this Type type) - { - return type.IsEnum; - } - - public static bool IsGenericType(this Type type) - { - return type.IsGenericType; - } - - public static bool IsInstanceOfType(this Type type, object obj) - { - return type.IsInstanceOfType(obj); - } - - public static bool IsValueType(this Type type) - { - return type.IsValueType; - } - } - -#else -using System.Linq; public static class TypeExtensions { public static Assembly Assembly(this Type type) @@ -76,6 +41,4 @@ public static bool IsValueType(this Type type) } } -#endif - } From d07d2bd40fdfd0a879f0a642a8631b08e251207d Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Thu, 30 Oct 2025 23:32:37 +0000 Subject: [PATCH 02/34] add step title factory tests --- .../UsingCustomStepTitleFactory.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs new file mode 100644 index 00000000..ba46ab60 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs @@ -0,0 +1,75 @@ +using Shouldly; +using System.Linq; +using System.Reflection; +using TestStack.BDDfy.Abstractions; +using TestStack.BDDfy.Configuration; +using Xunit; + +namespace TestStack.BDDfy.Tests.Scanner.FluentScanner +{ + public class UsingCustomStepTitleFactory + { + private class CustomStepTitleFactory : IStepTitleFactory + { + public StepTitle Create( + string stepTextTemplate, + bool includeInputsInStepTitle, + MethodInfo methodInfo, + StepArgument[] inputArguments, + ITestContext testContext, + string stepPrefix) => new StepTitle("Custom Step Title"); + + public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new StepTitle(title); + } + + [Fact] + public void ShouldUseCustomStepTitleFactoryWhenSet() + { + var scenario = new UsingCustomStepTitleFactory(); + var configurator = TestContext.GetContext(scenario).Configurator; + configurator.StepTitleFactory = new CustomStepTitleFactory(); + + var story = scenario + .Given(_ => SomeState()) + .When(_ => SomethingHappens()) + .Then(_ => SomeResult()) + .BDDfy(); + + story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + } + + [Fact] + public void ShouldUseCustomStepTitleFactoryWhenSetWithStepTitles() + { + var scenario = new UsingCustomStepTitleFactory(); + var configurator = TestContext.GetContext(scenario).Configurator; + configurator.StepTitleFactory = new CustomStepTitleFactory(); + var story = scenario + .Given(_ => SomeState(), "Not this") + .When(_ => SomethingHappens(), "Or this") + .Then(_ => SomeResult(), "should not appear") + .BDDfy(); + + story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + } + + [StepTitle("given from attribute")] + private void SomeState() + { + } + + [StepTitle("when from attribute")] + private void SomethingHappens() + { + } + + [StepTitle("then from attribute")] + private void SomeResult() + { + } + } +} \ No newline at end of file From fbfeaa5cf92331bd3dc7bb75615167f8183df62e Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Thu, 30 Oct 2025 23:38:58 +0000 Subject: [PATCH 03/34] add step title factory tests --- .../UsingCustomStepTitleFactory.cs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs index ba46ab60..89d40aa1 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs @@ -25,36 +25,49 @@ public StepTitle Create( [Fact] public void ShouldUseCustomStepTitleFactoryWhenSet() { - var scenario = new UsingCustomStepTitleFactory(); - var configurator = TestContext.GetContext(scenario).Configurator; - configurator.StepTitleFactory = new CustomStepTitleFactory(); + Configurator.StepTitleFactory = new CustomStepTitleFactory(); - var story = scenario + try + { + var story = new UsingCustomStepTitleFactory() .Given(_ => SomeState()) .When(_ => SomethingHappens()) .Then(_ => SomeResult()) .BDDfy(); - story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); - story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); - story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + } + finally + { + Configurator.StepTitleFactory = new DefaultStepTitleFactory(); + } + } [Fact] public void ShouldUseCustomStepTitleFactoryWhenSetWithStepTitles() { - var scenario = new UsingCustomStepTitleFactory(); - var configurator = TestContext.GetContext(scenario).Configurator; - configurator.StepTitleFactory = new CustomStepTitleFactory(); - var story = scenario + Configurator.StepTitleFactory = new CustomStepTitleFactory(); + + try + { + var story = new UsingCustomStepTitleFactory() .Given(_ => SomeState(), "Not this") .When(_ => SomethingHappens(), "Or this") .Then(_ => SomeResult(), "should not appear") .BDDfy(); - story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); - story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); - story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(0).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(1).Title.ShouldBe("Custom Step Title"); + story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("Custom Step Title"); + } + finally + { + Configurator.StepTitleFactory = new DefaultStepTitleFactory(); + + } } [StepTitle("given from attribute")] From 3d1a0b43cd6e4469a6effb2769bcac6ce7fa8c06 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Thu, 30 Oct 2025 23:56:13 +0000 Subject: [PATCH 04/34] run tests fighting for configurator in same collection --- .../Scanner/FluentScanner/UsingCustomStepTitleFactory.cs | 1 + .../FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs index 89d40aa1..765498e6 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs @@ -7,6 +7,7 @@ namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { + [Collection("UseConfigurator")] public class UsingCustomStepTitleFactory { private class CustomStepTitleFactory : IStepTitleFactory diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs index 1123d70e..c8e7efc8 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs @@ -5,6 +5,7 @@ namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { + [Collection("UseConfigurator")] public class WhenStepsAreScannedUsingFluentScanner { private IEnumerable _steps; From 32fd607843c187ed1fbc57b788cf77719534e34a Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 20:21:45 +0000 Subject: [PATCH 05/34] cleanup ExceptionProcessor --- .../Processors/ExceptionProcessor.cs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs index 04259d4d..d7ffd2be 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -10,34 +10,11 @@ public class ExceptionProcessor(Action assertInconclusive): IProcessor private readonly Action _assertInconclusive = assertInconclusive; private static readonly Action BestGuessInconclusiveAssertion; - static readonly List ExcludedAssemblies = - new(new[] { "System", "mscorlib", "TestStack.BDDfy", "TestDriven", "JetBrains.ReSharper" }); - static ExceptionProcessor() { - var exceptionType = typeof(Exception); - BestGuessInconclusiveAssertion = () => { throw new InconclusiveException(); }; } - private static IEnumerable GetTypesSafely(Assembly assembly) - { - try - { - return assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - return ex.Types.Where(x => x != null); - } - } - - //http://stackoverflow.com/questions/520290/how-can-i-get-the-default-value-of-a-type-in-a-non-generic-way - static object DefaultValue(Type myType) - { - return !myType.IsValueType() ? null : Activator.CreateInstance(myType); - } - public ExceptionProcessor() : this(BestGuessInconclusiveAssertion) { } From e3e1409c98cd9ffeeafca17257f2ddea7182253d Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 20:26:46 +0000 Subject: [PATCH 06/34] remove un used property function --- src/TestStack.BDDfy/Configuration/IHumanizer.cs | 1 - src/TestStack.BDDfy/DefaultHumanizer.cs | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/TestStack.BDDfy/Configuration/IHumanizer.cs b/src/TestStack.BDDfy/Configuration/IHumanizer.cs index aafb27ae..fba0f833 100644 --- a/src/TestStack.BDDfy/Configuration/IHumanizer.cs +++ b/src/TestStack.BDDfy/Configuration/IHumanizer.cs @@ -4,7 +4,6 @@ namespace TestStack.BDDfy.Configuration { public interface IHumanizer { - Func PreserveCasingWhen { get; set; } 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 34336101..be71f363 100644 --- a/src/TestStack.BDDfy/DefaultHumanizer.cs +++ b/src/TestStack.BDDfy/DefaultHumanizer.cs @@ -12,12 +12,10 @@ internal partial class DefaultHumanizer: IHumanizer { private static readonly TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; - public Func PreserveCasingWhen { get; set; } = input - => input.Replace("__", "-").Contains('_'); - public string Humanize(string input) { - var shouldPreserveCasing = PreserveCasingWhen(input); + var shouldPreserveCasing = input.Replace("__", "-").Contains('_'); + input = TokensPattern().Replace(input, "-#$1#-"); var words = input.Split(['_','-']); From 4e471e3098d1f9fc4c08df9fb3eb2e11dcf16bb7 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 20:50:21 +0000 Subject: [PATCH 07/34] cleanup --- src/.editorconfig | 5 +- src/TestStack.BDDfy/DefaultHumanizer.cs | 51 ++++++------------- .../Processors/UnusedExampleException.cs | 17 ++++--- .../Properties/AssemblyInfo.cs | 21 -------- src/TestStack.BDDfy/TestStack.BDDfy.csproj | 6 +++ 5 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 src/TestStack.BDDfy/Properties/AssemblyInfo.cs diff --git a/src/.editorconfig b/src/.editorconfig index dd2b5504..e3d80304 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,4 +1,5 @@ [*.cs] -# Default severity for analyzer diagnostics with category 'Style' -dotnet_analyzer_diagnostic.category-Style.severity = none + +# SYSLIB1045: Convert to 'GeneratedRegexAttribute'. +dotnet_diagnostic.SYSLIB1045.severity = none diff --git a/src/TestStack.BDDfy/DefaultHumanizer.cs b/src/TestStack.BDDfy/DefaultHumanizer.cs index be71f363..145248d7 100644 --- a/src/TestStack.BDDfy/DefaultHumanizer.cs +++ b/src/TestStack.BDDfy/DefaultHumanizer.cs @@ -1,7 +1,4 @@ -using Microsoft.VisualBasic; -using System; -using System.Collections.Generic; -using System.Globalization; +using System; using System.Linq; using System.Text.RegularExpressions; using TestStack.BDDfy.Configuration; @@ -10,32 +7,38 @@ namespace TestStack.BDDfy { internal partial class DefaultHumanizer: IHumanizer { - private static readonly TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; + private static readonly Regex ConsecutiveCapitalLetters = new("([A-Z]+)([A-Z][a-z])"); + private static readonly Regex TokensPattern = new("__([a-zA-Z0-9]+)__"); + private static readonly Regex PascalCaseRegex = new(@"(?]*>)"); + private static readonly Regex UnicodeMatchPattern = new(@"[^\u0000-\u007F]"); + private static readonly Regex LoneIReplacePattern = new(@"(?<=^|\s)i(?=\s|$)"); public string Humanize(string input) { var shouldPreserveCasing = input.Replace("__", "-").Contains('_'); - input = TokensPattern().Replace(input, "-#$1#-"); + input = TokensPattern.Replace(input, "-#$1#-"); var words = input.Split(['_','-']); - var finalWords = words.Select(x => TokenReplacePattern().Replace(x, "<$1>")); + var finalWords = words.Select(x => TokenReplacePattern.Replace(x, "<$1>")); var sentence = string.Join(" ", finalWords); if (!shouldPreserveCasing) { - sentence = ConsecutiveCapitalLetters().Replace(sentence, "$1 $2"); + sentence = ConsecutiveCapitalLetters.Replace(sentence, "$1 $2"); sentence = PascalToSentence(sentence); - sentence = SentenceWithNumerals().Replace(sentence, " "); - if (UnicodeMatchPattern().IsMatch(input)) + sentence = SentenceWithNumerals.Replace(sentence, " "); + if (UnicodeMatchPattern.IsMatch(input)) throw new ArgumentException("Non ascii characters detected"); } sentence = sentence.Trim().Replace(" "," "); - sentence = LoneIReplacePattern().Replace(sentence, "I"); + sentence = LoneIReplacePattern.Replace(sentence, "I"); return sentence; } @@ -43,32 +46,10 @@ public static string PascalToSentence(string input) { if (string.IsNullOrEmpty(input)) return input; - var sentence = PascalCaseRegex().Replace(input, " $1"); + var sentence = PascalCaseRegex.Replace(input, " $1"); sentence = sentence.Replace("< ", "<").Replace(" >",">"); - var final = char.ToUpper(sentence[0]) + sentence.Substring(1).ToLower(); + var final = char.ToUpper(sentence[0]) + sentence[1..].ToLower(); return final; } - - - [GeneratedRegex("([A-Z]+)([A-Z][a-z])")] - private static partial Regex ConsecutiveCapitalLetters(); - - [GeneratedRegex("__([a-zA-Z0-9]+)__")] - private static partial Regex TokensPattern(); - - [GeneratedRegex(@"(?]*>)")] - private static partial Regex SentenceWithNumerals(); - - [GeneratedRegex(@"[^\u0000-\u007F]")] - private static partial Regex UnicodeMatchPattern(); - - [GeneratedRegex(@"(?<=^|\s)i(?=\s|$)")] - private static partial Regex LoneIReplacePattern(); } } diff --git a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs index 62d01f09..27471b6e 100644 --- a/src/TestStack.BDDfy/Processors/UnusedExampleException.cs +++ b/src/TestStack.BDDfy/Processors/UnusedExampleException.cs @@ -2,12 +2,13 @@ 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" - + "If this is not the case, raise an issue at https://github.com/TestStack/TestStack.BDDfy/issues.", unusedValue.Header)) - { - } - } + [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" + + "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/AssemblyInfo.cs b/src/TestStack.BDDfy/Properties/AssemblyInfo.cs deleted file mode 100644 index c03b4d14..00000000 --- a/src/TestStack.BDDfy/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("TestStack.BDDfy - http://teststack.github.com/TestStack.BDDfy/")] -[assembly: AssemblyProduct("TestStack.BDDfy")] -[assembly: AssemblyCopyright("Copyright © 2011-2017 TestStack.BDDfy contributors")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("20482e5c-b996-4cc8-b30b-fc7b16268b29")] -[assembly: InternalsVisibleTo("TestStack.BDDfy.Tests")] \ No newline at end of file diff --git a/src/TestStack.BDDfy/TestStack.BDDfy.csproj b/src/TestStack.BDDfy/TestStack.BDDfy.csproj index 2ffbe71c..e54f9e2d 100644 --- a/src/TestStack.BDDfy/TestStack.BDDfy.csproj +++ b/src/TestStack.BDDfy/TestStack.BDDfy.csproj @@ -1,4 +1,9 @@  + + TestStack.BDDfy - http://teststack.github.com/TestStack.BDDfy/ + TestStack.BDDfy + Copyright © 2011-2017 TestStack.BDDfy contributors + net8.0 TestStack.BDDfy @@ -15,5 +20,6 @@ + From 4d7de815cc2fcc8ce8f0d4d4050a053027587969 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 21:30:02 +0000 Subject: [PATCH 08/34] remove unused code. these attributes are defined in this assembly but are not used or handled by any code. --- .../Concurrency/TestCollectionName.cs | 6 + .../WhenWhenThrowsException.cs | 2 + .../Scanner/FluentScanner/StepTitleTests.cs | 2 + .../UsingCustomStepTitleFactory.cs | 3 +- .../WhenStepsAreScannedUsingFluentScanner.cs | 3 +- src/TestStack.BDDfy/Properties/Annotations.cs | 554 ++---------------- src/TestStack.BDDfy/TestStack.BDDfy.csproj | 6 +- 7 files changed, 56 insertions(+), 520 deletions(-) create mode 100644 src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs diff --git a/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs b/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs new file mode 100644 index 00000000..156c5e85 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs @@ -0,0 +1,6 @@ +namespace TestStack.BDDfy.Tests.Concurrency +{ + internal static class TestCollectionName { + public const string ModifiesConfigurator = "ModifiesConfigurator"; + } +} diff --git a/src/TestStack.BDDfy.Tests/Exceptions/OtherExceptions/WhenWhenThrowsException.cs b/src/TestStack.BDDfy.Tests/Exceptions/OtherExceptions/WhenWhenThrowsException.cs index 11741e33..1ea549b7 100644 --- a/src/TestStack.BDDfy.Tests/Exceptions/OtherExceptions/WhenWhenThrowsException.cs +++ b/src/TestStack.BDDfy.Tests/Exceptions/OtherExceptions/WhenWhenThrowsException.cs @@ -1,9 +1,11 @@ using System; using Shouldly; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Exceptions.OtherExceptions { + [Collection(TestCollectionName.ModifiesConfigurator)] public class WhenWhenThrowsException : OtherExceptionBase { private void ExecuteUsingFluentScanner() diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepTitleTests.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepTitleTests.cs index cd637327..e405fc62 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepTitleTests.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepTitleTests.cs @@ -1,9 +1,11 @@ using System.Linq; using Shouldly; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { + [Collection(TestCollectionName.ModifiesConfigurator)] public class StepTitleTests { private string _mutatedState; diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs index 765498e6..0a303360 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/UsingCustomStepTitleFactory.cs @@ -3,11 +3,12 @@ using System.Reflection; using TestStack.BDDfy.Abstractions; using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { - [Collection("UseConfigurator")] + [Collection(TestCollectionName.ModifiesConfigurator)] public class UsingCustomStepTitleFactory { private class CustomStepTitleFactory : IStepTitleFactory diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs index c8e7efc8..032dda2a 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/WhenStepsAreScannedUsingFluentScanner.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using System.Linq; using Shouldly; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { - [Collection("UseConfigurator")] + [Collection(TestCollectionName.ModifiesConfigurator)] public class WhenStepsAreScannedUsingFluentScanner { private IEnumerable _steps; diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 027dcdcf..65d45e51 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -1,539 +1,59 @@ using System; -#pragma warning disable 1591 -// 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)] - 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)] - 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 + /// Indicates that the marked symbol is used implicitly + /// (e.g. via reflection, in external library), so this symbol + /// will not be marked as unused (as well as by other usage inspections) /// - /// - /// [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)] - 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)] - 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)] - public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute - { - public NotifyPropertyChangedInvocatorAttribute() { } - public NotifyPropertyChangedInvocatorAttribute(string parameterName) + [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] + public sealed class UsedImplicitlyAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute { - 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)] - 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)] - 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)] - public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } - /// - /// 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))] - 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 - /// will not be marked as unused (as well as by other usage inspections) - /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] - public sealed class UsedImplicitlyAttribute( - ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags): Attribute - { - public UsedImplicitlyAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) { } - - public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) { } + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } public ImplicitUseKindFlags UseKindFlags { get; private set; } = useKindFlags; public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; } - /// - /// Should be used on attributes and causes ReSharper - /// to not mark symbols marked with such attributes as unused - /// (as well as by other usage inspections) - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class MeansImplicitUseAttribute( - ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags): Attribute - { - public MeansImplicitUseAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) { } - - public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) { } - - [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } = useKindFlags; - [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; - } - - [Flags] - public enum ImplicitUseKindFlags - { - Default = Access | Assign | InstantiatedWithFixedConstructorSignature, - /// Only entity marked with attribute considered used - Access = 1, - /// Indicates implicit assignment to a member - Assign = 2, - /// - /// Indicates implicit instantiation of a type with fixed constructor signature. - /// That means any unused constructor parameters won't be reported as such. - /// - InstantiatedWithFixedConstructorSignature = 4, - /// Indicates implicit instantiation of a type - InstantiatedNoFixedConstructorSignature = 8, - } - - /// - /// Specify what is considered used implicitly - /// when marked with - /// or - /// - [Flags] - public enum ImplicitUseTargetFlags - { - Default = Itself, - Itself = 1, - /// Members of entity marked with attribute are considered used - Members = 2, - /// 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 - /// - [MeansImplicitUse] - public sealed class PublicAPIAttribute : Attribute - { - public PublicAPIAttribute() { } - public PublicAPIAttribute([NotNull] string comment) + [Flags] + public enum ImplicitUseKindFlags { - Comment = comment; + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used + Access = 1, + /// Indicates implicit assignment to a member + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type + InstantiatedNoFixedConstructorSignature = 8, } - [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)] - public sealed class InstantHandleAttribute : Attribute { } - - /// - /// Indicates that a method does not make any observable state changes. - /// The same as System.Diagnostics.Contracts.PureAttribute - /// - /// - /// [Pure] private int Multiply(int x, int y) { return x * y; } - /// public void Foo() { - /// const int a = 2, b = 2; - /// Multiply(a, b); // Waring: Return value of pure method is not used - /// } - /// - [AttributeUsage(AttributeTargets.Method, Inherited = true)] - public sealed class PureAttribute : 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)] - public class PathReferenceAttribute : Attribute - { - public PathReferenceAttribute() { } - public PathReferenceAttribute([PathReference] string basePath) - { - BasePath = basePath; - } - - [NotNull] public string BasePath { get; private set; } - } - /// - /// 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) + /// Specify what is considered used implicitly + /// when marked with + /// or /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - 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)] - public sealed class AspMvcAreaAttribute : PathReferenceAttribute - { - public AspMvcAreaAttribute() { } - public AspMvcAreaAttribute([NotNull] string anonymousProperty) + [Flags] + public enum ImplicitUseTargetFlags { - AnonymousProperty = anonymousProperty; + Default = Itself, + Itself = 1, + /// Members of entity marked with attribute are considered used + Members = 2, + /// Entity marked with attribute and all its members considered used + WithMembers = Itself | Members } - - [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)] - 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)] - 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)] - 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)] - 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)] - 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)] - 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)] - 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)] - 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)] - 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)] - public sealed class AspMvcActionSelectorAttribute : Attribute { } - - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Field, Inherited = true)] - 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)] - 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)] - public sealed class RazorSectionAttribute : Attribute { } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/TestStack.BDDfy.csproj b/src/TestStack.BDDfy/TestStack.BDDfy.csproj index e54f9e2d..0e62ec6a 100644 --- a/src/TestStack.BDDfy/TestStack.BDDfy.csproj +++ b/src/TestStack.BDDfy/TestStack.BDDfy.csproj @@ -20,6 +20,10 @@ - + + + + + From 4e379ec964ec1f80bc862d65dc66b503c1f12948 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 21:38:07 +0000 Subject: [PATCH 09/34] remove missing doc reference --- src/TestStack.BDDfy/Properties/Annotations.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 65d45e51..bd53acd1 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -43,7 +43,6 @@ public enum ImplicitUseKindFlags /// /// Specify what is considered used implicitly - /// when marked with /// or /// [Flags] From dfd7e559a04a18185cb70da79d4ceb163955c6c9 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:02:31 +0000 Subject: [PATCH 10/34] use beta tag for packages built from a branch --- .github/workflows/build.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e34c57da..d86e48d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: runPublish: description: 'Publish Nuget ?' required: true - default: 'false' + default: false type: boolean jobs: @@ -34,8 +34,14 @@ jobs: id: gitversion - name: Format NuGet version - run: | - packageVersion="${{ steps.gitversion.outputs.majorMinorPatch }}.${{steps.gitversion.outputs.preReleaseNumber}}" + run: | + buildNumber="${{steps.gitversion.outputs.preReleaseNumber}}${{ steps.gitversion.outputs.buildMetaData }}" + + if [[ "${GITHUB_REF}" != "refs/heads/${{ github.event.repository.default_branch }}" ]]; then + buildNumber = "${buildNumber}-beta" + fi + + packageVersion="${{ steps.gitversion.outputs.majorMinorPatch }}.${buildNumber}" echo "packageVersion=$packageVersion" >> $GITHUB_OUTPUT id: formatversion From 21df381ef12e4817efb865acbb6e2a33410d81bb Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:09:43 +0000 Subject: [PATCH 11/34] fix the action script --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d86e48d7..c0eefe9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: buildNumber="${{steps.gitversion.outputs.preReleaseNumber}}${{ steps.gitversion.outputs.buildMetaData }}" if [[ "${GITHUB_REF}" != "refs/heads/${{ github.event.repository.default_branch }}" ]]; then - buildNumber = "${buildNumber}-beta" + buildNumber="${buildNumber}-beta" fi packageVersion="${{ steps.gitversion.outputs.majorMinorPatch }}.${buildNumber}" From 03c6ea3a042d4f660998b972866d29a5a3bf995c Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:22:59 +0000 Subject: [PATCH 12/34] try update gitversion to use beta for non default branches --- GitVersion.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 29e4741b..3646257a 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,9 +1,14 @@ -mode: ContinuousDelivery -next-version: 8.0.0 +mode: ContinuousDeployment +assembly-versioning-scheme: MajorMinorPatch branches: main: - increment: Patch regex: ^main$ is-release-branch: true + tag: '' + + other: + regex: .* + tag: beta + increment: Patch ignore: - sha: [] + sha: [] \ No newline at end of file From fec903edf8570de0455c1aae904077da49d9617f Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:26:56 +0000 Subject: [PATCH 13/34] fix GitVersion.yml --- GitVersion.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index 3646257a..640679d0 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -4,7 +4,6 @@ branches: main: regex: ^main$ is-release-branch: true - tag: '' other: regex: .* From fe622a4907230121f08e86fa255679bd382a5585 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:40:11 +0000 Subject: [PATCH 14/34] fixed gitversion --- GitVersion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index 640679d0..c89295f0 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -4,10 +4,10 @@ branches: main: regex: ^main$ is-release-branch: true + increment: Patch other: regex: .* - tag: beta increment: Patch ignore: sha: [] \ No newline at end of file From b547846306a538fb72c4f53552d6cf48be928c79 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 22:50:11 +0000 Subject: [PATCH 15/34] add MeansImplicitUseAttribute back in code --- src/TestStack.BDDfy/Properties/Annotations.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index bd53acd1..dde2ab95 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -2,6 +2,29 @@ namespace TestStack.BDDfy.Annotations { + /// + /// Should be used on attributes and causes ReSharper + /// to not mark symbols marked with such attributes as unused + /// (as well as by other usage inspections) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class MeansImplicitUseAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } = useKindFlags; + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; + } + + /// /// Indicates that the marked symbol is used implicitly /// (e.g. via reflection, in external library), so this symbol @@ -44,7 +67,7 @@ public enum ImplicitUseKindFlags /// /// Specify what is considered used implicitly /// or - /// + /// Ex [Flags] public enum ImplicitUseTargetFlags { From 1153dd0d39f58a937eb7848b4f574dc2e484acec Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Fri, 31 Oct 2025 23:03:08 +0000 Subject: [PATCH 16/34] clean up --- .../TestStack.BDDfy.Samples/TicTacToe/TicTacToe.cs | 1 - .../TestStack.BDDfy.Samples/TicTacToe/WinnerGame.cs | 1 - .../TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs | 6 +----- src/TestStack.BDDfy.Tests/Disposer/DisposingScenarios.cs | 1 - src/TestStack.BDDfy.Tests/HumanizerTests.cs | 1 - .../Scanner/FluentScanner/BDDfyUsingFluentApi.cs | 2 -- .../Scanner/FluentScanner/ExpressionExtensionsTests.cs | 1 - .../DefaultScanForStepsByMethodNameTests.cs | 1 - .../ReflectiveScanner/ExecutableAttributeScannerTests.cs | 1 - .../Stories/CanUseACustomStoryAttribute.cs | 3 +-- .../Stories/WhenAStoryHasUriAndImageMetadata.cs | 7 +------ src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs | 1 - src/TestStack.BDDfy/Configuration/Configurator.cs | 2 -- src/TestStack.BDDfy/Configuration/HtmlReportFactory.cs | 1 - src/TestStack.BDDfy/Configuration/IHumanizer.cs | 2 -- src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs | 8 +------- src/TestStack.BDDfy/Processors/ExceptionProcessor.cs | 1 - src/TestStack.BDDfy/Processors/InconclusiveException.cs | 1 - src/TestStack.BDDfy/Properties/Annotations.cs | 3 +++ .../Reporters/Html/DefaultHtmlReportConfiguration.cs | 4 ---- src/TestStack.BDDfy/Reporters/Html/HtmlReportModel.cs | 4 +--- src/TestStack.BDDfy/Reporters/Writers/FileWriter.cs | 2 -- .../Scanners/StepScanners/StepTitleException.cs | 1 - .../Scanners/StoryAttributeMetaDataScanner.cs | 1 - src/TestStack.BDDfy/Story.cs | 1 - 25 files changed, 8 insertions(+), 49 deletions(-) diff --git a/src/Samples/TestStack.BDDfy.Samples/TicTacToe/TicTacToe.cs b/src/Samples/TestStack.BDDfy.Samples/TicTacToe/TicTacToe.cs index 726295a3..056ce792 100644 --- a/src/Samples/TestStack.BDDfy.Samples/TicTacToe/TicTacToe.cs +++ b/src/Samples/TestStack.BDDfy.Samples/TicTacToe/TicTacToe.cs @@ -1,6 +1,5 @@ using Shouldly; using Xunit; -using Xunit.Extensions; namespace TestStack.BDDfy.Samples.TicTacToe { diff --git a/src/Samples/TestStack.BDDfy.Samples/TicTacToe/WinnerGame.cs b/src/Samples/TestStack.BDDfy.Samples/TicTacToe/WinnerGame.cs index 43e363eb..ce50e23e 100644 --- a/src/Samples/TestStack.BDDfy.Samples/TicTacToe/WinnerGame.cs +++ b/src/Samples/TestStack.BDDfy.Samples/TicTacToe/WinnerGame.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using NUnit.Framework; using Shouldly; namespace TestStack.BDDfy.Samples.TicTacToe diff --git a/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs b/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs index b2b10b6f..19cd6603 100644 --- a/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs +++ b/src/Samples/TestStack.BDDfy.Samples/UseExamplesWithFluentApi.cs @@ -1,8 +1,4 @@ -using Microsoft.VisualStudio.TestPlatform.Utilities; -using Shouldly; -using TestStack.BDDfy.Configuration; -using TestStack.BDDfy.Reporters; -using TestStack.BDDfy.Reporters.Html; +using Shouldly; using Xunit; namespace TestStack.BDDfy.Samples diff --git a/src/TestStack.BDDfy.Tests/Disposer/DisposingScenarios.cs b/src/TestStack.BDDfy.Tests/Disposer/DisposingScenarios.cs index a47ad897..86b41b84 100644 --- a/src/TestStack.BDDfy.Tests/Disposer/DisposingScenarios.cs +++ b/src/TestStack.BDDfy.Tests/Disposer/DisposingScenarios.cs @@ -3,7 +3,6 @@ using Shouldly; using TestStack.BDDfy.Tests.Exceptions; using Xunit; -using Xunit.Extensions; namespace TestStack.BDDfy.Tests.Disposer { diff --git a/src/TestStack.BDDfy.Tests/HumanizerTests.cs b/src/TestStack.BDDfy.Tests/HumanizerTests.cs index 6898f5d6..e5d1f9ff 100644 --- a/src/TestStack.BDDfy.Tests/HumanizerTests.cs +++ b/src/TestStack.BDDfy.Tests/HumanizerTests.cs @@ -1,6 +1,5 @@ using System; using Shouldly; -using TestStack.BDDfy.Configuration; using Xunit; namespace TestStack.BDDfy.Tests diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/BDDfyUsingFluentApi.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/BDDfyUsingFluentApi.cs index 08e10ab2..cbca64c6 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/BDDfyUsingFluentApi.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/BDDfyUsingFluentApi.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using NUnit.Framework; using Shouldly; using TestStack.BDDfy.Configuration; using Xunit; diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ExpressionExtensionsTests.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ExpressionExtensionsTests.cs index 988d7966..f58b2a25 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ExpressionExtensionsTests.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ExpressionExtensionsTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using NUnit.Framework; using Shouldly; using Xunit; diff --git a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/DefaultScanForStepsByMethodNameTests.cs b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/DefaultScanForStepsByMethodNameTests.cs index 02579cad..390eb199 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/DefaultScanForStepsByMethodNameTests.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/DefaultScanForStepsByMethodNameTests.cs @@ -1,5 +1,4 @@ using System.Linq; -using NUnit.Framework; using Shouldly; using Xunit; diff --git a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/ExecutableAttributeScannerTests.cs b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/ExecutableAttributeScannerTests.cs index 2d5b6481..c8a49be2 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/ExecutableAttributeScannerTests.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/ExecutableAttributeScannerTests.cs @@ -1,5 +1,4 @@ using System.Linq; -using NUnit.Framework; using Shouldly; using Xunit; diff --git a/src/TestStack.BDDfy.Tests/Stories/CanUseACustomStoryAttribute.cs b/src/TestStack.BDDfy.Tests/Stories/CanUseACustomStoryAttribute.cs index 5df7282a..fc2e003b 100644 --- a/src/TestStack.BDDfy.Tests/Stories/CanUseACustomStoryAttribute.cs +++ b/src/TestStack.BDDfy.Tests/Stories/CanUseACustomStoryAttribute.cs @@ -1,5 +1,4 @@ -using NUnit.Framework; -using Shouldly; +using Shouldly; using Xunit; namespace TestStack.BDDfy.Tests.Stories diff --git a/src/TestStack.BDDfy.Tests/Stories/WhenAStoryHasUriAndImageMetadata.cs b/src/TestStack.BDDfy.Tests/Stories/WhenAStoryHasUriAndImageMetadata.cs index d585ab83..39b6d75f 100644 --- a/src/TestStack.BDDfy.Tests/Stories/WhenAStoryHasUriAndImageMetadata.cs +++ b/src/TestStack.BDDfy.Tests/Stories/WhenAStoryHasUriAndImageMetadata.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; +using Xunit; using Shouldly; namespace TestStack.BDDfy.Tests.Stories diff --git a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs index ed8c9cf1..55365c19 100644 --- a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs @@ -1,5 +1,4 @@ using System.Reflection; -using TestStack.BDDfy; namespace TestStack.BDDfy.Abstractions; diff --git a/src/TestStack.BDDfy/Configuration/Configurator.cs b/src/TestStack.BDDfy/Configuration/Configurator.cs index a33b2566..b31d876c 100644 --- a/src/TestStack.BDDfy/Configuration/Configurator.cs +++ b/src/TestStack.BDDfy/Configuration/Configurator.cs @@ -1,5 +1,3 @@ -using System; -using TestStack.BDDfy; using TestStack.BDDfy.Abstractions; using TestStack.BDDfy.Factories; diff --git a/src/TestStack.BDDfy/Configuration/HtmlReportFactory.cs b/src/TestStack.BDDfy/Configuration/HtmlReportFactory.cs index e9df6fc2..980f46ea 100644 --- a/src/TestStack.BDDfy/Configuration/HtmlReportFactory.cs +++ b/src/TestStack.BDDfy/Configuration/HtmlReportFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using TestStack.BDDfy.Processors; using TestStack.BDDfy.Reporters.Html; namespace TestStack.BDDfy.Configuration diff --git a/src/TestStack.BDDfy/Configuration/IHumanizer.cs b/src/TestStack.BDDfy/Configuration/IHumanizer.cs index fba0f833..02c4583c 100644 --- a/src/TestStack.BDDfy/Configuration/IHumanizer.cs +++ b/src/TestStack.BDDfy/Configuration/IHumanizer.cs @@ -1,5 +1,3 @@ -using System; - namespace TestStack.BDDfy.Configuration { public interface IHumanizer diff --git a/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs b/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs index 44a1acfa..2196a3e7 100644 --- a/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs +++ b/src/TestStack.BDDfy/Factories/IFluentScannerFactory.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TestStack.BDDfy.Factories; +namespace TestStack.BDDfy.Factories; public interface IFluentScannerFactory { diff --git a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs index d7ffd2be..f9e04ca2 100644 --- a/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs +++ b/src/TestStack.BDDfy/Processors/ExceptionProcessor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; diff --git a/src/TestStack.BDDfy/Processors/InconclusiveException.cs b/src/TestStack.BDDfy/Processors/InconclusiveException.cs index a0ee8841..a67f9e05 100644 --- a/src/TestStack.BDDfy/Processors/InconclusiveException.cs +++ b/src/TestStack.BDDfy/Processors/InconclusiveException.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; namespace TestStack.BDDfy.Processors { diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index dde2ab95..600293f8 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace TestStack.BDDfy.Annotations { @@ -8,6 +9,7 @@ namespace TestStack.BDDfy.Annotations /// (as well as by other usage inspections) /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + [ExcludeFromCodeCoverage] public sealed class MeansImplicitUseAttribute( ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute { @@ -31,6 +33,7 @@ public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) /// will not be marked as unused (as well as by other usage inspections) /// [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] + [ExcludeFromCodeCoverage] public sealed class UsedImplicitlyAttribute( ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute { diff --git a/src/TestStack.BDDfy/Reporters/Html/DefaultHtmlReportConfiguration.cs b/src/TestStack.BDDfy/Reporters/Html/DefaultHtmlReportConfiguration.cs index 1bb87d82..b1617c32 100644 --- a/src/TestStack.BDDfy/Reporters/Html/DefaultHtmlReportConfiguration.cs +++ b/src/TestStack.BDDfy/Reporters/Html/DefaultHtmlReportConfiguration.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Reflection; - namespace TestStack.BDDfy.Reporters.Html { public class DefaultHtmlReportConfiguration : IHtmlReportConfiguration diff --git a/src/TestStack.BDDfy/Reporters/Html/HtmlReportModel.cs b/src/TestStack.BDDfy/Reporters/Html/HtmlReportModel.cs index 3efc1375..07321a80 100644 --- a/src/TestStack.BDDfy/Reporters/Html/HtmlReportModel.cs +++ b/src/TestStack.BDDfy/Reporters/Html/HtmlReportModel.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace TestStack.BDDfy.Reporters.Html +namespace TestStack.BDDfy.Reporters.Html { public class HtmlReportModel(IHtmlReportConfiguration configuration, ReportModel reportModel): FileReportModel(reportModel) { diff --git a/src/TestStack.BDDfy/Reporters/Writers/FileWriter.cs b/src/TestStack.BDDfy/Reporters/Writers/FileWriter.cs index 0919e338..4012b659 100644 --- a/src/TestStack.BDDfy/Reporters/Writers/FileWriter.cs +++ b/src/TestStack.BDDfy/Reporters/Writers/FileWriter.cs @@ -1,6 +1,4 @@ -using System; using System.IO; -using TestStack.BDDfy.Reporters.Diagnostics; namespace TestStack.BDDfy.Reporters.Writers { diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/StepTitleException.cs b/src/TestStack.BDDfy/Scanners/StepScanners/StepTitleException.cs index 1e68d310..3caf6b75 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/StepTitleException.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/StepTitleException.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; namespace TestStack.BDDfy { diff --git a/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs b/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs index 0d4f3d5e..f2a95995 100644 --- a/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StoryAttributeMetaDataScanner.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Linq; namespace TestStack.BDDfy diff --git a/src/TestStack.BDDfy/Story.cs b/src/TestStack.BDDfy/Story.cs index 0765350b..6c37a2b6 100644 --- a/src/TestStack.BDDfy/Story.cs +++ b/src/TestStack.BDDfy/Story.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; From 0f563803b9c8e44d0e13784956136fbd08eaf5f8 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 19:25:39 +0000 Subject: [PATCH 17/34] revert to keep all Annotations.cs --- src/TestStack.BDDfy/Properties/Annotations.cs | 552 +++++++++++++++++- 1 file changed, 537 insertions(+), 15 deletions(-) diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 600293f8..fc41d04f 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -1,31 +1,251 @@ using System; using System.Diagnostics.CodeAnalysis; +#pragma warning disable 1591 +// 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 { /// - /// Should be used on attributes and causes ReSharper - /// to not mark symbols marked with such attributes as unused - /// (as well as by other usage inspections) + /// Indicates that the value of the marked element could be null sometimes, + /// so the check for null is necessary before its usage /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + /// + /// [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 MeansImplicitUseAttribute( - ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute + 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 MeansImplicitUseAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + public string FormatParameterName { get; private set; } = formatParameterName; + } - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + /// + /// 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 { } - public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) { } + /// + /// 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; + } - [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } = useKindFlags; - [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; + 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 @@ -50,6 +270,29 @@ public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; } + /// + /// Should be used on attributes and causes ReSharper + /// to not mark symbols marked with such attributes as unused + /// (as well as by other usage inspections) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + [ExcludeFromCodeCoverage] + public sealed class MeansImplicitUseAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } = useKindFlags; + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } = targetFlags; + } + [Flags] public enum ImplicitUseKindFlags { @@ -69,8 +312,9 @@ public enum ImplicitUseKindFlags /// /// Specify what is considered used implicitly + /// when marked with /// or - /// Ex + /// [Flags] public enum ImplicitUseTargetFlags { @@ -81,4 +325,282 @@ 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 + /// + [MeansImplicitUse] + public sealed class PublicAPIAttribute : Attribute + { + 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 method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute + /// + /// + /// [Pure] private int Multiply(int x, int y) { return x * y; } + /// public void Foo() { + /// const int a = 2, b = 2; + /// Multiply(a, b); // Waring: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method, Inherited = true)] + public sealed class PureAttribute : 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 From 39b701f95a25c3c3a6b5c2ed9434f0e71b2a9d77 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 20:05:37 +0000 Subject: [PATCH 18/34] additional coverage --- .gitignore | 3 +- .../Atm/AccountHasInsufficientFund.cs | 23 ++++++++----- .../Configuration/TestRunnerTests.cs | 5 ++- src/TestStack.BDDfy/BDDfyExtensions.cs | 32 ++++++++++++++----- src/TestStack.BDDfy/Properties/Annotations.cs | 1 + 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index ad4aa3cb..ecb096af 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ obj/ packages/* PackageBuild/* Build/* -TestResult.xml +TestResult* +CoverageReport TestStack.BDDfy.sln.ide/graph _NCrunch_TestStack.BDDfy/ TestStack.BDDfy.sln.ide/ diff --git a/src/Samples/TestStack.BDDfy.Samples/Atm/AccountHasInsufficientFund.cs b/src/Samples/TestStack.BDDfy.Samples/Atm/AccountHasInsufficientFund.cs index e9e2a205..ae90dcb9 100644 --- a/src/Samples/TestStack.BDDfy.Samples/Atm/AccountHasInsufficientFund.cs +++ b/src/Samples/TestStack.BDDfy.Samples/Atm/AccountHasInsufficientFund.cs @@ -10,43 +10,43 @@ public class AccountHasInsufficientFund // You can override step text using executable attributes [Given("Given the Account Balance is $10")] - void GivenTheAccountBalanceIs10() + internal void GivenTheAccountBalanceIs10() { _card = new Card(true, 10); } - void And_given_the_Card_is_valid() + internal void And_given_the_Card_is_valid() { } - void AndGivenTheMachineContainsEnoughMoney() + internal void AndGivenTheMachineContainsEnoughMoney() { _atm = new Atm(100); } [When("When the Account Holder requests $20")] - void WhenTheAccountHolderRequests20() + internal void WhenTheAccountHolderRequests20() { _atm.RequestMoney(_card, 20); } - void Then_the_ATM_should_not_dispense_any_Money() + internal void Then_the_ATM_should_not_dispense_any_Money() { _atm.DispenseValue.ShouldBe(0); } - void And_the_ATM_should_say_there_are_Insufficient_Funds() + internal void And_the_ATM_should_say_there_are_Insufficient_Funds() { _atm.Message.ShouldBe(DisplayMessage.InsufficientFunds); } [AndThen("And the Account Balance should be $20")] - void AndTheAccountBalanceShouldBe20() + internal void AndTheAccountBalanceShouldBe20() { _card.AccountBalance.ShouldBe(10); } - void And_the_Card_should_be_returned() + internal void And_the_Card_should_be_returned() { _atm.CardIsRetained.ShouldBe(false); } @@ -56,5 +56,12 @@ public void Verify() { this.BDDfy(); } + + [Fact] + public void VerifyLazy() + { + var engine = this.LazyBDDfy(); + engine.Run(); + } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Configuration/TestRunnerTests.cs b/src/TestStack.BDDfy.Tests/Configuration/TestRunnerTests.cs index f89aece7..1213e381 100644 --- a/src/TestStack.BDDfy.Tests/Configuration/TestRunnerTests.cs +++ b/src/TestStack.BDDfy.Tests/Configuration/TestRunnerTests.cs @@ -2,11 +2,11 @@ using System.Linq; using Shouldly; using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Configuration { - [Collection("ExclusiveAccessToConfigurator")] public class TestRunnerTests { public class ScenarioWithFailingThen @@ -33,6 +33,9 @@ public void PassingAndThen() } } + + [Collection(TestCollectionName.ModifiesConfigurator)] + public class When_StopExecutionOnFailingThen_IsSetToTrue { [Fact] diff --git a/src/TestStack.BDDfy/BDDfyExtensions.cs b/src/TestStack.BDDfy/BDDfyExtensions.cs index 3340f3fa..6e484c8f 100644 --- a/src/TestStack.BDDfy/BDDfyExtensions.cs +++ b/src/TestStack.BDDfy/BDDfyExtensions.cs @@ -50,12 +50,20 @@ public static Engine LazyBDDfy(this object testObject, string scenarioTi /// Overrides the default scenario title and is displayed in the reports. /// Caller (populated by [CallerMemberName]) /// - public static Story BDDfy(this object testObject, string scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] string caller = null) + public static Story BDDfy( + this object testObject, + string scenarioTitle = null, + [System.Runtime.CompilerServices.CallerMemberName] + string caller = null) { return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(caller)).Run(); } - public static Engine LazyBDDfy(this object testObject, string scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] string caller = null) + public static Engine LazyBDDfy( + this object testObject, + string scenarioTitle = null, + [System.Runtime.CompilerServices.CallerMemberName] + string caller = null) { return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(caller)); } @@ -68,22 +76,30 @@ public static Engine LazyBDDfy(this object testObject, string scenarioTitle = nu /// Overrides the default scenario title and is displayed in the reports. /// Caller (populated by [CallerMemberName]) /// - public static Story BDDfy(this object testObject, string scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] string caller = null) - where TStory : class + public static Story BDDfy( + this object testObject, + string scenarioTitle = null, + [System.Runtime.CompilerServices.CallerMemberName] + string caller = null) + where TStory : class { return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(caller), typeof(TStory)).Run(); } - public static Engine LazyBDDfy(this object testObject, string scenarioTitle = null, [System.Runtime.CompilerServices.CallerMemberName] string caller = null) - where TStory : class + public static Engine LazyBDDfy( + this object testObject, + string scenarioTitle = null, + [System.Runtime.CompilerServices.CallerMemberName] + string caller = null) + where TStory : class { return InternalLazyBDDfy(testObject, scenarioTitle ?? Configurator.Humanizer.Humanize(caller), typeof(TStory)); } #endif static Engine InternalLazyBDDfy( - object testObject, - string scenarioTitle, + object testObject, + string scenarioTitle, Type explicitStoryType = null) { var testContext = TestContext.GetContext(testObject); diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index fc41d04f..8e56fe91 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -331,6 +331,7 @@ public enum ImplicitUseTargetFlags /// which should not be removed and so is treated as used /// [MeansImplicitUse] + [ExcludeFromCodeCoverage] public sealed class PublicAPIAttribute : Attribute { public PublicAPIAttribute() { } From 71cde216d7189ac908e8ee8b1277a58a6ef020c7 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 23:01:21 +0000 Subject: [PATCH 19/34] improving code coverage --- coverage-report.ps1 | 10 +++ .../Configuration/BatchProcessorsTests.cs | 38 ++++++++++- .../Configuration/ProcessorPipelineTests.cs | 3 +- .../Configuration/StepExecutorTests.cs | 3 +- .../ExclusiveAccessToConfiguratorFixture.cs | 9 --- .../ModifiesConfiguratorFixture.cs | 10 +++ .../FluentScanner/ComplexStepsTests.cs | 3 +- ...EmptyOrWhitespaceWillResultInMethodName.cs | 48 ++++++++++++++ .../Configuration/BatchProcessors.cs | 22 +++---- .../Configuration/ComponentFactory.cs | 16 ++--- src/TestStack.BDDfy/Properties/Annotations.cs | 8 +-- .../ReflectiveScenarioScanner.cs | 7 +- .../ExecutableAttribute.cs | 1 + .../ExecutableAttributeStepScanner.cs | 66 ++++++++++--------- 14 files changed, 169 insertions(+), 75 deletions(-) create mode 100644 coverage-report.ps1 delete mode 100644 src/TestStack.BDDfy.Tests/ExclusiveAccessToConfiguratorFixture.cs create mode 100644 src/TestStack.BDDfy.Tests/ModifiesConfiguratorFixture.cs create mode 100644 src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs diff --git a/coverage-report.ps1 b/coverage-report.ps1 new file mode 100644 index 00000000..9c7b651c --- /dev/null +++ b/coverage-report.ps1 @@ -0,0 +1,10 @@ +dotnet test ./src ` + --collect:"XPlat Code Coverage" ` + --results-directory ./TestResults + +reportgenerator ` + -reports:./TestResults/**/coverage.cobertura.xml ` + -targetdir:./CoverageReport ` + -reporttypes:Html + +Remove-Item -Recurse -Force ./TestResults \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Configuration/BatchProcessorsTests.cs b/src/TestStack.BDDfy.Tests/Configuration/BatchProcessorsTests.cs index b6ac86f7..b253c45c 100644 --- a/src/TestStack.BDDfy.Tests/Configuration/BatchProcessorsTests.cs +++ b/src/TestStack.BDDfy.Tests/Configuration/BatchProcessorsTests.cs @@ -1,17 +1,21 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using Shouldly; using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Reporters.Diagnostics; using TestStack.BDDfy.Reporters.Html; using TestStack.BDDfy.Reporters.MarkDown; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Configuration { + [Collection(TestCollectionName.ModifiesConfigurator)] public class BatchProcessorsTests { static bool MetroReportProcessorIsActive(IBatchProcessor batchProcessor) { - return batchProcessor is HtmlReporter && ((HtmlReporter)batchProcessor).ReportBuilder is MetroReportBuilder; + return batchProcessor is HtmlReporter reporter && reporter.ReportBuilder is MetroReportBuilder; } [Fact] @@ -67,5 +71,35 @@ public void ReturnsHtmlMetroReporterWhenItIsActivated() Configurator.BatchProcessors.HtmlMetroReport.Disable(); } + + [Fact] + public void ReturnsDianosticsReporterWhenItIsActivated() + { + Configurator.BatchProcessors.DiagnosticsReport.Enable(); + + var processors = Configurator.BatchProcessors.GetProcessors().ToList(); + + processors.ShouldContain(p=> p is DiagnosticsReporter, 1); + + Configurator.BatchProcessors.DiagnosticsReport.Disable(); + } + + [Fact] + public void ReturnsAdditionalBatchProcessorsWhenAdded() + { + Configurator.BatchProcessors.Add(new FooBatchProcessor()); + + var processors = Configurator.BatchProcessors.GetProcessors().ToList(); + + processors.ShouldContain(p => p is FooBatchProcessor, 1); + } + + private class FooBatchProcessor : IBatchProcessor + { + public void Process(IEnumerable stories) + { + throw new System.NotImplementedException(); + } + } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Configuration/ProcessorPipelineTests.cs b/src/TestStack.BDDfy.Tests/Configuration/ProcessorPipelineTests.cs index 3ca8a1ab..1c042227 100644 --- a/src/TestStack.BDDfy.Tests/Configuration/ProcessorPipelineTests.cs +++ b/src/TestStack.BDDfy.Tests/Configuration/ProcessorPipelineTests.cs @@ -3,11 +3,12 @@ using TestStack.BDDfy.Configuration; using TestStack.BDDfy.Processors; using TestStack.BDDfy.Reporters; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Configuration { - [Collection("ExclusiveAccessToConfigurator")] + [Collection(TestCollectionName.ModifiesConfigurator)] public class ProcessorPipelineTests { [Fact] diff --git a/src/TestStack.BDDfy.Tests/Configuration/StepExecutorTests.cs b/src/TestStack.BDDfy.Tests/Configuration/StepExecutorTests.cs index 8d9e1916..8e78f3b2 100644 --- a/src/TestStack.BDDfy.Tests/Configuration/StepExecutorTests.cs +++ b/src/TestStack.BDDfy.Tests/Configuration/StepExecutorTests.cs @@ -1,11 +1,12 @@ using System.Text; using Shouldly; using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Tests.Concurrency; using Xunit; namespace TestStack.BDDfy.Tests.Configuration { - [Collection("ExclusiveAccessToConfigurator")] + [Collection(TestCollectionName.ModifiesConfigurator)] public class StepExecutorTests { private class TestStepExecutor : StepExecutor diff --git a/src/TestStack.BDDfy.Tests/ExclusiveAccessToConfiguratorFixture.cs b/src/TestStack.BDDfy.Tests/ExclusiveAccessToConfiguratorFixture.cs deleted file mode 100644 index 2c9f5f33..00000000 --- a/src/TestStack.BDDfy.Tests/ExclusiveAccessToConfiguratorFixture.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace TestStack.BDDfy.Tests -{ - [CollectionDefinition("ExclusiveAccessToConfigurator", DisableParallelization = true)] - public class ExclusiveAccessToConfiguratorFixture - { - } -} \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/ModifiesConfiguratorFixture.cs b/src/TestStack.BDDfy.Tests/ModifiesConfiguratorFixture.cs new file mode 100644 index 00000000..c5fae0dc --- /dev/null +++ b/src/TestStack.BDDfy.Tests/ModifiesConfiguratorFixture.cs @@ -0,0 +1,10 @@ +using TestStack.BDDfy.Tests.Concurrency; +using Xunit; + +namespace TestStack.BDDfy.Tests +{ + [CollectionDefinition(TestCollectionName.ModifiesConfigurator, DisableParallelization = true)] + public class ModifiesConfiguratorFixture + { + } +} \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ComplexStepsTests.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ComplexStepsTests.cs index e97b5c23..b23776b3 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ComplexStepsTests.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ComplexStepsTests.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using Shouldly; +using TestStack.BDDfy.Tests.Concurrency; using TestStack.BDDfy.Tests.Configuration; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { - [Collection("ExclusiveAccessToConfigurator")] + [Collection(TestCollectionName.ModifiesConfigurator)] public class ComplexStepsTests { private int count; diff --git a/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs new file mode 100644 index 00000000..c9e26a34 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/ReflectiveScanner/AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName.cs @@ -0,0 +1,48 @@ +using Shouldly; +using System.Linq; +using TestStack.BDDfy.Configuration; +using Xunit; + +namespace TestStack.BDDfy.Tests.Scanner.ReflectiveScanner +{ + public class AnyExecutableAttributeWhichIsEmptyOrWhitespaceWillResultInMethodName + { + private class TypeWithDecoratedMethods + { + public TypeWithDecoratedMethods() + { + this.WithExamples(new ExampleTable("Example") + { + {1}, + {2} + }); + } + + [Given("")] + public void GivenWithEmptyString() { } + [When(" ")] + public void WhenWithWhitespace() { } + [Then(null)] + public void ThenWithNull() { } + + [AndThen("Then with title should work too")] + public void AndThenWithTitle() { } + } + + [Fact] + public void WhenUsingReflectiveScanner_MethodNamesAreUsedAsStepTitles_When_StepTitlesAreWhiteSpace() + { + var testObject = new TypeWithDecoratedMethods(); + + var stepScanners = Configurator.Scanners.GetStepScanners(testObject).ToArray(); + var scanner = new ReflectiveScenarioScanner(stepScanners); + var scenario = scanner.Scan(TestContext.GetContext(testObject)).First(); + var steps = scenario.Steps; + + steps[0].Title.ShouldBe("Given with empty string"); + steps[1].Title.ShouldBe("When with whitespace"); + steps[2].Title.ShouldBe("Then with null"); + steps[3].Title.ShouldBe("Then with title should work too"); + } + } +} diff --git a/src/TestStack.BDDfy/Configuration/BatchProcessors.cs b/src/TestStack.BDDfy/Configuration/BatchProcessors.cs index b90000cf..c17a52dd 100644 --- a/src/TestStack.BDDfy/Configuration/BatchProcessors.cs +++ b/src/TestStack.BDDfy/Configuration/BatchProcessors.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using TestStack.BDDfy.Processors; using TestStack.BDDfy.Reporters.Diagnostics; using TestStack.BDDfy.Reporters.Html; @@ -9,7 +8,9 @@ namespace TestStack.BDDfy.Configuration { public class BatchProcessors { - IEnumerable _GetProcessors() + readonly List _addedProcessors = []; + + private IEnumerable YieldProcessors() { var htmlReporter = HtmlReport.ConstructFor(StoryCache.Stories); if (htmlReporter != null) @@ -33,19 +34,14 @@ IEnumerable _GetProcessors() } } - private readonly BatchProcessorFactory _htmlReportFactory = new(() => new HtmlReporter(new DefaultHtmlReportConfiguration())); - public BatchProcessorFactory HtmlReport { get { return _htmlReportFactory; } } - - private readonly BatchProcessorFactory _htmlMetroReportFactory = new(() => new HtmlReporter(new DefaultHtmlReportConfiguration(), new MetroReportBuilder()), false); - public BatchProcessorFactory HtmlMetroReport { get { return _htmlMetroReportFactory; } } + public BatchProcessorFactory HtmlReport { get; } = new(() => new HtmlReporter(new DefaultHtmlReportConfiguration())); - private readonly BatchProcessorFactory _markDownFactory = new(() => new MarkDownReporter(), false); - public BatchProcessorFactory MarkDownReport { get { return _markDownFactory; } } + public BatchProcessorFactory HtmlMetroReport { get; } = new(() + => new HtmlReporter(new DefaultHtmlReportConfiguration(), new MetroReportBuilder()), false); - private readonly BatchProcessorFactory _diagnosticsFactory = new(() => new DiagnosticsReporter(), false); - public BatchProcessorFactory DiagnosticsReport { get { return _diagnosticsFactory; } } + public BatchProcessorFactory MarkDownReport { get; } = new(() => new MarkDownReporter(), false); - readonly List _addedProcessors = new(); + public BatchProcessorFactory DiagnosticsReport { get; } = new(() => new DiagnosticsReporter(), false); public BatchProcessors Add(IBatchProcessor processor) { @@ -55,7 +51,7 @@ public BatchProcessors Add(IBatchProcessor processor) public IEnumerable GetProcessors() { - return _GetProcessors().ToList(); + return [.. YieldProcessors()]; } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Configuration/ComponentFactory.cs b/src/TestStack.BDDfy/Configuration/ComponentFactory.cs index 8f5ed7ca..6f99ca68 100644 --- a/src/TestStack.BDDfy/Configuration/ComponentFactory.cs +++ b/src/TestStack.BDDfy/Configuration/ComponentFactory.cs @@ -2,15 +2,13 @@ namespace TestStack.BDDfy.Configuration { - public class ComponentFactory where TComponent : class + public abstract class ComponentFactory where TComponent : class { - private bool _active = true; + private const bool DefaultState = true; + private bool _active = DefaultState; private Predicate _runsOn = o => true; readonly Func _factory; - internal ComponentFactory(Func factory) - { - _factory = factory; - } + internal ComponentFactory(Func factory):this(factory, DefaultState) { } internal ComponentFactory(Func factory, bool active) { @@ -30,10 +28,10 @@ public void Enable() public TComponent ConstructFor(TMaterial material) { - if (!_active || !_runsOn(material)) - return null; + if (_active && _runsOn(material)) + return _factory(); - return _factory(); + return null; } public void RunsOn(Predicate runOn) diff --git a/src/TestStack.BDDfy/Properties/Annotations.cs b/src/TestStack.BDDfy/Properties/Annotations.cs index 8e56fe91..27f81204 100644 --- a/src/TestStack.BDDfy/Properties/Annotations.cs +++ b/src/TestStack.BDDfy/Properties/Annotations.cs @@ -1,7 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; -#pragma warning disable 1591 +#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 @@ -277,7 +278,7 @@ public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] [ExcludeFromCodeCoverage] - public sealed class MeansImplicitUseAttribute( + public class MeansImplicitUseAttribute( ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) : Attribute { public MeansImplicitUseAttribute() @@ -330,9 +331,8 @@ public enum ImplicitUseTargetFlags /// This attribute is intended to mark publicly available API /// which should not be removed and so is treated as used /// - [MeansImplicitUse] [ExcludeFromCodeCoverage] - public sealed class PublicAPIAttribute : Attribute + public sealed class PublicAPIAttribute : MeansImplicitUseAttribute { public PublicAPIAttribute() { } public PublicAPIAttribute([NotNull] string comment) diff --git a/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs b/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs index 21ca2330..adcca8a5 100644 --- a/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs +++ b/src/TestStack.BDDfy/Scanners/ScenarioScanners/ReflectiveScenarioScanner.cs @@ -103,11 +103,10 @@ public virtual IEnumerable GetMethodsOfInterest(Type scenarioType) var setMethods = properties.Select(p => p.GetSetMethod(true)); var allPropertyMethods = getMethods.Union(setMethods); - return scenarioType + return [.. scenarioType .GetMethods(bindingFlags) - .Where(m => !m.GetCustomAttributes(typeof(IgnoreStepAttribute), false).Any()) // it is not decorated with IgnoreStep - .Except(allPropertyMethods) // properties cannot be steps; only methods - .ToList(); + .Where(m => m.GetCustomAttribute() is null) + .Except(allPropertyMethods)]; } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttribute.cs b/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttribute.cs index 7b64b30f..dcc4c58d 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttribute.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using TestStack.BDDfy.Annotations; namespace TestStack.BDDfy diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttributeStepScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttributeStepScanner.cs index 748fe08f..3c4fc75d 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttributeStepScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/ExecutableAttribute/ExecutableAttributeStepScanner.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -28,25 +29,30 @@ public class ExecutableAttributeStepScanner : IStepScanner { public IEnumerable Scan(ITestContext testContext, MethodInfo candidateMethod) { - var executableAttribute = (ExecutableAttribute)candidateMethod.GetCustomAttributes(typeof(ExecutableAttribute), true).FirstOrDefault(); - if(executableAttribute == null) + var executableAttribute = candidateMethod.GetCustomAttribute(true); + if (executableAttribute == null) yield break; var stepTitle = new StepTitle(executableAttribute.StepTitle); - if(string.IsNullOrEmpty(stepTitle)) + if (string.IsNullOrWhiteSpace(stepTitle)) stepTitle = new StepTitle(Configurator.Humanizer.Humanize(candidateMethod.Name)); - var stepAsserts = IsAssertingByAttribute(candidateMethod); var shouldReport = executableAttribute.ShouldReport; var runStepWithArgsAttributes = (RunStepWithArgsAttribute[])candidateMethod.GetCustomAttributes(typeof(RunStepWithArgsAttribute), true); if (runStepWithArgsAttributes.Length == 0) { - var stepAction = StepActionFactory.GetStepAction(candidateMethod, new object[0]); - yield return new Step(stepAction, stepTitle, stepAsserts, executableAttribute.ExecutionOrder, shouldReport, new List()) - { - ExecutionSubOrder = executableAttribute.Order - }; + var stepAction = StepActionFactory.GetStepAction(candidateMethod, []); + yield return new Step( + stepAction, + stepTitle, + executableAttribute.Asserts, + executableAttribute.ExecutionOrder, + shouldReport, + []) + { + ExecutionSubOrder = executableAttribute.Order + }; } foreach (var runStepWithArgsAttribute in runStepWithArgsAttributes) @@ -62,25 +68,29 @@ public IEnumerable Scan(ITestContext testContext, MethodInfo candidateMeth methodName = string.Format(executableAttribute.StepTitle, flatInput); var stepAction = StepActionFactory.GetStepAction(candidateMethod, inputArguments); - yield return new Step(stepAction, new StepTitle(methodName), stepAsserts, - executableAttribute.ExecutionOrder, shouldReport, new List()) - { - ExecutionSubOrder = executableAttribute.Order - }; + yield return new Step( + stepAction, + new StepTitle(methodName), + executableAttribute.Asserts, + executableAttribute.ExecutionOrder, + shouldReport, + []) + { + ExecutionSubOrder = executableAttribute.Order + }; } } public IEnumerable Scan(ITestContext testContext, MethodInfo method, Example example) { - var executableAttribute = (ExecutableAttribute)method.GetCustomAttributes(typeof(ExecutableAttribute), true).FirstOrDefault(); + var executableAttribute = method.GetCustomAttribute(true); if (executableAttribute == null) yield break; string stepTitle = executableAttribute.StepTitle; - if (string.IsNullOrEmpty(stepTitle)) + if (string.IsNullOrWhiteSpace(stepTitle)) stepTitle = Configurator.Humanizer.Humanize(method.Name); - var stepAsserts = IsAssertingByAttribute(method); var shouldReport = executableAttribute.ShouldReport; var methodParameters = method.GetParameters(); @@ -101,20 +111,14 @@ public IEnumerable Scan(ITestContext testContext, MethodInfo method, Examp } } - var stepAction = StepActionFactory.GetStepAction(method, inputs.ToArray()); - yield return new Step(stepAction, new StepTitle(stepTitle), stepAsserts, executableAttribute.ExecutionOrder, shouldReport, new List()); - } - - - private static bool IsAssertingByAttribute(MethodInfo method) - { - var attribute = GetExecutableAttribute(method); - return attribute.Asserts; - } - - private static ExecutableAttribute GetExecutableAttribute(MethodInfo method) - { - return (ExecutableAttribute)method.GetCustomAttributes(typeof(ExecutableAttribute), true).First(); + var stepAction = StepActionFactory.GetStepAction(method, [.. inputs]); + yield return new Step( + stepAction, + new StepTitle(stepTitle), + executableAttribute.Asserts, + executableAttribute.ExecutionOrder, + shouldReport, + []); } } } \ No newline at end of file From ce882667f9510f56d7f8782193e17e541639210f Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 23:07:10 +0000 Subject: [PATCH 20/34] publish manual or default branch --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0eefe9b..e9db4cda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,7 +166,7 @@ jobs: publish-nuget: runs-on: ubuntu-latest needs: build - if: github.event.inputs.runPublish == 'true' && github.ref_name == github.event.repository.default_branch + if: github.event.inputs.runPublish == 'true' || github.ref_name == github.event.repository.default_branch environment: name: Publish url: https://www.nuget.org/packages/TestStack.BDDfy/ From b02f1037f7c1494afc0345f09ed8a8ae8267b796 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 23:29:05 +0000 Subject: [PATCH 21/34] change workflow to main line --- GitVersion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index c89295f0..affe7841 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -mode: ContinuousDeployment +mode: MainLine assembly-versioning-scheme: MajorMinorPatch branches: main: From 65e6f1be81657af589217ce39b68dd1a0a2dd3d5 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 1 Nov 2025 23:36:21 +0000 Subject: [PATCH 22/34] fix gitversion config --- GitVersion.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index affe7841..07063aa5 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,12 +1,15 @@ -mode: MainLine assembly-versioning-scheme: MajorMinorPatch +strategies: +- Mainline branches: main: + mode: ContinuousDeployment regex: ^main$ is-release-branch: true increment: Patch other: + mode: ContinuousDelivery regex: .* increment: Patch ignore: From ee49527b6702c63d5269cee87ce55d63f4875c22 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Mon, 3 Nov 2025 18:33:06 +0000 Subject: [PATCH 23/34] additional code coverage --- .github/workflows/build.yml | 3 +- coverage-by-samples.ps1 | 11 ++++++ coverage-by-tests.ps1 | 11 ++++++ coverage-report.ps1 => coverage.ps1 | 3 +- .../CanRunAsyncVoidSteps.cs | 18 ++++++++++ .../Concurrency/TestCollectionName.cs | 1 + .../Configuration/ExceptionResolverTests.cs | 26 ++++++++++++++ src/TestStack.BDDfy.Tests/HumanizerTests.cs | 9 +++++ ...sts.ShouldProduceExpectedHtml.approved.txt | 2 +- .../Html/ClassicReportBuilderTests.cs | 6 ++-- .../Reporters/Html/MetroReportBuilderTests.cs | 4 +-- .../Reporters/Html/TemporaryCulture.cs | 5 ++- .../MarkDown/MarkDownReportBuilderTests.cs | 6 ++-- .../Reporters/ReportApprover.cs | 9 ++--- .../TextReporter/TextReporterTests.cs | 35 +++++++++---------- .../Scanner/Examples/ExampleActionTests.cs | 8 ++--- .../Scanner/Examples/ExampleTableTests.cs | 8 ++--- .../Scanner/Examples/FluentWithExamples.cs | 8 ++--- ...edWithExamplesEndingInANumber.approved.txt | 2 +- .../Examples/FluentWithExamplesAtEnd.cs | 8 ++--- .../Examples/ReflectiveWithExamples.cs | 8 ++--- .../FluentScanner/PrependStepTypeTests.cs | 6 ++-- src/TestStack.BDDfy.Tests/TagsTests.cs | 8 ++--- src/TestStack.BDDfy.sln | 1 + src/TestStack.BDDfy/DefaultHumanizer.cs | 6 ++-- .../Processors/AsyncTestRunner.cs | 3 +- .../Processors/ExceptionResolver.cs | 11 ++++++ .../Processors/ScenarioExecutor.cs | 8 ++--- src/TestStack.BDDfy/Reporters/TextReporter.cs | 8 ++--- src/default.runsettings | 5 +++ 30 files changed, 161 insertions(+), 86 deletions(-) create mode 100644 coverage-by-samples.ps1 create mode 100644 coverage-by-tests.ps1 rename coverage-report.ps1 => coverage.ps1 (73%) create mode 100644 src/Samples/TestStack.BDDfy.Samples/CanRunAsyncVoidSteps.cs create mode 100644 src/TestStack.BDDfy.Tests/Configuration/ExceptionResolverTests.cs create mode 100644 src/TestStack.BDDfy/Processors/ExceptionResolver.cs create mode 100644 src/default.runsettings diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9db4cda..718c8ed5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,7 +73,8 @@ jobs: dotnet test --configuration Release --collect:"XPlat Code Coverage" - --results-directory ../coverage + --results-directory ../coverage ` + --settings ./default.runsettings - name: Generate coverage report uses: danielpalme/ReportGenerator-GitHub-Action@5.4.16 diff --git a/coverage-by-samples.ps1 b/coverage-by-samples.ps1 new file mode 100644 index 00000000..f44e1708 --- /dev/null +++ b/coverage-by-samples.ps1 @@ -0,0 +1,11 @@ +dotnet test ./src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj ` + --collect:"XPlat Code Coverage" ` + --results-directory ./TestResults ` + --settings ./src/default.runsettings + +reportgenerator ` + -reports:./TestResults/**/coverage.cobertura.xml ` + -targetdir:./CoverageReport ` + -reporttypes:Html + +Remove-Item -Recurse -Force ./TestResults \ No newline at end of file diff --git a/coverage-by-tests.ps1 b/coverage-by-tests.ps1 new file mode 100644 index 00000000..e85d5754 --- /dev/null +++ b/coverage-by-tests.ps1 @@ -0,0 +1,11 @@ +dotnet test ./src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj ` + --collect:"XPlat Code Coverage" ` + --results-directory ./TestResults ` + --settings ./src/default.runsettings + +reportgenerator ` + -reports:./TestResults/**/coverage.cobertura.xml ` + -targetdir:./CoverageReport ` + -reporttypes:Html + +Remove-Item -Recurse -Force ./TestResults \ No newline at end of file diff --git a/coverage-report.ps1 b/coverage.ps1 similarity index 73% rename from coverage-report.ps1 rename to coverage.ps1 index 9c7b651c..dce21895 100644 --- a/coverage-report.ps1 +++ b/coverage.ps1 @@ -1,6 +1,7 @@ dotnet test ./src ` --collect:"XPlat Code Coverage" ` - --results-directory ./TestResults + --results-directory ./TestResults ` + --settings ./src/default.runsettings reportgenerator ` -reports:./TestResults/**/coverage.cobertura.xml ` diff --git a/src/Samples/TestStack.BDDfy.Samples/CanRunAsyncVoidSteps.cs b/src/Samples/TestStack.BDDfy.Samples/CanRunAsyncVoidSteps.cs new file mode 100644 index 00000000..36afc1e5 --- /dev/null +++ b/src/Samples/TestStack.BDDfy.Samples/CanRunAsyncVoidSteps.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using TestStack.BDDfy.Configuration; +using Xunit; + +namespace TestStack.BDDfy.Samples +{ + public class CanRunAsyncVoidSteps + { + [Fact] + internal void Run() => this.BDDfy(); + internal void SetUp() => Configurator.AsyncVoidSupportEnabled = false; + internal void TearDown() => Configurator.AsyncVoidSupportEnabled = true; + + internal async void GivenNonAsyncStep() => await Task.CompletedTask; + internal async void WhenSomethingHappens() => await Task.CompletedTask; + internal async void ThenAssertSomething() => await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs b/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs index 156c5e85..2ff6aa4e 100644 --- a/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs +++ b/src/TestStack.BDDfy.Tests/Concurrency/TestCollectionName.cs @@ -2,5 +2,6 @@ { internal static class TestCollectionName { public const string ModifiesConfigurator = "ModifiesConfigurator"; + public const string Approvals = "Approvals"; } } diff --git a/src/TestStack.BDDfy.Tests/Configuration/ExceptionResolverTests.cs b/src/TestStack.BDDfy.Tests/Configuration/ExceptionResolverTests.cs new file mode 100644 index 00000000..9f1f8988 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Configuration/ExceptionResolverTests.cs @@ -0,0 +1,26 @@ +using System; +using System.Reflection; +using Shouldly; +using TestStack.BDDfy.Processors; +using Xunit; + +namespace TestStack.BDDfy.Tests.Configuration; + +public class ExceptionResolverTests +{ + [Fact] + public void WhenTargetInvocationException_ResolveRoot_WhenInnerNotAvailable() + { + var ex = new TargetInvocationException(null); + var resolved = ExceptionResolver.Resolve(ex); + resolved.ShouldBeOfType(); + } + + [Fact] + public void WhenNotTargetInvocationException_ResolveRoot_EvenWhenInnerIsAvailable() + { + var ex = new System.InvalidCastException("error", new AmbiguousMatchException()); + var resolved = ExceptionResolver.Resolve(ex); + resolved.ShouldBeOfType(); + } +} diff --git a/src/TestStack.BDDfy.Tests/HumanizerTests.cs b/src/TestStack.BDDfy.Tests/HumanizerTests.cs index e5d1f9ff..079666a9 100644 --- a/src/TestStack.BDDfy.Tests/HumanizerTests.cs +++ b/src/TestStack.BDDfy.Tests/HumanizerTests.cs @@ -88,5 +88,14 @@ public void ReportsIllegalExampleStepNames(string stepName, string expectedStepT exception.ShouldNotBeNull(); exception.ShouldBeOfType(); } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + internal void HumanizeWithNullOrEmptyInput_ReturnsTheSame(string providedInput) + { + Humanizer.Humanize(providedInput).ShouldBe(providedInput); + } } } \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/Reporters/Html/ClassicReportBuilderTests.ShouldProduceExpectedHtml.approved.txt b/src/TestStack.BDDfy.Tests/Reporters/Html/ClassicReportBuilderTests.ShouldProduceExpectedHtml.approved.txt index 436429e4..02482b18 100644 --- a/src/TestStack.BDDfy.Tests/Reporters/Html/ClassicReportBuilderTests.ShouldProduceExpectedHtml.approved.txt +++ b/src/TestStack.BDDfy.Tests/Reporters/Html/ClassicReportBuilderTests.ShouldProduceExpectedHtml.approved.txt @@ -159,7 +159,7 @@ body{margin:0;padding:0;padding-bottom:40px;max-width:100%;background-color:#fff - +