diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs index c8ac539822..322c1d1524 100644 --- a/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs +++ b/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs @@ -14,20 +14,19 @@ internal static class BehaviorPipelineBuilderExtensions /// /// The builder type. /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). /// The behavior to be injected. /// The same builder instance. /// Thrown when is . /// Thrown when the options produced from the arguments are invalid. - public static TBuilder AddChaosBehavior(this TBuilder builder, bool enabled, double injectionRate, Func behavior) + public static TBuilder AddChaosBehavior(this TBuilder builder, double injectionRate, Func behavior) where TBuilder : ResiliencePipelineBuilderBase { Guard.NotNull(builder); return builder.AddChaosBehavior(new BehaviorStrategyOptions { - Enabled = enabled, + Enabled = true, InjectionRate = injectionRate, BehaviorAction = (_) => behavior() }); diff --git a/src/Polly.Core/Simmy/Fault/FaultChaosStrategy.cs b/src/Polly.Core/Simmy/Fault/FaultChaosStrategy.cs new file mode 100644 index 0000000000..a394f05c23 --- /dev/null +++ b/src/Polly.Core/Simmy/Fault/FaultChaosStrategy.cs @@ -0,0 +1,60 @@ +using Polly.Telemetry; + +namespace Polly.Simmy.Fault; + +internal class FaultChaosStrategy : MonkeyStrategy +{ + private readonly ResilienceStrategyTelemetry _telemetry; + + public FaultChaosStrategy(FaultStrategyOptions options, ResilienceStrategyTelemetry telemetry) + : base(options) + { + if (options.Fault is null && options.FaultGenerator is null) + { + throw new InvalidOperationException("Either Fault or FaultGenerator is required."); + } + + _telemetry = telemetry; + Fault = options.Fault; + OnFaultInjected = options.OnFaultInjected; + FaultGenerator = options.FaultGenerator is not null ? options.FaultGenerator : (_) => new(options.Fault); + } + + public Func? OnFaultInjected { get; } + + public Func> FaultGenerator { get; } + + public Exception? Fault { get; } + + protected internal override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + var fault = await FaultGenerator(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + if (fault is not null) + { + var args = new OnFaultInjectedArguments(context, fault); + _telemetry.Report(new(ResilienceEventSeverity.Information, FaultConstants.OnFaultInjectedEvent), context, args); + + if (OnFaultInjected is not null) + { + await OnFaultInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return new Outcome(fault); + } + } + + return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + catch (OperationCanceledException e) + { + return new Outcome(e); + } + } +} diff --git a/src/Polly.Core/Simmy/Fault/FaultConstants.cs b/src/Polly.Core/Simmy/Fault/FaultConstants.cs new file mode 100644 index 0000000000..1e8f325af5 --- /dev/null +++ b/src/Polly.Core/Simmy/Fault/FaultConstants.cs @@ -0,0 +1,6 @@ +namespace Polly.Simmy.Fault; + +internal static class FaultConstants +{ + public const string OnFaultInjectedEvent = "OnFaultInjectedEvent"; +} diff --git a/src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs b/src/Polly.Core/Simmy/Fault/FaultGeneratorArguments.cs similarity index 95% rename from src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs rename to src/Polly.Core/Simmy/Fault/FaultGeneratorArguments.cs index 78fa6a35d6..6addf26ae5 100644 --- a/src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs +++ b/src/Polly.Core/Simmy/Fault/FaultGeneratorArguments.cs @@ -1,4 +1,4 @@ -namespace Polly.Simmy.Outcomes; +namespace Polly.Simmy.Fault; #pragma warning disable CA1815 // Override equals and operator equals on value types diff --git a/src/Polly.Core/Simmy/Fault/FaultPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Fault/FaultPipelineBuilderExtensions.cs new file mode 100644 index 0000000000..3705c4cb28 --- /dev/null +++ b/src/Polly.Core/Simmy/Fault/FaultPipelineBuilderExtensions.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; +using Polly.Simmy.Fault; + +namespace Polly.Simmy; + +/// +/// Extension methods for adding outcome to a . +/// +internal static class FaultPipelineBuilderExtensions +{ + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder instance. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception to inject. + /// The builder instance with the retry strategy added. + public static TBuilder AddChaosFault(this TBuilder builder, double injectionRate, Exception fault) + where TBuilder : ResiliencePipelineBuilderBase + { + builder.AddChaosFault(new FaultStrategyOptions + { + Enabled = true, + InjectionRate = injectionRate, + Fault = fault + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder instance. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception generator delegate. + /// The builder instance with the retry strategy added. + public static TBuilder AddChaosFault(this TBuilder builder, double injectionRate, Func faultGenerator) + where TBuilder : ResiliencePipelineBuilderBase + { + builder.AddChaosFault(new FaultStrategyOptions + { + Enabled = true, + InjectionRate = injectionRate, + FaultGenerator = (_) => new ValueTask(Task.FromResult(faultGenerator())) + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder type. + /// The builder instance. + /// The fault strategy options. + /// The builder instance with the retry strategy added. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + public static TBuilder AddChaosFault(this TBuilder builder, FaultStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + Guard.NotNull(builder); + Guard.NotNull(options); + + builder.AddStrategy( + context => new FaultChaosStrategy(options, context.Telemetry), + options); + + return builder; + } +} diff --git a/src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs b/src/Polly.Core/Simmy/Fault/FaultStrategyOptions.cs similarity index 97% rename from src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs rename to src/Polly.Core/Simmy/Fault/FaultStrategyOptions.cs index eeb94f7eaa..1c8ca2d7a0 100644 --- a/src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs +++ b/src/Polly.Core/Simmy/Fault/FaultStrategyOptions.cs @@ -1,4 +1,4 @@ -namespace Polly.Simmy.Outcomes; +namespace Polly.Simmy.Fault; #pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null diff --git a/src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs b/src/Polly.Core/Simmy/Fault/OnFaultInjectedArguments.cs similarity index 96% rename from src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs rename to src/Polly.Core/Simmy/Fault/OnFaultInjectedArguments.cs index 36e9287866..d883da412d 100644 --- a/src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs +++ b/src/Polly.Core/Simmy/Fault/OnFaultInjectedArguments.cs @@ -1,4 +1,4 @@ -namespace Polly.Simmy.Outcomes; +namespace Polly.Simmy.Fault; #pragma warning disable CA1815 // Override equals and operator equals on value types diff --git a/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs index f3321f6344..2fdc749ca0 100644 --- a/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs +++ b/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs @@ -14,20 +14,19 @@ internal static class LatencyPipelineBuilderExtensions /// /// The builder type. /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). /// The delay value. /// The same builder instance. /// Thrown when is . /// Thrown when the options produced from the arguments are invalid. - public static TBuilder AddChaosLatency(this TBuilder builder, bool enabled, double injectionRate, TimeSpan latency) + public static TBuilder AddChaosLatency(this TBuilder builder, double injectionRate, TimeSpan latency) where TBuilder : ResiliencePipelineBuilderBase { Guard.NotNull(builder); return builder.AddChaosLatency(new LatencyStrategyOptions { - Enabled = enabled, + Enabled = true, InjectionRate = injectionRate, Latency = latency }); diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs index 06a1322310..da5fd02b63 100644 --- a/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs @@ -8,45 +8,17 @@ internal class OutcomeChaosStrategy : MonkeyStrategy { private readonly ResilienceStrategyTelemetry _telemetry; - public OutcomeChaosStrategy(FaultStrategyOptions options, ResilienceStrategyTelemetry telemetry) - : base(options) - { - if (options.Fault is null && options.FaultGenerator is null) - { - throw new InvalidOperationException("Either Fault or FaultGenerator is required."); - } - - _telemetry = telemetry; - Fault = options.Fault; - OnFaultInjected = options.OnFaultInjected; - FaultGenerator = options.FaultGenerator is not null ? options.FaultGenerator : (_) => new(options.Fault); - } - public OutcomeChaosStrategy(OutcomeStrategyOptions options, ResilienceStrategyTelemetry telemetry) : base(options) { - if (options.Outcome is null && options.OutcomeGenerator is null) - { - throw new InvalidOperationException("Either Outcome or OutcomeGenerator is required."); - } - _telemetry = telemetry; - Outcome = options.Outcome; OnOutcomeInjected = options.OnOutcomeInjected; - OutcomeGenerator = options.OutcomeGenerator is not null ? options.OutcomeGenerator : (_) => new(options.Outcome); + OutcomeGenerator = options.OutcomeGenerator; } public Func, ValueTask>? OnOutcomeInjected { get; } - public Func? OnFaultInjected { get; } - - public Func?>>? OutcomeGenerator { get; } - - public Func>? FaultGenerator { get; } - - public Outcome? Outcome { get; } - - public Exception? Fault { get; } + public Func?>> OutcomeGenerator { get; } protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) { @@ -54,19 +26,16 @@ protected internal override async ValueTask> ExecuteCore(Func { if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) { - if (FaultGenerator is not null) - { - var fault = await InjectFault(context).ConfigureAwait(context.ContinueOnCapturedContext); - if (fault is not null) - { - return new Outcome(fault); - } - } - else if (OutcomeGenerator is not null) + var outcome = await OutcomeGenerator(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + var args = new OnOutcomeInjectedArguments(context, outcome.Value); + _telemetry.Report(new(ResilienceEventSeverity.Information, OutcomeConstants.OnOutcomeInjectedEvent), context, args); + + if (OnOutcomeInjected is not null) { - var outcome = await InjectOutcome(context).ConfigureAwait(context.ContinueOnCapturedContext); - return new Outcome(outcome.Value.Result); + await OnOutcomeInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); } + + return new Outcome(outcome.Value.Result); } return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); @@ -76,37 +45,4 @@ protected internal override async ValueTask> ExecuteCore(Func return new Outcome(e); } } - - private async ValueTask?> InjectOutcome(ResilienceContext context) - { - var outcome = await OutcomeGenerator!(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); - var args = new OnOutcomeInjectedArguments(context, outcome.Value); - _telemetry.Report(new(ResilienceEventSeverity.Information, OutcomeConstants.OnOutcomeInjectedEvent), context, args); - - if (OnOutcomeInjected is not null) - { - await OnOutcomeInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); - } - - return outcome; - } - - private async ValueTask InjectFault(ResilienceContext context) - { - var fault = await FaultGenerator!(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); - if (fault is null) - { - return null; - } - - var args = new OnFaultInjectedArguments(context, fault); - _telemetry.Report(new(ResilienceEventSeverity.Information, OutcomeConstants.OnFaultInjectedEvent), context, args); - - if (OnFaultInjected is not null) - { - await OnFaultInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); - } - - return fault; - } } diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs index c3fbf42e8d..d061a0579f 100644 --- a/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs @@ -3,6 +3,4 @@ internal static class OutcomeConstants { public const string OnOutcomeInjectedEvent = "OnOutcomeInjected"; - - public const string OnFaultInjectedEvent = "OnFaultInjectedEvent"; } diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs deleted file mode 100644 index e68873980b..0000000000 --- a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Polly.Simmy.Outcomes; - -namespace Polly.Simmy; - -/// -/// Extension methods for adding outcome to a . -/// -internal static partial class OutcomePipelineBuilderExtensions -{ - /// - /// Adds a fault chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. - /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The exception to inject. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Exception fault) - { - Guard.NotNull(builder); - - builder.AddFaultCore(new FaultStrategyOptions - { - Enabled = enabled, - InjectionRate = injectionRate, - Fault = fault - }); - return builder; - } - - /// - /// Adds a fault chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. - /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The exception generator delegate. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault( - this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func faultGenerator) - { - Guard.NotNull(builder); - - builder.AddFaultCore(new FaultStrategyOptions - { - Enabled = enabled, - InjectionRate = injectionRate, - FaultGenerator = (_) => new ValueTask(Task.FromResult(faultGenerator())) - }); - return builder; - } - - /// - /// Adds a fault chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// The fault strategy options. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) - { - Guard.NotNull(builder); - Guard.NotNull(options); - - builder.AddFaultCore(options); - return builder; - } - - /// - /// Adds an outcome chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. - /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The outcome to inject. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosResult(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, TResult result) - { - Guard.NotNull(builder); - - builder.AddOutcomeCore>(new OutcomeStrategyOptions - { - Enabled = enabled, - InjectionRate = injectionRate, - Outcome = new Outcome(result) - }); - return builder; - } - - /// - /// Adds an outcome chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. - /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The outcome generator delegate. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosResult( - this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func outcomeGenerator) - { - Guard.NotNull(builder); - - builder.AddOutcomeCore>(new OutcomeStrategyOptions - { - Enabled = enabled, - InjectionRate = injectionRate, - OutcomeGenerator = (_) => new ValueTask?>(Task.FromResult?>(Outcome.FromResult(outcomeGenerator()))) - }); - return builder; - } - - /// - /// Adds an outcome chaos strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// The outcome strategy options. - /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosResult(this ResiliencePipelineBuilder builder, OutcomeStrategyOptions options) - { - Guard.NotNull(builder); - Guard.NotNull(options); - - builder.AddOutcomeCore>(options); - return builder; - } - - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "All options members preserved.")] - private static void AddOutcomeCore( - this ResiliencePipelineBuilder builder, - OutcomeStrategyOptions options) - { - builder.AddStrategy( - context => new OutcomeChaosStrategy(options, context.Telemetry), - options); - } - - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "All options members preserved.")] - private static void AddFaultCore( - this ResiliencePipelineBuilder builder, - FaultStrategyOptions options) - { - builder.AddStrategy( - context => new OutcomeChaosStrategy(options, context.Telemetry), - options); - } -} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs index f0f397c21e..433ffb8148 100644 --- a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs +++ b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs @@ -6,76 +6,73 @@ namespace Polly.Simmy; /// /// Extension methods for adding outcome to a . /// -internal static partial class OutcomePipelineBuilderExtensions +internal static class OutcomePipelineBuilderExtensions { /// - /// Adds a fault chaos strategy to the builder. + /// Adds an outcome chaos strategy to the builder. /// + /// The type of result the retry strategy handles. /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The exception to inject. + /// The outcome to inject. For disposable outcomes use either the generator or the options overload. /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Exception fault) + public static ResiliencePipelineBuilder AddChaosResult(this ResiliencePipelineBuilder builder, double injectionRate, TResult result) { Guard.NotNull(builder); - builder.AddFaultCore(new FaultStrategyOptions + builder.AddChaosResult(new OutcomeStrategyOptions { - Enabled = enabled, + Enabled = true, InjectionRate = injectionRate, - Fault = fault + OutcomeGenerator = (_) => new ValueTask?>(Task.FromResult?>(Outcome.FromResult(result))) }); return builder; } /// - /// Adds a fault chaos strategy to the builder. + /// Adds an outcome chaos strategy to the builder. /// + /// The type of result the retry strategy handles. /// The builder instance. - /// A value that indicates whether or not the chaos strategy is enabled for a given execution. /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). - /// The exception generator delegate. + /// The outcome generator delegate. /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault( - this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func faultGenerator) + public static ResiliencePipelineBuilder AddChaosResult( + this ResiliencePipelineBuilder builder, double injectionRate, Func resultGenerator) { Guard.NotNull(builder); - builder.AddFaultCore(new FaultStrategyOptions + builder.AddChaosResult(new OutcomeStrategyOptions { - Enabled = enabled, + Enabled = true, InjectionRate = injectionRate, - FaultGenerator = (_) => new ValueTask(Task.FromResult(faultGenerator())) + OutcomeGenerator = (_) => new ValueTask?>(Task.FromResult?>(Outcome.FromResult(resultGenerator()))) }); return builder; } /// - /// Adds a fault chaos strategy to the builder. + /// Adds an outcome chaos strategy to the builder. /// + /// The type of result the retry strategy handles. /// The builder instance. - /// The fault strategy options. + /// The outcome strategy options. /// The builder instance with the retry strategy added. - public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) - { - Guard.NotNull(builder); - Guard.NotNull(options); - - builder.AddFaultCore(options); - return builder; - } - [UnconditionalSuppressMessage( "Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "All options members preserved.")] - private static void AddFaultCore(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) + public static ResiliencePipelineBuilder AddChaosResult<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>( + this ResiliencePipelineBuilder builder, + OutcomeStrategyOptions options) { - builder.AddStrategy(context => - new OutcomeChaosStrategy( - options, - context.Telemetry), + Guard.NotNull(builder); + Guard.NotNull(options); + + builder.AddStrategy( + context => new OutcomeChaosStrategy(options, context.Telemetry), options); + + return builder; } } diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs index 1b7e74a9fd..8e177cacea 100644 --- a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs @@ -1,4 +1,6 @@ -namespace Polly.Simmy.Outcomes; +using System.ComponentModel.DataAnnotations; + +namespace Polly.Simmy.Outcomes; #pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null @@ -19,18 +21,6 @@ internal class OutcomeStrategyOptions : MonkeyStrategyOptions /// /// Gets or sets the outcome generator to be injected for a given execution. /// - /// - /// Defaults to . Either or this property is required. - /// When this property is the is used. - /// - public Func?>>? OutcomeGenerator { get; set; } - - /// - /// Gets or sets the outcome to be injected for a given execution. - /// - /// - /// Defaults to . Either or this property is required. - /// When this property is the is used. - /// - public Outcome? Outcome { get; set; } + [Required] + public Func?>> OutcomeGenerator { get; set; } } diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs index ba176a5a1b..4e41dbec53 100644 --- a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs @@ -13,7 +13,7 @@ public static IEnumerable AddBehavior_Ok_Data() Func behavior = () => new ValueTask(Task.CompletedTask); yield return new object[] { - (ResiliencePipelineBuilder builder) => { builder.AddChaosBehavior(true, 0.5, behavior); }, + (ResiliencePipelineBuilder builder) => { builder.AddChaosBehavior(0.5, behavior); }, (BehaviorChaosStrategy strategy) => { strategy.Behavior!.Invoke(new(context)).Preserve().GetAwaiter().IsCompleted.Should().BeTrue(); @@ -26,7 +26,7 @@ public static IEnumerable AddBehavior_Ok_Data() [Fact] public void AddBehavior_Shortcut_Option_Ok() { - var sut = new ResiliencePipelineBuilder().AddChaosBehavior(true, 0.5, () => new ValueTask(Task.CompletedTask)).Build(); + var sut = new ResiliencePipelineBuilder().AddChaosBehavior(0.5, () => new ValueTask(Task.CompletedTask)).Build(); sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); } @@ -34,7 +34,7 @@ public void AddBehavior_Shortcut_Option_Ok() public void AddBehavior_Shortcut_Option_Throws() { new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosBehavior(true, -1, () => new ValueTask(Task.CompletedTask))) + .Invoking(b => b.AddChaosBehavior(-1, () => new ValueTask(Task.CompletedTask))) .Should() .Throw(); } diff --git a/test/Polly.Core.Tests/Simmy/Fault/FaultChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultChaosPipelineBuilderExtensionsTests.cs new file mode 100644 index 0000000000..17fab140a8 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultChaosPipelineBuilderExtensionsTests.cs @@ -0,0 +1,133 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Simmy.Fault; +using Polly.Testing; + +namespace Polly.Core.Tests.Simmy.Fault; + +public class FaultChaosPipelineBuilderExtensionsTests +{ + public static readonly TheoryData> FaultStrategy = new() + { + builder => + { + builder.AddChaosFault(new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = new InvalidOperationException("Dummy exception.") + }); + + AssertFaultStrategy(builder, true, 0.6) + .Fault.Should().BeOfType(typeof(InvalidOperationException)); + } + }; + + private static void AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) + where TException : Exception + { + var context = ResilienceContextPool.Shared.Get(); + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType().Subject; + + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); + strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); + } + + private static FaultChaosStrategy AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) + where TException : Exception + { + var context = ResilienceContextPool.Shared.Get(); + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType().Subject; + + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); + strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); + + return strategy; + } + + [MemberData(nameof(FaultStrategy))] + [Theory] + internal void AddFault_Options_Ok(Action configure) + { + var builder = new ResiliencePipelineBuilder(); + builder.Invoking(b => configure(b)).Should().NotThrow(); + } + + [Fact] + public void AddFault_Shortcut_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(0.5, new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Generator_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault( + 1.5, + () => new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Shortcut_Generator_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault( + 1.5, + () => new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Shortcut_Generator_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(0.5, () => new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(0.5, new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault(-1, new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Generic_Shortcut_Generator_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(0.5, () => new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Fault/FaultChaosStrategyTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultChaosStrategyTests.cs new file mode 100644 index 0000000000..c7014e1a5c --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultChaosStrategyTests.cs @@ -0,0 +1,270 @@ +using Polly.Simmy; +using Polly.Simmy.Fault; +using Polly.Telemetry; + +namespace Polly.Core.Tests.Simmy.Fault; + +public class FaultChaosStrategyTests +{ + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly List> _args = new(); + + public FaultChaosStrategyTests() => _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); + + public static List FaultCtorTestCases => + new() + { + new object[] { null!, "Value cannot be null. (Parameter 'options')", typeof(ArgumentNullException) }, + new object[] + { + new FaultStrategyOptions + { + InjectionRate = 1, + Enabled = true, + }, + "Either Fault or FaultGenerator is required.", + typeof(InvalidOperationException) + }, + }; + + [Theory] + [MemberData(nameof(FaultCtorTestCases))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void FaultInvalidCtor(object options, string expectedMessage, Type expectedException) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + var _ = new FaultChaosStrategy((FaultStrategyOptions)options, _telemetry); + } + catch (Exception ex) + { + Assert.IsType(expectedException, ex); +#if !NET481 + Assert.Equal(expectedMessage, ex.Message); +#endif + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + [Fact] + public void Given_not_enabled_should_not_inject_fault() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = false, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = CreateSut(options); + sut.Execute(() => { userDelegateExecuted = true; }); + + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_not_enabled_should_not_inject_fault_and_return_outcome() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = false, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + var response = await sut.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }); + + response.Should().Be(HttpStatusCode.OK); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault_instead_returning_outcome() + { + var onFaultInjected = false; + var userDelegateExecuted = false; + var exceptionMessage = "Dummy exception"; + var fault = new InvalidOperationException(exceptionMessage); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault, + OnFaultInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onFaultInjected = true; + return default; + } + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }).AsTask()) + .Should() + .ThrowAsync() + .WithMessage(exceptionMessage); + + userDelegateExecuted.Should().BeFalse(); + onFaultInjected.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault() + { + var onFaultInjected = false; + var userDelegateExecuted = false; + var exceptionMessage = "Dummy exception"; + var fault = new InvalidOperationException(exceptionMessage); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault, + OnFaultInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onFaultInjected = true; + return default; + } + }; + + var sut = CreateSut(options); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(200); + }).AsTask()) + .Should() + .ThrowAsync() + .WithMessage(exceptionMessage); + + userDelegateExecuted.Should().BeFalse(); + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType(); + _args[0].Event.EventName.Should().Be(FaultConstants.OnFaultInjectedEvent); + onFaultInjected.Should().BeTrue(); + } + + [Fact] + public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_fault() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.3, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = CreateSut(options); + var result = sut.Execute(_ => + { + userDelegateExecuted = true; + return 200; + }); + + result.Should().Be(200); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public void Given_enabled_and_randomly_within_threshold_should_not_inject_fault_when_exception_is_null() + { + var userDelegateExecuted = false; + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + FaultGenerator = (_) => new ValueTask(Task.FromResult(null)) + }; + + var sut = CreateSut(options); + sut.Execute(_ => + { + userDelegateExecuted = true; + }); + + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running_the_strategy() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + using var cts = new CancellationTokenSource(); + var options = new FaultStrategyOptions + { + InjectionRate = 0.3, + EnabledGenerator = (_) => + { + cts.Cancel(); + return new ValueTask(true); + }, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = CreateSut(options); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(1); + }, cts.Token) + .AsTask()) + .Should() + .ThrowAsync(); + + userDelegateExecuted.Should().BeFalse(); + } + + private ResiliencePipeline CreateSut(FaultStrategyOptions options) => + new FaultChaosStrategy(options, _telemetry).AsPipeline(); +} + +/// +/// Borrowing this from the actual dotnet standard implementation since it is not available in the net481. +/// +public enum HttpStatusCode +{ + // Summary: + // Equivalent to HTTP status 200. System.Net.HttpStatusCode.OK indicates that the + // request succeeded and that the requested information is in the response. This + // is the most common status code to receive. + OK = 200, + + // Summary: + // Equivalent to HTTP status 429. System.Net.HttpStatusCode.TooManyRequests indicates + // that the user has sent too many requests in a given amount of time. + TooManyRequests = 429, +} diff --git a/test/Polly.Core.Tests/Simmy/Fault/FaultConstantsTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultConstantsTests.cs new file mode 100644 index 0000000000..ffc1cfd1ff --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultConstantsTests.cs @@ -0,0 +1,12 @@ +using Polly.Simmy.Fault; + +namespace Polly.Core.Tests.Simmy.Fault; + +public class FaultConstantsTests +{ + [Fact] + public void EnsureDefaults() + { + FaultConstants.OnFaultInjectedEvent.Should().Be("OnFaultInjectedEvent"); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorArgumentsTests.cs similarity index 75% rename from test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs rename to test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorArgumentsTests.cs index 6d05a24039..0c7b8e63a3 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorArgumentsTests.cs @@ -1,6 +1,6 @@ -using Polly.Simmy.Outcomes; +using Polly.Simmy.Fault; -namespace Polly.Core.Tests.Simmy.Outcomes; +namespace Polly.Core.Tests.Simmy.Fault; public class FaultGeneratorArgumentsTests { diff --git a/test/Polly.Core.Tests/Simmy/Fault/FaultStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultStrategyOptionsTests.cs new file mode 100644 index 0000000000..ba211b4993 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultStrategyOptionsTests.cs @@ -0,0 +1,21 @@ +using Polly.Simmy; +using Polly.Simmy.Fault; + +namespace Polly.Core.Tests.Simmy.Fault; + +public class FaultStrategyOptionsTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new FaultStrategyOptions(); + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + sut.Fault.Should().BeNull(); + sut.OnFaultInjected.Should().BeNull(); + sut.FaultGenerator.Should().BeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Fault/OnFaultInjectedArgumentsTests.cs similarity index 80% rename from test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs rename to test/Polly.Core.Tests/Simmy/Fault/OnFaultInjectedArgumentsTests.cs index 52fec6411e..4c9d932574 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Fault/OnFaultInjectedArgumentsTests.cs @@ -1,6 +1,6 @@ -using Polly.Simmy.Outcomes; +using Polly.Simmy.Fault; -namespace Polly.Core.Tests.Simmy.Outcomes; +namespace Polly.Core.Tests.Simmy.Fault; public class OnFaultInjectedArgumentsTests { diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs index 57be2dbcde..31b957f804 100644 --- a/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs @@ -12,7 +12,7 @@ public static IEnumerable AddLatency_Ok_Data() Func behavior = () => new ValueTask(Task.CompletedTask); yield return new object[] { - (ResiliencePipelineBuilder builder) => { builder.AddChaosLatency(true, 0.5, TimeSpan.FromSeconds(10)); }, + (ResiliencePipelineBuilder builder) => { builder.AddChaosLatency(0.5, TimeSpan.FromSeconds(10)); }, (LatencyChaosStrategy strategy) => { strategy.Latency.Should().Be(TimeSpan.FromSeconds(10)); @@ -26,7 +26,7 @@ public static IEnumerable AddLatency_Ok_Data() [Fact] public void AddLatency_Shortcut_Option_Ok() { - var sut = new ResiliencePipelineBuilder().AddChaosLatency(true, 0.5, TimeSpan.FromSeconds(10)).Build(); + var sut = new ResiliencePipelineBuilder().AddChaosLatency(0.5, TimeSpan.FromSeconds(10)).Build(); sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); } diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs index 1dbb2fe5a1..946b2a442f 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs @@ -16,49 +16,14 @@ public class OutcomeChaosPipelineBuilderExtensionsTests InjectionRate = 0.6, Enabled = true, Randomizer = () => 0.5, - Outcome = new Outcome(100) + OutcomeGenerator = (_) => new ValueTask?>(Outcome.FromResult(100)) }); - AssertResultStrategy(builder, true, 0.6, new(100)) - .Outcome.Should().Be(new Outcome(100)); + AssertResultStrategy(builder, true, 0.6, new(100)); } }; - public static readonly TheoryData>> FaultGenericStrategy = new() - { - builder => - { - builder.AddChaosFault(new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = true, - Randomizer = () => 0.5, - Fault = new InvalidOperationException("Dummy exception.") - }); - - AssertFaultStrategy(builder, true, 0.6) - .Fault.Should().BeOfType(typeof(InvalidOperationException)); - } - }; - - public static readonly TheoryData> FaultStrategy = new() - { - builder => - { - builder.AddChaosFault(new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = true, - Randomizer = () => 0.5, - Fault = new InvalidOperationException("Dummy exception.") - }); - - AssertFaultStrategy(builder, true, 0.6) - .Fault.Should().BeOfType(typeof(InvalidOperationException)); - } - }; - - private static OutcomeChaosStrategy AssertResultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Outcome outcome) + private static void AssertResultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Outcome outcome) { var context = ResilienceContextPool.Shared.Get(); var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; @@ -66,34 +31,6 @@ private static OutcomeChaosStrategy AssertResultStrategy(ResiliencePipelin strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); strategy.OutcomeGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(outcome); - - return strategy; - } - - private static OutcomeChaosStrategy AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) - where TException : Exception - { - var context = ResilienceContextPool.Shared.Get(); - var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; - - strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); - strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); - strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); - - return strategy; - } - - private static OutcomeChaosStrategy AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) - where TException : Exception - { - var context = ResilienceContextPool.Shared.Get(); - var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; - - strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); - strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); - strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); - - return strategy; } [MemberData(nameof(ResultStrategy))] @@ -104,28 +41,12 @@ internal void AddResult_Options_Ok(Action> config builder.Invoking(b => configure(b)).Should().NotThrow(); } - [MemberData(nameof(FaultGenericStrategy))] - [Theory] - internal void AddFault_Generic_Options_Ok(Action> configure) - { - var builder = new ResiliencePipelineBuilder(); - builder.Invoking(b => configure(b)).Should().NotThrow(); - } - - [MemberData(nameof(FaultStrategy))] - [Theory] - internal void AddFault_Options_Ok(Action configure) - { - var builder = new ResiliencePipelineBuilder(); - builder.Invoking(b => configure(b)).Should().NotThrow(); - } - [Fact] public void AddResult_Shortcut_Option_Ok() { var builder = new ResiliencePipelineBuilder(); builder - .AddChaosResult(true, 0.5, 120) + .AddChaosResult(0.5, 120) .Build(); AssertResultStrategy(builder, true, 0.5, new(120)); @@ -136,7 +57,7 @@ public void AddResult_Shortcut_Generator_Option_Ok() { var builder = new ResiliencePipelineBuilder(); builder - .AddChaosResult(true, 0.5, () => 120) + .AddChaosResult(0.5, () => 120) .Build(); AssertResultStrategy(builder, true, 0.5, new(120)); @@ -146,85 +67,8 @@ public void AddResult_Shortcut_Generator_Option_Ok() public void AddResult_Shortcut_Option_Throws() { new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosResult(true, -1, () => 120)) - .Should() - .Throw(); - } - - [Fact] - public void AddFault_Shortcut_Option_Ok() - { - var builder = new ResiliencePipelineBuilder(); - builder - .AddChaosFault(true, 0.5, new InvalidOperationException("Dummy exception")) - .Build(); - - AssertFaultStrategy(builder, true, 0.5); - } - - [Fact] - public void AddFault_Generic_Shortcut_Generator_Option_Throws() - { - new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosFault( - true, - 1.5, - () => new InvalidOperationException())) - .Should() - .Throw(); - } - - [Fact] - public void AddFault_Shortcut_Generator_Option_Throws() - { - new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosFault( - true, - 1.5, - () => new InvalidOperationException())) - .Should() - .Throw(); - } - - [Fact] - public void AddFault_Shortcut_Generator_Option_Ok() - { - var builder = new ResiliencePipelineBuilder(); - builder - .AddChaosFault(true, 0.5, () => new InvalidOperationException("Dummy exception")) - .Build(); - - AssertFaultStrategy(builder, true, 0.5); - } - - [Fact] - public void AddFault_Generic_Shortcut_Option_Ok() - { - var builder = new ResiliencePipelineBuilder(); - builder - .AddChaosFault(true, 0.5, new InvalidOperationException("Dummy exception")) - .Build(); - - AssertFaultStrategy(builder, true, 0.5); - } - - [Fact] - public void AddFault_Generic_Shortcut_Option_Throws() - { - new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosFault(true, -1, new InvalidOperationException())) + .Invoking(b => b.AddChaosResult(-1, () => 120)) .Should() .Throw(); } - - [Fact] - public void AddFault_Generic_Shortcut_Generator_Option_Ok() - { - var builder = new ResiliencePipelineBuilder(); - builder - .AddChaosFault(true, 0.5, () => new InvalidOperationException("Dummy exception")) - .Build(); - - AssertFaultStrategy(builder, true, 0.5); - } } diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs index 7ba2b003b3..7a7e8fb631 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs @@ -1,5 +1,4 @@ -using Polly.Simmy; -using Polly.Simmy.Outcomes; +using Polly.Simmy.Outcomes; using Polly.Telemetry; namespace Polly.Core.Tests.Simmy.Outcomes; @@ -11,22 +10,6 @@ public class OutcomeChaosStrategyTests public OutcomeChaosStrategyTests() => _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); - public static List FaultCtorTestCases => - new() - { - new object[] { null!, "Value cannot be null. (Parameter 'options')", typeof(ArgumentNullException) }, - new object[] - { - new FaultStrategyOptions - { - InjectionRate = 1, - Enabled = true, - }, - "Either Fault or FaultGenerator is required.", - typeof(InvalidOperationException) - }, - }; - public static List ResultCtorTestCases => new() { @@ -43,27 +26,6 @@ public class OutcomeChaosStrategyTests }, }; - [Theory] - [MemberData(nameof(FaultCtorTestCases))] -#pragma warning disable xUnit1026 // Theory methods should use all of their parameters - public void FaultInvalidCtor(object options, string expectedMessage, Type expectedException) -#pragma warning restore xUnit1026 // Theory methods should use all of their parameters - { -#pragma warning disable CA1031 // Do not catch general exception types - try - { - var _ = new OutcomeChaosStrategy((FaultStrategyOptions)options, _telemetry); - } - catch (Exception ex) - { - Assert.IsType(expectedException, ex); -#if !NET481 - Assert.Equal(expectedMessage, ex.Message); -#endif - } -#pragma warning restore CA1031 // Do not catch general exception types - } - [Theory] [MemberData(nameof(ResultCtorTestCases))] #pragma warning disable xUnit1026 // Theory methods should use all of their parameters @@ -85,174 +47,6 @@ public void ResultInvalidCtor(object options, string expectedMessage, Type expec #pragma warning restore CA1031 // Do not catch general exception types } - [Fact] - public void Given_not_enabled_should_not_inject_fault() - { - var userDelegateExecuted = false; - var fault = new InvalidOperationException("Dummy exception"); - - var options = new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = false, - Randomizer = () => 0.5, - Fault = fault - }; - - var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); - sut.Execute(() => { userDelegateExecuted = true; }); - - userDelegateExecuted.Should().BeTrue(); - } - - [Fact] - public async Task Given_not_enabled_should_not_inject_fault_and_return_outcome() - { - var userDelegateExecuted = false; - var fault = new InvalidOperationException("Dummy exception"); - - var options = new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = false, - Randomizer = () => 0.5, - Fault = fault - }; - - var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); - var response = await sut.ExecuteAsync(async _ => - { - userDelegateExecuted = true; - return await Task.FromResult(HttpStatusCode.OK); - }); - - response.Should().Be(HttpStatusCode.OK); - userDelegateExecuted.Should().BeTrue(); - } - - [Fact] - public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault_instead_returning_outcome() - { - var onFaultInjected = false; - var userDelegateExecuted = false; - var exceptionMessage = "Dummy exception"; - var fault = new InvalidOperationException(exceptionMessage); - - var options = new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = true, - Randomizer = () => 0.5, - Fault = fault, - OnFaultInjected = args => - { - args.Context.Should().NotBeNull(); - args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); - onFaultInjected = true; - return default; - } - }; - - var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); - await sut.Invoking(s => s.ExecuteAsync(async _ => - { - userDelegateExecuted = true; - return await Task.FromResult(HttpStatusCode.OK); - }).AsTask()) - .Should() - .ThrowAsync() - .WithMessage(exceptionMessage); - - userDelegateExecuted.Should().BeFalse(); - onFaultInjected.Should().BeTrue(); - } - - [Fact] - public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault() - { - var onFaultInjected = false; - var userDelegateExecuted = false; - var exceptionMessage = "Dummy exception"; - var fault = new InvalidOperationException(exceptionMessage); - - var options = new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = true, - Randomizer = () => 0.5, - Fault = fault, - OnFaultInjected = args => - { - args.Context.Should().NotBeNull(); - args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); - onFaultInjected = true; - return default; - } - }; - - var sut = CreateSut(options); - await sut.Invoking(s => s.ExecuteAsync(async _ => - { - userDelegateExecuted = true; - return await Task.FromResult(200); - }).AsTask()) - .Should() - .ThrowAsync() - .WithMessage(exceptionMessage); - - userDelegateExecuted.Should().BeFalse(); - _args.Should().HaveCount(1); - _args[0].Arguments.Should().BeOfType(); - _args[0].Event.EventName.Should().Be(OutcomeConstants.OnFaultInjectedEvent); - onFaultInjected.Should().BeTrue(); - } - - [Fact] - public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_fault() - { - var userDelegateExecuted = false; - var fault = new InvalidOperationException("Dummy exception"); - - var options = new FaultStrategyOptions - { - InjectionRate = 0.3, - Enabled = true, - Randomizer = () => 0.5, - Fault = fault - }; - - var sut = CreateSut(options); - var result = sut.Execute(_ => - { - userDelegateExecuted = true; - return 200; - }); - - result.Should().Be(200); - userDelegateExecuted.Should().BeTrue(); - } - - [Fact] - public void Given_enabled_and_randomly_within_threshold_should_not_inject_fault_when_exception_is_null() - { - var userDelegateExecuted = false; - var options = new FaultStrategyOptions - { - InjectionRate = 0.6, - Enabled = true, - Randomizer = () => 0.5, - FaultGenerator = (_) => new ValueTask(Task.FromResult(null)) - }; - - var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); - sut.Execute(_ => - { - userDelegateExecuted = true; - }); - - userDelegateExecuted.Should().BeTrue(); - } - [Fact] public void Given_not_enabled_should_not_inject_result() { @@ -264,7 +58,7 @@ public void Given_not_enabled_should_not_inject_result() InjectionRate = 0.6, Enabled = false, Randomizer = () => 0.5, - Outcome = new Outcome(fakeResult) + OutcomeGenerator = (_) => new ValueTask?>(Outcome.FromResult(fakeResult)) }; var sut = CreateSut(options); @@ -286,7 +80,7 @@ public async Task Given_enabled_and_randomly_within_threshold_should_inject_resu InjectionRate = 0.6, Enabled = true, Randomizer = () => 0.5, - Outcome = new Outcome(fakeResult), + OutcomeGenerator = (_) => new ValueTask?>(Outcome.FromResult(fakeResult)), OnOutcomeInjected = args => { args.Context.Should().NotBeNull(); @@ -323,7 +117,7 @@ public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_re InjectionRate = 0.3, Enabled = false, Randomizer = () => 0.5, - Outcome = new Outcome(fakeResult) + OutcomeGenerator = (_) => new ValueTask?>(Outcome.FromResult(fakeResult)) }; var sut = CreateSut(options); @@ -341,12 +135,13 @@ public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_re public async Task Given_enabled_and_randomly_within_threshold_should_inject_result_even_as_null() { var userDelegateExecuted = false; + Outcome? nullOutcome = Outcome.FromResult(null); var options = new OutcomeStrategyOptions { InjectionRate = 0.6, Enabled = true, Randomizer = () => 0.5, - Outcome = Outcome.FromResult(null) + OutcomeGenerator = (_) => new ValueTask?>(nullOutcome) }; var sut = CreateSut(options); @@ -375,7 +170,7 @@ public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running cts.Cancel(); return new ValueTask(true); }, - Outcome = Outcome.FromResult(HttpStatusCode.TooManyRequests) + OutcomeGenerator = (_) => new ValueTask?>(Outcome.FromResult(HttpStatusCode.TooManyRequests)) }; var sut = CreateSut(options); @@ -393,9 +188,6 @@ await sut.Invoking(s => s.ExecuteAsync(async _ => private ResiliencePipeline CreateSut(OutcomeStrategyOptions options) => new OutcomeChaosStrategy(options, _telemetry).AsPipeline(); - - private ResiliencePipeline CreateSut(FaultStrategyOptions options) => - new OutcomeChaosStrategy(options, _telemetry).AsPipeline(); } /// diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs index f0427f90cd..dcd9291051 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs @@ -7,7 +7,6 @@ public class OutcomeConstantsTests [Fact] public void EnsureDefaults() { - OutcomeConstants.OnFaultInjectedEvent.Should().Be("OnFaultInjectedEvent"); OutcomeConstants.OnOutcomeInjectedEvent.Should().Be("OnOutcomeInjected"); } } diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs index d2442bedd6..246c2c8c68 100644 --- a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs @@ -14,7 +14,6 @@ public void Ctor_Ok() sut.EnabledGenerator.Should().BeNull(); sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); sut.InjectionRateGenerator.Should().BeNull(); - sut.Outcome.Should().BeNull(); sut.OnOutcomeInjected.Should().BeNull(); sut.OutcomeGenerator.Should().BeNull(); }