-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial project added by copying xRetry.SpecFlow, renaming & replacing strings, and doing the bare-minimum to make it build. No Docs (except the generated readme copied from xRetry.SpecFlow) No Tests
- Loading branch information
1 parent
d08ce32
commit 0c039b6
Showing
13 changed files
with
433 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace xRetry.Reqnroll | ||
{ | ||
internal static class Constants | ||
{ | ||
public const string RETRY_TAG = "retry"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using Reqnroll.Generator.Plugins; | ||
using Reqnroll.Generator.UnitTestProvider; | ||
using Reqnroll.Infrastructure; | ||
using Reqnroll.UnitTestProvider; | ||
using xRetry.Reqnroll; | ||
using xRetry.Reqnroll.Parsers; | ||
|
||
[assembly: GeneratorPlugin(typeof(GeneratorPlugin))] | ||
namespace xRetry.Reqnroll | ||
{ | ||
public class GeneratorPlugin : IGeneratorPlugin | ||
{ | ||
public void Initialize(GeneratorPluginEvents generatorPluginEvents, GeneratorPluginParameters generatorPluginParameters, | ||
UnitTestProviderConfiguration unitTestProviderConfiguration) | ||
{ | ||
generatorPluginEvents.CustomizeDependencies += customiseDependencies; | ||
} | ||
|
||
private void customiseDependencies(object sender, CustomizeDependenciesEventArgs eventArgs) | ||
{ | ||
eventArgs.ObjectContainer.RegisterTypeAs<RetryTagParser, IRetryTagParser>(); | ||
eventArgs.ObjectContainer.RegisterTypeAs<TestGeneratorProvider, IUnitTestGeneratorProvider>(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace xRetry.Reqnroll.Parsers | ||
{ | ||
public interface IRetryTagParser | ||
{ | ||
RetryTag Parse(string tag); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using System; | ||
|
||
namespace xRetry.Reqnroll.Parsers | ||
{ | ||
public class RetryTag : IEquatable<RetryTag> | ||
{ | ||
public readonly int? MaxRetries; | ||
public readonly int? DelayBetweenRetriesMs; | ||
|
||
public RetryTag(int? maxRetries, int? delayBetweenRetriesMs) | ||
{ | ||
MaxRetries = maxRetries; | ||
DelayBetweenRetriesMs = delayBetweenRetriesMs; | ||
} | ||
|
||
public bool Equals(RetryTag other) | ||
{ | ||
if (ReferenceEquals(null, other)) return false; | ||
if (ReferenceEquals(this, other)) return true; | ||
return MaxRetries == other.MaxRetries && DelayBetweenRetriesMs == other.DelayBetweenRetriesMs; | ||
} | ||
|
||
public override bool Equals(object obj) | ||
{ | ||
if (ReferenceEquals(null, obj)) return false; | ||
if (ReferenceEquals(this, obj)) return true; | ||
if (obj.GetType() != this.GetType()) return false; | ||
return Equals((RetryTag) obj); | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
unchecked | ||
{ | ||
return (MaxRetries.GetHashCode() * 397) ^ DelayBetweenRetriesMs.GetHashCode(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace xRetry.Reqnroll.Parsers | ||
{ | ||
public class RetryTagParser : IRetryTagParser | ||
{ | ||
// unescaped: ^retry(\(([0-9]+)(,([0-9]+))?\))?$ | ||
private readonly Regex regex = new Regex($"^{Constants.RETRY_TAG}(\\(([0-9]+)(,([0-9]+))?\\))?$", | ||
RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
|
||
public RetryTag Parse(string tag) | ||
{ | ||
if (tag == null) | ||
{ | ||
throw new ArgumentNullException(nameof(tag)); | ||
} | ||
|
||
int? maxRetries = null; | ||
int? delayBetweenRetriesMs = null; | ||
|
||
Match match = regex.Match(tag); | ||
if (match.Success) | ||
{ | ||
// Group 2 is max retries | ||
if (match.Groups[2].Success) | ||
{ | ||
maxRetries = int.Parse(match.Groups[2].Value); | ||
|
||
// Group 4 is delay between retries | ||
if (match.Groups[4].Success) | ||
{ | ||
delayBetweenRetriesMs = int.Parse(match.Groups[4].Value); | ||
} | ||
} | ||
} | ||
|
||
return new RetryTag(maxRetries, delayBetweenRetriesMs); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
[//]: # (This file is auto-generated, do not modify it directly. Instead, update the files under docs/) | ||
|
||
|
||
[//]: \# (Src: xRetry.Reqnroll/header.md) | ||
|
||
# xRetry.Reqnroll | ||
Retry flickering test cases for Reqnroll when using xUnit. | ||
|
||
[//]: \# (Src: ciBadge.md) | ||
|
||
[![pipeline status](https://github.com/JoshKeegan/xRetry/actions/workflows/cicd.yaml/badge.svg)](https://github.com/JoshKeegan/xRetry/actions) | ||
|
||
|
||
[//]: \# (Src: whenToUse.md) | ||
|
||
## When to use this | ||
This is intended for use on flickering tests, where the reason for failure is an external | ||
dependency and the failure is transient, e.g: | ||
- HTTP request over the network | ||
- Database call that could deadlock, timeout etc... | ||
|
||
Whenever a test includes real-world infrastructure, particularly when communicated with via the | ||
internet, there is a risk of the test randomly failing so we want to try and run it again. | ||
This is the intended use case of the library. | ||
|
||
If you have a test that covers some flaky code, where sporadic failures are caused by a bug, | ||
this library should **not** be used to cover it up! | ||
|
||
[//]: \# (Src: xRetry.Reqnroll/usage.md) | ||
|
||
## Usage: Reqnroll 3 | ||
|
||
Add the [`xRetry.Reqnroll` NuGet package](https://www.nuget.org/packages/xRetry.Reqnroll "xRetry NuGet.Reqnroll package") to your project. | ||
|
||
### Scenarios (and outlines) | ||
|
||
Above any scenario or scenario outline that should be retried, add a `@retry` tag, e.g: | ||
|
||
```gherkin | ||
@retry | ||
Scenario: Retry three times by default | ||
When I increment the default retry count | ||
Then the default result should be 3 | ||
``` | ||
|
||
This will attempt to run the test until it passes, up to 3 times by default. | ||
You can optionally specify a number of times to attempt to run the test in brackets, e.g. `@retry(5)`. | ||
|
||
You can also optionally specify a delay between each retry (in milliseconds) as a second | ||
parameter, e.g. `@retry(5,100)` will run your test up to 5 times, waiting 100ms between each attempt. | ||
Note that you must not include a space between the parameters, as Cucumber/Reqnroll uses | ||
a space to separate tags, i.e. `@retry(5, 100)` would not work due to the space after the comma. | ||
|
||
### Features | ||
|
||
You can also make every test in a feature retryable by adding the `@retry` tag to the feature, e.g: | ||
|
||
```gherkin | ||
@retry | ||
Feature: Retryable Feature | ||
Scenario: Retry scenario three times by default | ||
When I increment the retry count | ||
Then the result should be 3 | ||
``` | ||
|
||
All options that can be used against an individual scenario can also be applied like this at the feature level. | ||
If a `@retry` tag exists on both the feature and a scenario within that feature, the tag on the scenario will take | ||
precedent over the one on the feature. This is useful if you wanted all scenarios in a feature to be retried | ||
by default but for some cases also wanted to wait some time before each retry attempt. You can also use this to prevent a specific scenario not be retried, even though it is within a feature with a `@retry` tag, by adding `@retry(1)` to the scenario. | ||
|
||
|
||
[//]: \# (Src: logs.md) | ||
|
||
## Viewing retry logs | ||
By default, you won't see whether your tests are being retried as we make this information available | ||
via the xunit diagnostic logs but test runners will hide these detailed logs by default. | ||
To enable them you must configure your xUnit test project to have `diagnosticMessages` set to `true` in the `xunit.runner.json`. | ||
See the [xUnit docs](https://xunit.net/docs/configuration-files) for a full setup guide of their config file, or see | ||
this projects own unit tests which has been set up with this enabled. | ||
|
||
[//]: \# (Src: issues.md) | ||
|
||
## Issues | ||
If you find a bug whilst using this library, please report it [on GitHub](https://github.com/JoshKeegan/xRetry/issues). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System; | ||
|
||
// ReSharper disable once CheckNamespace | ||
// ReSharper disable once IdentifierTypo | ||
namespace Xunit | ||
{ | ||
/// <summary> | ||
/// Do not use. | ||
/// Exists purely as a marker to replicate the exception thrown by Xunit.SkippableFact that Reqnroll.xUnit | ||
/// makes use of. That way we can intercept the exception that is throwing without also having our own runtime | ||
/// plugin, or adding a direct dependency on either of these other libraries. | ||
/// </summary> | ||
internal class SkipException : Exception { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
using System; | ||
using System.CodeDom; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Reqnroll.Generator; | ||
using Reqnroll.Generator.CodeDom; | ||
using Reqnroll.Generator.UnitTestProvider; | ||
using xRetry.Reqnroll.Parsers; | ||
|
||
namespace xRetry.Reqnroll | ||
{ | ||
public class TestGeneratorProvider : XUnit2TestGeneratorProvider | ||
{ | ||
private const string IGNORE_TAG = "ignore"; | ||
private const string RETRY_FACT_ATTRIBUTE = "xRetry.RetryFact"; | ||
private const string RETRY_THEORY_ATTRIBUTE = "xRetry.RetryTheory"; | ||
|
||
private readonly IRetryTagParser retryTagParser; | ||
|
||
public TestGeneratorProvider(CodeDomHelper codeDomHelper, IRetryTagParser retryTagParser) | ||
: base(codeDomHelper) | ||
{ | ||
this.retryTagParser = retryTagParser; | ||
} | ||
|
||
// Called for scenario outlines, even when it has no tags. | ||
// We don't yet have access to tags against the scenario at this point, but can handle feature tags now. | ||
public override void SetRowTest(TestClassGenerationContext generationContext, CodeMemberMethod testMethod, string scenarioTitle) | ||
{ | ||
base.SetRowTest(generationContext, testMethod, scenarioTitle); | ||
|
||
string[] featureTags = generationContext.Feature.Tags.Select(t => stripLeadingAtSign(t.Name)).ToArray(); | ||
|
||
applyRetry(featureTags, Enumerable.Empty<string>(), testMethod); | ||
} | ||
|
||
// Called for scenarios, even when it has no tags. | ||
// We don't yet have access to tags against the scenario at this point, but can handle feature tags now. | ||
public override void SetTestMethod(TestClassGenerationContext generationContext, CodeMemberMethod testMethod, string friendlyTestName) | ||
{ | ||
base.SetTestMethod(generationContext, testMethod, friendlyTestName); | ||
|
||
string[] featureTags = generationContext.Feature.Tags.Select(t => stripLeadingAtSign(t.Name)).ToArray(); | ||
|
||
applyRetry(featureTags, Enumerable.Empty<string>(), testMethod); | ||
} | ||
|
||
// Called for both scenarios & scenario outlines, but only if it has tags | ||
public override void SetTestMethodCategories(TestClassGenerationContext generationContext, | ||
CodeMemberMethod testMethod, IEnumerable<string> scenarioCategories) | ||
{ | ||
// Optimisation: Prevent multiple enumerations | ||
scenarioCategories = scenarioCategories as string[] ?? scenarioCategories.ToArray(); | ||
|
||
base.SetTestMethodCategories(generationContext, testMethod, scenarioCategories); | ||
|
||
// Feature tags will have already been processed in one of the methods above, which are executed before this | ||
IEnumerable<string> featureTags = generationContext.Feature.Tags.Select(t => stripLeadingAtSign(t.Name)); | ||
applyRetry((string[]) scenarioCategories, featureTags, testMethod); | ||
} | ||
|
||
/// <summary> | ||
/// Apply retry tags to the current test | ||
/// </summary> | ||
/// <param name="tags">Tags that haven't yet been processed. If the test has just been created these will be for the feature, otherwise for the scenario</param> | ||
/// <param name="processedTags">Tags that have already been processed. If the test has just been created this will be empty, otherwise they will be the feature tags</param> | ||
/// <param name="testMethod">Test method we are applying retries for</param> | ||
private void applyRetry(IList<string> tags, IEnumerable<string> processedTags, CodeMemberMethod testMethod) | ||
{ | ||
// Do not add retries to skipped tests (even if they have the retry attribute) as retrying won't affect the outcome. | ||
// This allows for the new (for Reqnroll 3.1.x) implementation that relies on Xunit.SkippableFact to still work, as it | ||
// too will replace the attribute for running the test with a custom one. | ||
if (tags.Any(isIgnoreTag) || processedTags.Any(isIgnoreTag)) | ||
{ | ||
return; | ||
} | ||
|
||
string strRetryTag = getRetryTag(tags); | ||
if (strRetryTag == null) | ||
{ | ||
return; | ||
} | ||
|
||
RetryTag retryTag = retryTagParser.Parse(strRetryTag); | ||
|
||
// Remove the original fact or theory attribute | ||
CodeAttributeDeclaration originalAttribute = testMethod.CustomAttributes.OfType<CodeAttributeDeclaration>() | ||
.FirstOrDefault(a => | ||
a.Name == FACT_ATTRIBUTE || | ||
a.Name == THEORY_ATTRIBUTE || | ||
a.Name == RETRY_FACT_ATTRIBUTE || | ||
a.Name == RETRY_THEORY_ATTRIBUTE); | ||
if (originalAttribute == null) | ||
{ | ||
return; | ||
} | ||
testMethod.CustomAttributes.Remove(originalAttribute); | ||
|
||
// Add the Retry attribute | ||
CodeAttributeDeclaration retryAttribute = CodeDomHelper.AddAttribute(testMethod, | ||
originalAttribute.Name == FACT_ATTRIBUTE || originalAttribute.Name == RETRY_FACT_ATTRIBUTE | ||
? RETRY_FACT_ATTRIBUTE | ||
: RETRY_THEORY_ATTRIBUTE); | ||
|
||
retryAttribute.Arguments.Add(new CodeAttributeArgument( | ||
new CodePrimitiveExpression(retryTag.MaxRetries ?? RetryFactAttribute.DEFAULT_MAX_RETRIES))); | ||
retryAttribute.Arguments.Add(new CodeAttributeArgument( | ||
new CodePrimitiveExpression(retryTag.DelayBetweenRetriesMs ?? | ||
RetryFactAttribute.DEFAULT_DELAY_BETWEEN_RETRIES_MS))); | ||
|
||
// Always skip on Xunit.SkipException (from Xunit.SkippableFact) which is used by Reqnroll.xUnit to implement | ||
// dynamic test skipping. This way we can intercept the exception that is already thrown without also having | ||
// our own runtime plugin. | ||
retryAttribute.Arguments.Add(new CodeAttributeArgument( | ||
new CodeArrayCreateExpression(new CodeTypeReference(typeof(Type)), | ||
new CodeExpression[] | ||
{ | ||
new CodeTypeOfExpression(typeof(Xunit.SkipException)) | ||
}))); | ||
|
||
// Copy arguments from the original attribute. If it's already a retry attribute, don't copy the retry arguments though | ||
for (int i = originalAttribute.Name == RETRY_FACT_ATTRIBUTE || | ||
originalAttribute.Name == RETRY_THEORY_ATTRIBUTE | ||
? retryAttribute.Arguments.Count | ||
: 0; | ||
i < originalAttribute.Arguments.Count; | ||
i++) | ||
{ | ||
retryAttribute.Arguments.Add(originalAttribute.Arguments[i]); | ||
} | ||
} | ||
|
||
private static string stripLeadingAtSign(string s) => s.StartsWith("@") ? s.Substring(1) : s; | ||
|
||
private static bool isIgnoreTag(string tag) => tag.Equals(IGNORE_TAG, StringComparison.OrdinalIgnoreCase); | ||
|
||
private static string getRetryTag(IEnumerable<string> tags) => | ||
tags.FirstOrDefault(t => | ||
t.StartsWith(Constants.RETRY_TAG, StringComparison.OrdinalIgnoreCase) && | ||
// Is just "retry", or is "retry("... for params | ||
(t.Length == Constants.RETRY_TAG.Length || t[Constants.RETRY_TAG.Length] == '(')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks> | ||
<AssemblyName>xRetry.ReqnrollPlugin</AssemblyName> | ||
|
||
<IsPackable>true</IsPackable> | ||
<NuspecProperties>version=$(Version)</NuspecProperties> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Reqnroll.CustomPlugin" Version="2.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\xRetry\xRetry.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.