From 84a6d5335d74ab3eff7d825ad1437ad6728b8197 Mon Sep 17 00:00:00 2001
From: martintmk <103487740+martintmk@users.noreply.github.com>
Date: Tue, 11 Jul 2023 16:27:29 +0200
Subject: [PATCH 1/2] Introduce `Polly.Testing` package (#1394)
---
.github/workflows/build.yml | 2 +-
Polly.sln | 14 +++
build.cake | 2 +
src/Polly.Core/Polly.Core.csproj | 1 +
src/Polly.Core/ResilienceStrategy.cs | 2 +
.../ResilienceStrategyBuilderBase.cs | 10 +-
.../InnerStrategiesDescriptor.cs | 9 ++
src/Polly.Testing/Polly.Testing.csproj | 17 +++
src/Polly.Testing/README.md | 28 +++++
.../ResilienceStrategyDescriptor.cs | 10 ++
.../ResilienceStrategyExtensions.cs | 68 ++++++++++++
.../Polly.Testing.Tests.csproj | 17 +++
.../ResilienceStrategyExtensionsTests.cs | 102 ++++++++++++++++++
13 files changed, 277 insertions(+), 5 deletions(-)
create mode 100644 src/Polly.Testing/InnerStrategiesDescriptor.cs
create mode 100644 src/Polly.Testing/Polly.Testing.csproj
create mode 100644 src/Polly.Testing/README.md
create mode 100644 src/Polly.Testing/ResilienceStrategyDescriptor.cs
create mode 100644 src/Polly.Testing/ResilienceStrategyExtensions.cs
create mode 100644 test/Polly.Testing.Tests/Polly.Testing.Tests.csproj
create mode 100644 test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3cd86dbe6ae..6310531a85f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -71,7 +71,7 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
- files: ./artifacts/coverage-reports/Polly.Core.Tests/Cobertura.xml,./artifacts/coverage-reports/Polly.Specs/Cobertura.xml,./artifacts/coverage-reports/Polly.RateLimiting.Tests/Cobertura.xml,./artifacts/coverage-reports/Polly.Extensions.Tests/Cobertura.xml,
+ files: ./artifacts/coverage-reports/Polly.Core.Tests/Cobertura.xml,./artifacts/coverage-reports/Polly.Specs/Cobertura.xml,./artifacts/coverage-reports/Polly.RateLimiting.Tests/Cobertura.xml,./artifacts/coverage-reports/Polly.Extensions.Tests/Cobertura.xml,./artifacts/coverage-reports/Polly.Testing.Tests/Cobertura.xml,
flags: ${{ matrix.os_name }}
- name: Upload Mutation Report
diff --git a/Polly.sln b/Polly.sln
index dde4741a533..97851b37fda 100644
--- a/Polly.sln
+++ b/Polly.sln
@@ -52,6 +52,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A6CC41B9-E
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B7BF406B-B06F-4025-83E6-7219C53196A6}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing", "src\Polly.Testing\Polly.Testing.csproj", "{9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing.Tests", "test\Polly.Testing.Tests\Polly.Testing.Tests.csproj", "{D333B5CE-982D-4C11-BDAF-4217AA02306E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +106,14 @@ Global
{C04DEE61-C1EA-4028-B457-CDBD304B8ED9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C04DEE61-C1EA-4028-B457-CDBD304B8ED9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C04DEE61-C1EA-4028-B457-CDBD304B8ED9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -119,6 +131,8 @@ Global
{BCA09595-A4D3-4D74-AC80-3E7017E51B24} = {B7BF406B-B06F-4025-83E6-7219C53196A6}
{06070F42-6738-4D0B-8D7E-9400B4030193} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB}
{C04DEE61-C1EA-4028-B457-CDBD304B8ED9} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB}
+ {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9} = {B7BF406B-B06F-4025-83E6-7219C53196A6}
+ {D333B5CE-982D-4C11-BDAF-4217AA02306E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E5D54CD-770A-4345-B585-1848FC2EA6F4}
diff --git a/build.cake b/build.cake
index 48abc0bf9a9..c029e84de30 100644
--- a/build.cake
+++ b/build.cake
@@ -170,6 +170,7 @@ Task("__RunMutationTests")
TestProject(File("../src/Polly.Core/Polly.Core.csproj"), File("./Polly.Core.Tests/Polly.Core.Tests.csproj"), "Polly.Core.csproj");
TestProject(File("../src/Polly.RateLimiting/Polly.RateLimiting.csproj"), File("./Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj"), "Polly.RateLimiting.csproj");
TestProject(File("../src/Polly.Extensions/Polly.Extensions.csproj"), File("./Polly.Extensions.Tests/Polly.Extensions.Tests.csproj"), "Polly.Extensions.csproj");
+ TestProject(File("../src/Polly.Testing/Polly.Testing.csproj"), File("./Polly.Testing.Tests/Polly.Testing.Tests.csproj"), "Polly.Testing.csproj");
TestProject(File("../src/Polly/Polly.csproj"), File("./Polly.Specs/Polly.Specs.csproj"), "Polly.csproj");
context.Environment.WorkingDirectory = oldDirectory;
@@ -221,6 +222,7 @@ Task("__CreateNuGetPackages")
System.IO.Path.Combine(srcDir, "Polly", "Polly.csproj"),
System.IO.Path.Combine(srcDir, "Polly.RateLimiting", "Polly.RateLimiting.csproj"),
System.IO.Path.Combine(srcDir, "Polly.Extensions", "Polly.Extensions.csproj"),
+ System.IO.Path.Combine(srcDir, "Polly.Testing", "Polly.Testing.csproj"),
};
Information("Building NuGet packages");
diff --git a/src/Polly.Core/Polly.Core.csproj b/src/Polly.Core/Polly.Core.csproj
index c9747fcc2c7..c4a3fbde39c 100644
--- a/src/Polly.Core/Polly.Core.csproj
+++ b/src/Polly.Core/Polly.Core.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Polly.Core/ResilienceStrategy.cs b/src/Polly.Core/ResilienceStrategy.cs
index 6ab5449f124..717269aaa15 100644
--- a/src/Polly.Core/ResilienceStrategy.cs
+++ b/src/Polly.Core/ResilienceStrategy.cs
@@ -11,6 +11,8 @@ namespace Polly;
///
public abstract partial class ResilienceStrategy
{
+ internal ResilienceStrategyOptions? Options { get; set; }
+
///
/// Executes the specified callback.
///
diff --git a/src/Polly.Core/ResilienceStrategyBuilderBase.cs b/src/Polly.Core/ResilienceStrategyBuilderBase.cs
index c7083f16034..4aeb59ed874 100644
--- a/src/Polly.Core/ResilienceStrategyBuilderBase.cs
+++ b/src/Polly.Core/ResilienceStrategyBuilderBase.cs
@@ -137,15 +137,17 @@ private ResilienceStrategy CreateResilienceStrategy(Entry entry)
builderName: BuilderName,
builderInstanceName: InstanceName,
builderProperties: Properties,
- strategyName: entry.Properties.StrategyName,
- strategyType: entry.Properties.StrategyType,
+ strategyName: entry.Options.StrategyName,
+ strategyType: entry.Options.StrategyType,
timeProvider: TimeProvider,
isGenericBuilder: IsGenericBuilder,
diagnosticSource: DiagnosticSource,
randomizer: Randomizer);
- return entry.Factory(context);
+ var strategy = entry.Factory(context);
+ strategy.Options = entry.Options;
+ return strategy;
}
- private sealed record Entry(Func Factory, ResilienceStrategyOptions Properties);
+ private sealed record Entry(Func Factory, ResilienceStrategyOptions Options);
}
diff --git a/src/Polly.Testing/InnerStrategiesDescriptor.cs b/src/Polly.Testing/InnerStrategiesDescriptor.cs
new file mode 100644
index 00000000000..14f171a73a1
--- /dev/null
+++ b/src/Polly.Testing/InnerStrategiesDescriptor.cs
@@ -0,0 +1,9 @@
+namespace Polly.Testing;
+
+///
+/// Describes the pipeline of a resilience strategy.
+///
+/// The strategies the pipeline is composed of.
+/// Gets a value indicating whether the pipeline has telemetry enabled.
+/// Gets a value indicating whether the resilience strategy is reloadable.
+public record class InnerStrategiesDescriptor(IReadOnlyList Strategies, bool HasTelemetry, bool IsReloadable);
diff --git a/src/Polly.Testing/Polly.Testing.csproj b/src/Polly.Testing/Polly.Testing.csproj
new file mode 100644
index 00000000000..d85ec8fba23
--- /dev/null
+++ b/src/Polly.Testing/Polly.Testing.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ Polly.Testing
+ Polly.Testing
+ enable
+ true
+ Library
+ 100
+
+
+
+
+
+
+
diff --git a/src/Polly.Testing/README.md b/src/Polly.Testing/README.md
new file mode 100644
index 00000000000..fa1b42f68d0
--- /dev/null
+++ b/src/Polly.Testing/README.md
@@ -0,0 +1,28 @@
+# About Polly.Testing
+
+This package exposes APIs and utilities that can be used to assert on the composition of resilience strategies.
+
+``` csharp
+// Build your resilience strategy.
+ResilienceStrategy strategy = new ResilienceStrategyBuilder()
+ .AddRetry(new RetryStrategyOptions
+ {
+ RetryCount = 4
+ })
+ .AddTimeout(TimeSpan.FromSeconds(1))
+ .ConfigureTelemetry(NullLoggerFactory.Instance)
+ .Build();
+
+// Retrieve inner strategies.
+InnerStrategiesDescriptor descriptor = strategy.GetInnerStrategies();
+
+// Assert the composition.
+Assert.True(descriptor.HasTelemetry);
+Assert.Equal(2, descriptor.Strategies.Count);
+
+var retryOptions = Assert.IsType(descriptor.Strategies[0]);
+Assert.Equal(4, retryOptions.RetryCount);
+
+var timeoutOptions = Assert.IsType(descriptor.Strategies[0]);
+Assert.Equal(TimeSpan.FromSeconds(1), timeoutOptions.Timeout);
+```
diff --git a/src/Polly.Testing/ResilienceStrategyDescriptor.cs b/src/Polly.Testing/ResilienceStrategyDescriptor.cs
new file mode 100644
index 00000000000..04005d3249e
--- /dev/null
+++ b/src/Polly.Testing/ResilienceStrategyDescriptor.cs
@@ -0,0 +1,10 @@
+namespace Polly.Testing;
+
+///
+/// This class provides additional information about a .
+///
+/// The options used by the resilience strategy, if any.
+/// The type of the strategy.
+public record ResilienceStrategyDescriptor(ResilienceStrategyOptions? Options, Type StrategyType)
+{
+}
diff --git a/src/Polly.Testing/ResilienceStrategyExtensions.cs b/src/Polly.Testing/ResilienceStrategyExtensions.cs
new file mode 100644
index 00000000000..bf8ca6a1c8d
--- /dev/null
+++ b/src/Polly.Testing/ResilienceStrategyExtensions.cs
@@ -0,0 +1,68 @@
+using Polly.Utils;
+
+namespace Polly.Testing;
+
+///
+/// The test-related extensions for and .
+///
+public static class ResilienceStrategyExtensions
+{
+ private const string TelemetryResilienceStrategy = "Polly.Extensions.Telemetry.TelemetryResilienceStrategy";
+
+ ///
+ /// Gets the inner strategies the is composed of.
+ ///
+ /// The type of result.
+ /// The strategy instance.
+ /// A list of inner strategies.
+ /// Thrown when is .
+ public static InnerStrategiesDescriptor GetInnerStrategies(this ResilienceStrategy strategy)
+ {
+ Guard.NotNull(strategy);
+
+ return strategy.Strategy.GetInnerStrategies();
+ }
+
+ ///
+ /// Gets the inner strategies the is composed of.
+ ///
+ /// The strategy instance.
+ /// A list of inner strategies.
+ /// Thrown when is .
+ public static InnerStrategiesDescriptor GetInnerStrategies(this ResilienceStrategy strategy)
+ {
+ Guard.NotNull(strategy);
+
+ var strategies = new List();
+ strategy.ExpandStrategies(strategies);
+
+ var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, s.GetType())).ToList();
+
+ return new InnerStrategiesDescriptor(
+ innerStrategies.Where(s => !ShouldSkip(s.StrategyType)).ToList().AsReadOnly(),
+ HasTelemetry: innerStrategies.Exists(s => s.StrategyType.FullName == TelemetryResilienceStrategy),
+ IsReloadable: innerStrategies.Exists(s => s.StrategyType == typeof(ReloadableResilienceStrategy)));
+ }
+
+ private static bool ShouldSkip(Type type) => type == typeof(ReloadableResilienceStrategy) || type.FullName == TelemetryResilienceStrategy;
+
+ private static void ExpandStrategies(this ResilienceStrategy strategy, List strategies)
+ {
+ if (strategy is ResilienceStrategyPipeline pipeline)
+ {
+ foreach (var inner in pipeline.Strategies)
+ {
+ inner.ExpandStrategies(strategies);
+ }
+ }
+ else if (strategy is ReloadableResilienceStrategy reloadable)
+ {
+ strategies.Add(reloadable);
+ ExpandStrategies(reloadable.Strategy, strategies);
+ }
+ else
+ {
+ strategies.Add(strategy);
+ }
+ }
+}
diff --git a/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj b/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj
new file mode 100644
index 00000000000..2469343b97e
--- /dev/null
+++ b/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+ net7.0;net6.0
+ $(TargetFrameworks);net481
+ Test
+ enable
+ 100
+ $(NoWarn);SA1600;SA1204
+ [Polly.Testing]*
+
+
+
+
+
+
+
+
diff --git a/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs
new file mode 100644
index 00000000000..f4ba0477b70
--- /dev/null
+++ b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs
@@ -0,0 +1,102 @@
+using System;
+using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using Polly.CircuitBreaker;
+using Polly.Fallback;
+using Polly.Hedging;
+using Polly.RateLimiting;
+using Polly.Registry;
+using Polly.Retry;
+using Polly.Timeout;
+
+namespace Polly.Testing.Tests;
+
+public class ResilienceStrategyExtensionsTests
+{
+ [Fact]
+ public void GetInnerStrategies_Ok()
+ {
+ // arrange
+ var strategy = new ResilienceStrategyBuilder()
+ .AddFallback(new()
+ {
+ FallbackAction = _ => Outcome.FromResultAsTask("dummy"),
+ })
+ .AddRetry(new())
+ .AddAdvancedCircuitBreaker(new())
+ .AddTimeout(TimeSpan.FromSeconds(1))
+ .AddHedging(new())
+ .AddConcurrencyLimiter(10)
+ .AddStrategy(new CustomStrategy())
+ .ConfigureTelemetry(NullLoggerFactory.Instance)
+ .Build();
+
+ // act
+ var descriptor = strategy.GetInnerStrategies();
+
+ // assert
+ descriptor.HasTelemetry.Should().BeTrue();
+ descriptor.IsReloadable.Should().BeFalse();
+ descriptor.Strategies.Should().HaveCount(7);
+ descriptor.Strategies[0].Options.Should().BeOfType>();
+ descriptor.Strategies[1].Options.Should().BeOfType>();
+ descriptor.Strategies[2].Options.Should().BeOfType>();
+ descriptor.Strategies[3].Options.Should().BeOfType();
+ descriptor.Strategies[3].Options
+ .Should()
+ .BeOfType().Subject.Timeout
+ .Should().Be(TimeSpan.FromSeconds(1));
+
+ descriptor.Strategies[4].Options.Should().BeOfType>();
+ descriptor.Strategies[5].Options.Should().BeOfType();
+ descriptor.Strategies[6].StrategyType.Should().Be(typeof(CustomStrategy));
+ }
+
+ [Fact]
+ public void GetInnerStrategies_SingleStrategy_Ok()
+ {
+ // arrange
+ var strategy = new ResilienceStrategyBuilder()
+ .AddTimeout(TimeSpan.FromSeconds(1))
+ .Build();
+
+ // act
+ var descriptor = strategy.GetInnerStrategies();
+
+ // assert
+ descriptor.HasTelemetry.Should().BeFalse();
+ descriptor.IsReloadable.Should().BeFalse();
+ descriptor.Strategies.Should().HaveCount(1);
+ descriptor.Strategies[0].Options.Should().BeOfType();
+ }
+
+ [Fact]
+ public void GetInnerStrategies_Reloadable_Ok()
+ {
+ // arrange
+ var strategy = new ResilienceStrategyRegistry().GetOrAddStrategy("dummy", (builder, context) =>
+ {
+ context.EnableReloads(() => () => CancellationToken.None);
+
+ builder
+ .AddConcurrencyLimiter(10)
+ .AddStrategy(new CustomStrategy());
+ });
+
+ // act
+ var descriptor = strategy.GetInnerStrategies();
+
+ // assert
+ descriptor.HasTelemetry.Should().BeFalse();
+ descriptor.IsReloadable.Should().BeTrue();
+ descriptor.Strategies.Should().HaveCount(2);
+ descriptor.Strategies[0].Options.Should().BeOfType();
+ descriptor.Strategies[1].StrategyType.Should().Be(typeof(CustomStrategy));
+ }
+
+ private sealed class CustomStrategy : ResilienceStrategy
+ {
+ protected override ValueTask> ExecuteCoreAsync(Func>> callback, ResilienceContext context, TState state)
+ => throw new NotSupportedException();
+ }
+}
From cebe1450bb00db545aa93654f4354877ed091fa9 Mon Sep 17 00:00:00 2001
From: martintmk <103487740+martintmk@users.noreply.github.com>
Date: Tue, 11 Jul 2023 16:32:05 +0200
Subject: [PATCH 2/2] Kill mutant (#1395)
---
src/Polly.Core/Retry/RetryResilienceStrategy.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs
index 9a6da5108ed..d8702c1df83 100644
--- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs
+++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs
@@ -87,6 +87,7 @@ protected override async ValueTask> ExecuteCallbackAsync(Func
}
// stryker disable once equality : no means to test this
+ // stryker disable once boolean : no means to test this
if (delay > TimeSpan.Zero)
{
try