Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into mtomka/fix-unstable-test
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Jul 12, 2023
2 parents e6d897a + cebe145 commit d7c84fd
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Polly.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/Polly.Core/Polly.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<Using Include="Polly.Utils" />
<InternalsVisibleToTest Include="Polly.Core.Tests" />
<InternalsVisibleToTest Include="Polly.Testing" />
<InternalsVisibleToTest Include="Polly.TestUtils" />
</ItemGroup>

Expand Down
2 changes: 2 additions & 0 deletions src/Polly.Core/ResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace Polly;
/// </remarks>
public abstract partial class ResilienceStrategy
{
internal ResilienceStrategyOptions? Options { get; set; }

/// <summary>
/// Executes the specified callback.
/// </summary>
Expand Down
10 changes: 6 additions & 4 deletions src/Polly.Core/ResilienceStrategyBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResilienceStrategyBuilderContext, ResilienceStrategy> Factory, ResilienceStrategyOptions Properties);
private sealed record Entry(Func<ResilienceStrategyBuilderContext, ResilienceStrategy> Factory, ResilienceStrategyOptions Options);
}
1 change: 1 addition & 0 deletions src/Polly.Core/Retry/RetryResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func
}

// stryker disable once equality : no means to test this
// stryker disable once boolean : no means to test this
if (delay > TimeSpan.Zero)
{
try
Expand Down
9 changes: 9 additions & 0 deletions src/Polly.Testing/InnerStrategiesDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Polly.Testing;

/// <summary>
/// Describes the pipeline of a resilience strategy.
/// </summary>
/// <param name="Strategies">The strategies the pipeline is composed of.</param>
/// <param name="HasTelemetry">Gets a value indicating whether the pipeline has telemetry enabled.</param>
/// <param name="IsReloadable">Gets a value indicating whether the resilience strategy is reloadable.</param>
public record class InnerStrategiesDescriptor(IReadOnlyList<ResilienceStrategyDescriptor> Strategies, bool HasTelemetry, bool IsReloadable);
17 changes: 17 additions & 0 deletions src/Polly.Testing/Polly.Testing.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyTitle>Polly.Testing</AssemblyTitle>
<RootNamespace>Polly.Testing</RootNamespace>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProjectType>Library</ProjectType>
<MutationScore>100</MutationScore>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Polly.Core\Polly.Core.csproj" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions src/Polly.Testing/README.md
Original file line number Diff line number Diff line change
@@ -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<RetryStrategyOptions>(descriptor.Strategies[0]);
Assert.Equal(4, retryOptions.RetryCount);

var timeoutOptions = Assert.IsType<TimeoutStrategyOptions>(descriptor.Strategies[0]);
Assert.Equal(TimeSpan.FromSeconds(1), timeoutOptions.Timeout);
```
10 changes: 10 additions & 0 deletions src/Polly.Testing/ResilienceStrategyDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Polly.Testing;

/// <summary>
/// This class provides additional information about a <see cref="ResilienceStrategy"/>.
/// </summary>
/// <param name="Options">The options used by the resilience strategy, if any.</param>
/// <param name="StrategyType">The type of the strategy.</param>
public record ResilienceStrategyDescriptor(ResilienceStrategyOptions? Options, Type StrategyType)
{
}
68 changes: 68 additions & 0 deletions src/Polly.Testing/ResilienceStrategyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Polly.Utils;

namespace Polly.Testing;

/// <summary>
/// The test-related extensions for <see cref="ResilienceStrategy"/> and <see cref="ResilienceStrategy{TResult}"/>.
/// </summary>
public static class ResilienceStrategyExtensions
{
private const string TelemetryResilienceStrategy = "Polly.Extensions.Telemetry.TelemetryResilienceStrategy";

/// <summary>
/// Gets the inner strategies the <paramref name="strategy"/> is composed of.
/// </summary>
/// <typeparam name="TResult">The type of result.</typeparam>
/// <param name="strategy">The strategy instance.</param>
/// <returns>A list of inner strategies.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="strategy"/> is <see langword="null"/>.</exception>
public static InnerStrategiesDescriptor GetInnerStrategies<TResult>(this ResilienceStrategy<TResult> strategy)
{
Guard.NotNull(strategy);

return strategy.Strategy.GetInnerStrategies();
}

/// <summary>
/// Gets the inner strategies the <paramref name="strategy"/> is composed of.
/// </summary>
/// <param name="strategy">The strategy instance.</param>
/// <returns>A list of inner strategies.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="strategy"/> is <see langword="null"/>.</exception>
public static InnerStrategiesDescriptor GetInnerStrategies(this ResilienceStrategy strategy)
{
Guard.NotNull(strategy);

var strategies = new List<ResilienceStrategy>();
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<ResilienceStrategy> 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);
}
}
}
17 changes: 17 additions & 0 deletions test/Polly.Testing.Tests/Polly.Testing.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net481</TargetFrameworks>
<ProjectType>Test</ProjectType>
<Nullable>enable</Nullable>
<Threshold>100</Threshold>
<NoWarn>$(NoWarn);SA1600;SA1204</NoWarn>
<Include>[Polly.Testing]*</Include>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Polly.Extensions\Polly.Extensions.csproj" />
<ProjectReference Include="..\..\src\Polly.RateLimiting\Polly.RateLimiting.csproj" />
<ProjectReference Include="..\..\src\Polly.Testing\Polly.Testing.csproj" />
</ItemGroup>
</Project>
102 changes: 102 additions & 0 deletions test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>()
.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<FallbackStrategyOptions<string>>();
descriptor.Strategies[1].Options.Should().BeOfType<RetryStrategyOptions<string>>();
descriptor.Strategies[2].Options.Should().BeOfType<AdvancedCircuitBreakerStrategyOptions<string>>();
descriptor.Strategies[3].Options.Should().BeOfType<TimeoutStrategyOptions>();
descriptor.Strategies[3].Options
.Should()
.BeOfType<TimeoutStrategyOptions>().Subject.Timeout
.Should().Be(TimeSpan.FromSeconds(1));

descriptor.Strategies[4].Options.Should().BeOfType<HedgingStrategyOptions<string>>();
descriptor.Strategies[5].Options.Should().BeOfType<RateLimiterStrategyOptions>();
descriptor.Strategies[6].StrategyType.Should().Be(typeof(CustomStrategy));
}

[Fact]
public void GetInnerStrategies_SingleStrategy_Ok()
{
// arrange
var strategy = new ResilienceStrategyBuilder<string>()
.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<TimeoutStrategyOptions>();
}

[Fact]
public void GetInnerStrategies_Reloadable_Ok()
{
// arrange
var strategy = new ResilienceStrategyRegistry<string>().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<RateLimiterStrategyOptions>();
descriptor.Strategies[1].StrategyType.Should().Be(typeof(CustomStrategy));
}

private sealed class CustomStrategy : ResilienceStrategy
{
protected override ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, TState>(Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, ResilienceContext context, TState state)
=> throw new NotSupportedException();
}
}

0 comments on commit d7c84fd

Please sign in to comment.