From e92e0c8e68c8ce24da27b8ad8061c5691e3c52bb Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sat, 1 Mar 2025 12:01:14 -0600 Subject: [PATCH] refactor: Use chain of responsibility for pipeline phases --- Directory.Build.props | 5 +- Directory.Packages.props | 2 +- .../CustomFormatPipelineContext.cs | 14 ++-- .../CustomFormatApiFetchPhase.cs | 8 +-- .../CustomFormatApiPersistencePhase.cs | 13 ++-- .../PipelinePhases/CustomFormatConfigPhase.cs | 25 +++++-- .../PipelinePhases/CustomFormatLogPhase.cs | 29 -------- .../CustomFormatPreviewPhase.cs | 6 +- .../CustomFormatTransactionPhase.cs | 8 +-- .../Generic/GenericPipelinePhases.cs | 13 ---- .../Pipelines/Generic/GenericSyncPipeline.cs | 48 ------------- .../Generic/IApiFetchPipelinePhase.cs | 7 -- .../Generic/IApiPersistencePipelinePhase.cs | 7 -- .../Pipelines/Generic/IConfigPipelinePhase.cs | 7 -- .../Pipelines/Generic/ILogPipelinePhase.cs | 9 --- .../Pipelines/Generic/IPipelineContext.cs | 9 --- .../Generic/IPreviewPipelinePhase.cs | 7 -- .../Generic/ITransactionPipelinePhase.cs | 7 -- .../Pipelines/GenericSyncPipeline.cs | 25 +++++++ src/Recyclarr.Cli/Pipelines/IPipelinePhase.cs | 6 ++ .../MediaNaming/MediaNamingPipelineContext.cs | 14 ++-- .../MediaNamingApiFetchPhase.cs | 8 +-- .../MediaNamingApiPersistencePhase.cs | 36 ++++++++-- .../PipelinePhases/MediaNamingConfigPhase.cs | 47 ++++++++++-- .../PipelinePhases/MediaNamingLogPhase.cs | 71 ------------------- .../PipelinePhases/MediaNamingPreviewPhase.cs | 7 +- .../MediaNamingTransactionPhase.cs | 9 +-- .../Pipelines/PipelineAutofacModule.cs | 18 ++--- .../Pipelines/PipelineContext.cs | 12 ++++ .../Pipelines/PreviewPipelinePhase.cs | 18 +++++ .../Models/QualityProfileServiceData.cs | 8 +++ .../QualityProfileApiFetchPhase.cs | 14 ++-- .../QualityProfileApiPersistencePhase.cs | 12 ++-- .../QualityProfileConfigPhase.cs | 16 +++-- .../QualityProfilePreviewPhase.cs | 7 +- .../QualityProfileTransactionPhase.cs | 12 ++-- ...ileLogPhase.cs => QualityProfileLogger.cs} | 18 +---- .../QualityProfilePipelineContext.cs | 21 ++---- .../Models/ProcessedQualitySizeData.cs | 8 --- .../QualitySizeApiFetchPhase.cs | 8 +-- .../QualitySizeApiPersistencePhase.cs | 38 ++++++++-- .../PipelinePhases/QualitySizeConfigPhase.cs | 66 ++++++++++------- .../PipelinePhases/QualitySizeLogPhase.cs | 51 ------------- .../PipelinePhases/QualitySizePreviewPhase.cs | 12 ++-- .../QualitySizeTransactionPhase.cs | 11 ++- .../QualitySize/QualitySizePipelineContext.cs | 18 ++--- .../Processors/Sync/CompositeSyncPipeline.cs | 2 +- .../CompositionRootTest.cs | 1 + .../CustomFormatTransactionPhaseTest.cs | 52 +++++++------- .../MediaNamingTransactionPhaseRadarrTest.cs | 18 ++--- .../MediaNamingTransactionPhaseSonarrTest.cs | 18 ++--- .../QualityProfileTransactionPhaseTest.cs | 48 +++++++------ .../QualitySizeConfigPhaseTest.cs | 14 ++-- .../QualitySizeTransactionPhaseTest.cs | 53 +++++++------- 54 files changed, 455 insertions(+), 566 deletions(-) delete mode 100644 src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatLogPhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/GenericPipelinePhases.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/GenericSyncPipeline.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/IApiFetchPipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/IApiPersistencePipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/IConfigPipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/ILogPipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/IPipelineContext.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/IPreviewPipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/Generic/ITransactionPipelinePhase.cs create mode 100644 src/Recyclarr.Cli/Pipelines/GenericSyncPipeline.cs create mode 100644 src/Recyclarr.Cli/Pipelines/IPipelinePhase.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingLogPhase.cs create mode 100644 src/Recyclarr.Cli/Pipelines/PipelineContext.cs create mode 100644 src/Recyclarr.Cli/Pipelines/PreviewPipelinePhase.cs create mode 100644 src/Recyclarr.Cli/Pipelines/QualityProfile/Models/QualityProfileServiceData.cs rename src/Recyclarr.Cli/Pipelines/QualityProfile/{PipelinePhases/QualityProfileLogPhase.cs => QualityProfileLogger.cs} (91%) delete mode 100644 src/Recyclarr.Cli/Pipelines/QualitySize/Models/ProcessedQualitySizeData.cs delete mode 100644 src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs diff --git a/Directory.Build.props b/Directory.Build.props index 55a070d2..55373324 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,8 +26,9 @@ - - + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index f60ff695..d4715004 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,4 +75,4 @@ - \ No newline at end of file + diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/CustomFormatPipelineContext.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/CustomFormatPipelineContext.cs index f5c27bd7..f636d419 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/CustomFormatPipelineContext.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/CustomFormatPipelineContext.cs @@ -1,20 +1,16 @@ using Recyclarr.Cli.Pipelines.CustomFormat.Cache; using Recyclarr.Cli.Pipelines.CustomFormat.Models; -using Recyclarr.Cli.Pipelines.Generic; -using Recyclarr.TrashGuide; using Recyclarr.TrashGuide.CustomFormat; namespace Recyclarr.Cli.Pipelines.CustomFormat; -public class CustomFormatPipelineContext : IPipelineContext +internal class CustomFormatPipelineContext : PipelineContext { - public string PipelineDescription => "Custom Format"; - public IReadOnlyCollection SupportedServiceTypes { get; } = - [SupportedServices.Sonarr, SupportedServices.Radarr]; + public override string PipelineDescription => "Custom Format"; public IList ConfigOutput { get; init; } = []; public IList ApiFetchOutput { get; init; } = []; - public CustomFormatTransactionData TransactionOutput { get; set; } = default!; - public IReadOnlyCollection InvalidFormats { get; set; } = default!; - public CustomFormatCache Cache { get; set; } = default!; + public CustomFormatTransactionData TransactionOutput { get; set; } = null!; + public IReadOnlyCollection InvalidFormats { get; set; } = null!; + public CustomFormatCache Cache { get; set; } = null!; } diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiFetchPhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiFetchPhase.cs index bdf18407..77242a1f 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiFetchPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiFetchPhase.cs @@ -1,16 +1,16 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Common.Extensions; using Recyclarr.ServarrApi.CustomFormat; namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; -public class CustomFormatApiFetchPhase(ICustomFormatApiService api) - : IApiFetchPipelinePhase +internal class CustomFormatApiFetchPhase(ICustomFormatApiService api) + : IPipelinePhase { - public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct) + public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct) { var result = await api.GetCustomFormats(ct); context.ApiFetchOutput.AddRange(result); context.Cache.RemoveStale(result); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiPersistencePhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiPersistencePhase.cs index 9491f9e0..7d6573c3 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiPersistencePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatApiPersistencePhase.cs @@ -1,16 +1,16 @@ using Recyclarr.Cache; using Recyclarr.Cli.Pipelines.CustomFormat.Cache; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.CustomFormat; namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; -public class CustomFormatApiPersistencePhase( +internal class CustomFormatApiPersistencePhase( ICustomFormatApiService api, - ICachePersister cachePersister -) : IApiPersistencePipelinePhase + ICachePersister cachePersister, + CustomFormatTransactionLogger cfLogger +) : IPipelinePhase { - public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct) + public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct) { var transactions = context.TransactionOutput; @@ -35,5 +35,8 @@ public class CustomFormatApiPersistencePhase( context.Cache.Update(transactions); cachePersister.Save(context.Cache); + + cfLogger.LogTransactions(context); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatConfigPhase.cs index 489472a0..3724efae 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatConfigPhase.cs @@ -1,21 +1,21 @@ using Recyclarr.Cache; using Recyclarr.Cli.Pipelines.CustomFormat.Cache; using Recyclarr.Cli.Pipelines.CustomFormat.Models; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Common.Extensions; using Recyclarr.Config.Models; using Recyclarr.TrashGuide.CustomFormat; namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; -public class CustomFormatConfigPhase( +internal class CustomFormatConfigPhase( + ILogger log, ICustomFormatGuideService guide, ProcessedCustomFormatCache cache, ICachePersister cachePersister, IServiceConfiguration config -) : IConfigPipelinePhase +) : IPipelinePhase { - public Task Execute(CustomFormatPipelineContext context, CancellationToken ct) + public Task Execute(CustomFormatPipelineContext context, CancellationToken ct) { // Match custom formats in the YAML config to those in the guide, by Trash ID // @@ -40,6 +40,21 @@ public class CustomFormatConfigPhase( context.Cache = cachePersister.Load(); cache.AddCustomFormats(context.ConfigOutput); - return Task.CompletedTask; + return Task.FromResult(LogConfigPhaseAndExitIfNeeded(context)); + } + + // Returning 'true' means to exit. 'false' means to proceed. + private bool LogConfigPhaseAndExitIfNeeded(CustomFormatPipelineContext context) + { + if (context.InvalidFormats.Count != 0) + { + log.Warning( + "These Custom Formats do not exist in the guide and will be skipped: {Cfs}", + context.InvalidFormats + ); + } + + // Do not exit when the config has zero custom formats. We still may need to delete old custom formats. + return false; } } diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatLogPhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatLogPhase.cs deleted file mode 100644 index e80cbb23..00000000 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatLogPhase.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Recyclarr.Cli.Pipelines.Generic; - -namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; - -internal class CustomFormatLogPhase(CustomFormatTransactionLogger cfLogger, ILogger log) - : ILogPipelinePhase -{ - // Returning 'true' means to exit. 'false' means to proceed. - public bool LogConfigPhaseAndExitIfNeeded(CustomFormatPipelineContext context) - { - if (context.InvalidFormats.Count != 0) - { - log.Warning( - "These Custom Formats do not exist in the guide and will be skipped: {Cfs}", - context.InvalidFormats - ); - } - - // Do not exit when the config has zero custom formats. We still may need to delete old custom formats. - return false; - } - - public void LogTransactionNotices(CustomFormatPipelineContext context) { } - - public void LogPersistenceResults(CustomFormatPipelineContext context) - { - cfLogger.LogTransactions(context); - } -} diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatPreviewPhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatPreviewPhase.cs index b62fe55d..7b1ea8fa 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatPreviewPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatPreviewPhase.cs @@ -1,11 +1,9 @@ -using Recyclarr.Cli.Pipelines.Generic; - namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; internal class CustomFormatPreviewPhase(CustomFormatTransactionLogger logger) - : IPreviewPipelinePhase + : PreviewPipelinePhase { - public void Execute(CustomFormatPipelineContext context) + protected override void RenderPreview(CustomFormatPipelineContext context) { logger.LogTransactions(context); } diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatTransactionPhase.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatTransactionPhase.cs index 33cce8a1..5971a096 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatTransactionPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/PipelinePhases/CustomFormatTransactionPhase.cs @@ -1,15 +1,14 @@ using Recyclarr.Cli.Pipelines.CustomFormat.Models; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Common.Extensions; using Recyclarr.Config.Models; using Recyclarr.TrashGuide.CustomFormat; namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; -public class CustomFormatTransactionPhase(ILogger log, IServiceConfiguration config) - : ITransactionPipelinePhase +internal class CustomFormatTransactionPhase(ILogger log, IServiceConfiguration config) + : IPipelinePhase { - public void Execute(CustomFormatPipelineContext context) + public Task Execute(CustomFormatPipelineContext context, CancellationToken ct) { var transactions = new CustomFormatTransactionData(); @@ -57,6 +56,7 @@ public class CustomFormatTransactionPhase(ILogger log, IServiceConfiguration con } context.TransactionOutput = transactions; + return Task.FromResult(true); } private void ProcessExistingCf( diff --git a/src/Recyclarr.Cli/Pipelines/Generic/GenericPipelinePhases.cs b/src/Recyclarr.Cli/Pipelines/Generic/GenericPipelinePhases.cs deleted file mode 100644 index b01a7e64..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/GenericPipelinePhases.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] -public class GenericPipelinePhases - where TContext : IPipelineContext -{ - public required IConfigPipelinePhase ConfigPhase { get; init; } - public required ILogPipelinePhase LogPhase { get; init; } - public required IApiFetchPipelinePhase ApiFetchPhase { get; init; } - public required ITransactionPipelinePhase TransactionPhase { get; init; } - public required IPreviewPipelinePhase PreviewPhase { get; init; } - public required IApiPersistencePipelinePhase ApiPersistencePhase { get; init; } -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/GenericSyncPipeline.cs b/src/Recyclarr.Cli/Pipelines/Generic/GenericSyncPipeline.cs deleted file mode 100644 index 39e79bd0..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/GenericSyncPipeline.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Recyclarr.Cli.Console.Settings; -using Recyclarr.Config.Models; - -namespace Recyclarr.Cli.Pipelines.Generic; - -public class GenericSyncPipeline( - ILogger log, - GenericPipelinePhases phases, - IServiceConfiguration config -) : ISyncPipeline - where TContext : IPipelineContext, new() -{ - public async Task Execute(ISyncSettings settings, CancellationToken ct) - { - var context = new TContext(); - - log.Debug("Executing Pipeline: {Pipeline}", context.PipelineDescription); - - if (!context.SupportedServiceTypes.Contains(config.ServiceType)) - { - log.Debug( - "Skipping this pipeline because it does not support service type {Service}", - config.ServiceType - ); - return; - } - - await phases.ConfigPhase.Execute(context, ct); - if (phases.LogPhase.LogConfigPhaseAndExitIfNeeded(context)) - { - return; - } - - await phases.ApiFetchPhase.Execute(context, ct); - phases.TransactionPhase.Execute(context); - - phases.LogPhase.LogTransactionNotices(context); - - if (settings.Preview) - { - phases.PreviewPhase.Execute(context); - return; - } - - await phases.ApiPersistencePhase.Execute(context, ct); - phases.LogPhase.LogPersistenceResults(context); - } -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/IApiFetchPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/IApiFetchPipelinePhase.cs deleted file mode 100644 index 3543388f..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/IApiFetchPipelinePhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface IApiFetchPipelinePhase - where TContext : IPipelineContext -{ - Task Execute(TContext context, CancellationToken ct); -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/IApiPersistencePipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/IApiPersistencePipelinePhase.cs deleted file mode 100644 index efb5c3e8..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/IApiPersistencePipelinePhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface IApiPersistencePipelinePhase - where TContext : IPipelineContext -{ - Task Execute(TContext context, CancellationToken ct); -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/IConfigPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/IConfigPipelinePhase.cs deleted file mode 100644 index 20b8aa6b..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/IConfigPipelinePhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface IConfigPipelinePhase - where TContext : IPipelineContext -{ - Task Execute(TContext context, CancellationToken ct); -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/ILogPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/ILogPipelinePhase.cs deleted file mode 100644 index e976158e..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/ILogPipelinePhase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface ILogPipelinePhase - where TContext : IPipelineContext -{ - bool LogConfigPhaseAndExitIfNeeded(TContext context); - void LogTransactionNotices(TContext context); - void LogPersistenceResults(TContext context); -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/IPipelineContext.cs b/src/Recyclarr.Cli/Pipelines/Generic/IPipelineContext.cs deleted file mode 100644 index 3b721371..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/IPipelineContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Recyclarr.TrashGuide; - -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface IPipelineContext -{ - string PipelineDescription { get; } - IReadOnlyCollection SupportedServiceTypes { get; } -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/IPreviewPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/IPreviewPipelinePhase.cs deleted file mode 100644 index 9fd509b8..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/IPreviewPipelinePhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface IPreviewPipelinePhase - where TContext : IPipelineContext -{ - void Execute(TContext context); -} diff --git a/src/Recyclarr.Cli/Pipelines/Generic/ITransactionPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/Generic/ITransactionPipelinePhase.cs deleted file mode 100644 index 59b1aa17..00000000 --- a/src/Recyclarr.Cli/Pipelines/Generic/ITransactionPipelinePhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Cli.Pipelines.Generic; - -public interface ITransactionPipelinePhase - where TContext : IPipelineContext -{ - void Execute(TContext context); -} diff --git a/src/Recyclarr.Cli/Pipelines/GenericSyncPipeline.cs b/src/Recyclarr.Cli/Pipelines/GenericSyncPipeline.cs new file mode 100644 index 00000000..3df7a0d2 --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/GenericSyncPipeline.cs @@ -0,0 +1,25 @@ +using Recyclarr.Cli.Console.Settings; + +namespace Recyclarr.Cli.Pipelines; + +internal class GenericSyncPipeline( + ILogger log, + IReadOnlyCollection> phases +) : ISyncPipeline + where TContext : PipelineContext, new() +{ + public async Task Execute(ISyncSettings settings, CancellationToken ct) + { + var context = new TContext { SyncSettings = settings }; + + log.Debug("Executing Pipeline: {Pipeline}", context.PipelineDescription); + + foreach (var phase in phases) + { + if (!await phase.Execute(context, ct)) + { + break; + } + } + } +} diff --git a/src/Recyclarr.Cli/Pipelines/IPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/IPipelinePhase.cs new file mode 100644 index 00000000..5d9bc85d --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/IPipelinePhase.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.Cli.Pipelines; + +internal interface IPipelinePhase +{ + Task Execute(TContext context, CancellationToken ct); +} diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/MediaNamingPipelineContext.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/MediaNamingPipelineContext.cs index ddead1b2..7aa83c06 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/MediaNamingPipelineContext.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/MediaNamingPipelineContext.cs @@ -1,17 +1,13 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; using Recyclarr.ServarrApi.MediaNaming; -using Recyclarr.TrashGuide; namespace Recyclarr.Cli.Pipelines.MediaNaming; -public class MediaNamingPipelineContext : IPipelineContext +internal class MediaNamingPipelineContext : PipelineContext { - public string PipelineDescription => "Media Naming"; - public IReadOnlyCollection SupportedServiceTypes { get; } = - [SupportedServices.Sonarr, SupportedServices.Radarr]; + public override string PipelineDescription => "Media Naming"; - public ProcessedNamingConfig ConfigOutput { get; set; } = default!; - public MediaNamingDto ApiFetchOutput { get; set; } = default!; - public MediaNamingDto TransactionOutput { get; set; } = default!; + public ProcessedNamingConfig ConfigOutput { get; set; } = null!; + public MediaNamingDto ApiFetchOutput { get; set; } = null!; + public MediaNamingDto TransactionOutput { get; set; } = null!; } diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiFetchPhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiFetchPhase.cs index 94a8a60a..ae0b779e 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiFetchPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiFetchPhase.cs @@ -1,13 +1,13 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.MediaNaming; namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; -public class MediaNamingApiFetchPhase(IMediaNamingApiService api) - : IApiFetchPipelinePhase +internal class MediaNamingApiFetchPhase(IMediaNamingApiService api) + : IPipelinePhase { - public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) + public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) { context.ApiFetchOutput = await api.GetNaming(ct); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiPersistencePhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiPersistencePhase.cs index 26f5c433..1684037c 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiPersistencePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingApiPersistencePhase.cs @@ -1,13 +1,41 @@ -using Recyclarr.Cli.Pipelines.Generic; +using Recyclarr.Notifications; using Recyclarr.ServarrApi.MediaNaming; namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; -public class MediaNamingApiPersistencePhase(IMediaNamingApiService api) - : IApiPersistencePipelinePhase +internal class MediaNamingApiPersistencePhase( + ILogger log, + IMediaNamingApiService api, + NotificationEmitter notificationEmitter +) : IPipelinePhase { - public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) + public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) { await api.UpdateNaming(context.TransactionOutput, ct); + LogPersistenceResults(context); + return true; + } + + private void LogPersistenceResults(MediaNamingPipelineContext context) + { + var differences = context.ApiFetchOutput switch + { + RadarrMediaNamingDto x => x.GetDifferences(context.TransactionOutput), + SonarrMediaNamingDto x => x.GetDifferences(context.TransactionOutput), + _ => throw new ArgumentException( + "Unsupported configuration type in LogPersistenceResults method" + ), + }; + + if (differences.Count != 0) + { + log.Information("Media naming has been updated"); + log.Debug("Naming differences: {Diff}", differences); + notificationEmitter.SendStatistic("Media Naming Synced"); + } + else + { + log.Information("Media naming is up to date!"); + } } } diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingConfigPhase.cs index bcaaedc3..b1bd9ebc 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingConfigPhase.cs @@ -1,5 +1,4 @@ using Autofac.Features.Indexed; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config; using Recyclarr.Config.Models; using Recyclarr.ServarrApi.MediaNaming; @@ -16,13 +15,14 @@ public record ProcessedNamingConfig public IReadOnlyCollection InvalidNaming { get; init; } = []; } -public class MediaNamingConfigPhase( +internal class MediaNamingConfigPhase( + ILogger log, IMediaNamingGuideService guide, IIndex configPhaseStrategyFactory, IServiceConfiguration config -) : IConfigPipelinePhase +) : IPipelinePhase { - public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) + public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct) { var lookup = new NamingFormatLookup(); var strategy = configPhaseStrategyFactory[config.ServiceType]; @@ -33,5 +33,44 @@ public class MediaNamingConfigPhase( Dto = dto, InvalidNaming = lookup.Errors, }; + + return LogConfigPhaseAndExitIfNeeded(context); + } + + // Returning 'true' means to exit. 'false' means to proceed. + public bool LogConfigPhaseAndExitIfNeeded(MediaNamingPipelineContext context) + { + var configOutput = context.ConfigOutput; + + if (configOutput.InvalidNaming.Count != 0) + { + foreach (var (topic, invalidValue) in configOutput.InvalidNaming) + { + log.Error( + "An invalid media naming format is specified for {Topic}: {Value}", + topic, + invalidValue + ); + } + + return true; + } + + var differences = configOutput.Dto switch + { + RadarrMediaNamingDto x => x.GetDifferences(new RadarrMediaNamingDto()), + SonarrMediaNamingDto x => x.GetDifferences(new SonarrMediaNamingDto()), + _ => throw new ArgumentException( + "Unsupported configuration type in LogConfigPhase method" + ), + }; + + if (differences.Count == 0) + { + log.Debug("No media naming changes to process"); + return true; + } + + return false; } } diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingLogPhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingLogPhase.cs deleted file mode 100644 index ae6d5587..00000000 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingLogPhase.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Recyclarr.Cli.Pipelines.Generic; -using Recyclarr.Notifications; -using Recyclarr.ServarrApi.MediaNaming; - -namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; - -public class MediaNamingLogPhase(ILogger log, NotificationEmitter notificationEmitter) - : ILogPipelinePhase -{ - // Returning 'true' means to exit. 'false' means to proceed. - public bool LogConfigPhaseAndExitIfNeeded(MediaNamingPipelineContext context) - { - var config = context.ConfigOutput; - - if (config.InvalidNaming.Count != 0) - { - foreach (var (topic, invalidValue) in config.InvalidNaming) - { - log.Error( - "An invalid media naming format is specified for {Topic}: {Value}", - topic, - invalidValue - ); - } - - return true; - } - - var differences = config.Dto switch - { - RadarrMediaNamingDto x => x.GetDifferences(new RadarrMediaNamingDto()), - SonarrMediaNamingDto x => x.GetDifferences(new SonarrMediaNamingDto()), - _ => throw new ArgumentException( - "Unsupported configuration type in LogConfigPhase method" - ), - }; - - if (differences.Count == 0) - { - log.Debug("No media naming changes to process"); - return true; - } - - return false; - } - - public void LogTransactionNotices(MediaNamingPipelineContext context) { } - - public void LogPersistenceResults(MediaNamingPipelineContext context) - { - var differences = context.ApiFetchOutput switch - { - RadarrMediaNamingDto x => x.GetDifferences(context.TransactionOutput), - SonarrMediaNamingDto x => x.GetDifferences(context.TransactionOutput), - _ => throw new ArgumentException( - "Unsupported configuration type in LogPersistenceResults method" - ), - }; - - if (differences.Count != 0) - { - log.Information("Media naming has been updated"); - log.Debug("Naming differences: {Diff}", differences); - notificationEmitter.SendStatistic("Media Naming Synced"); - } - else - { - log.Information("Media naming is up to date!"); - } - } -} diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingPreviewPhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingPreviewPhase.cs index a07f2b00..202a0d72 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingPreviewPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingPreviewPhase.cs @@ -1,15 +1,14 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.MediaNaming; using Spectre.Console; namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; -public class MediaNamingPreviewPhase(IAnsiConsole console) - : IPreviewPipelinePhase +internal class MediaNamingPreviewPhase(IAnsiConsole console) + : PreviewPipelinePhase { private Table? _table; - public void Execute(MediaNamingPipelineContext context) + protected override void RenderPreview(MediaNamingPipelineContext context) { _table = new Table() .Title("Media Naming [red](Preview)[/]") diff --git a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingTransactionPhase.cs b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingTransactionPhase.cs index b55b7a74..cd4f2602 100644 --- a/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingTransactionPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/MediaNaming/PipelinePhases/MediaNamingTransactionPhase.cs @@ -1,20 +1,21 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.MediaNaming; namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; -public class MediaNamingTransactionPhase : ITransactionPipelinePhase +internal class MediaNamingTransactionPhase : IPipelinePhase { - public void Execute(MediaNamingPipelineContext context) + public Task Execute(MediaNamingPipelineContext context, CancellationToken ct) { context.TransactionOutput = context.ApiFetchOutput switch { RadarrMediaNamingDto dto => UpdateRadarrDto(dto, context.ConfigOutput), SonarrMediaNamingDto dto => UpdateSonarrDto(dto, context.ConfigOutput), _ => throw new ArgumentException( - "Config type not supported in media naming transation phase" + "Config type not supported in media naming transaction phase" ), }; + + return Task.FromResult(true); } private static RadarrMediaNamingDto UpdateRadarrDto( diff --git a/src/Recyclarr.Cli/Pipelines/PipelineAutofacModule.cs b/src/Recyclarr.Cli/Pipelines/PipelineAutofacModule.cs index 7c25492b..f2a330ad 100644 --- a/src/Recyclarr.Cli/Pipelines/PipelineAutofacModule.cs +++ b/src/Recyclarr.Cli/Pipelines/PipelineAutofacModule.cs @@ -5,7 +5,6 @@ using Recyclarr.Cli.Pipelines.CustomFormat; using Recyclarr.Cli.Pipelines.CustomFormat.Cache; using Recyclarr.Cli.Pipelines.CustomFormat.Models; using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.MediaNaming; using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases; using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config; @@ -24,7 +23,6 @@ public class PipelineAutofacModule : Module { protected override void Load(ContainerBuilder builder) { - builder.RegisterGeneric(typeof(GenericPipelinePhases<>)); builder.RegisterComposite(); builder .RegisterTypes( @@ -61,8 +59,7 @@ public class PipelineAutofacModule : Module typeof(MediaNamingApiFetchPhase), typeof(MediaNamingTransactionPhase), typeof(MediaNamingPreviewPhase), - typeof(MediaNamingApiPersistencePhase), - typeof(MediaNamingLogPhase) + typeof(MediaNamingApiPersistencePhase) ) .AsImplementedInterfaces(); } @@ -70,6 +67,7 @@ public class PipelineAutofacModule : Module private static void RegisterQualityProfile(ContainerBuilder builder) { builder.RegisterType(); + builder.RegisterType(); builder .RegisterTypes( @@ -77,8 +75,7 @@ public class PipelineAutofacModule : Module typeof(QualityProfilePreviewPhase), typeof(QualityProfileApiFetchPhase), typeof(QualityProfileTransactionPhase), - typeof(QualityProfileApiPersistencePhase), - typeof(QualityProfileLogPhase) + typeof(QualityProfileApiPersistencePhase) ) .AsImplementedInterfaces(); } @@ -104,8 +101,7 @@ public class PipelineAutofacModule : Module typeof(QualitySizePreviewPhase), typeof(QualitySizeApiFetchPhase), typeof(QualitySizeTransactionPhase), - typeof(QualitySizeApiPersistencePhase), - typeof(QualitySizeLogPhase) + typeof(QualitySizeApiPersistencePhase) ) .AsImplementedInterfaces(); } @@ -128,9 +124,9 @@ public class PipelineAutofacModule : Module typeof(CustomFormatApiFetchPhase), typeof(CustomFormatTransactionPhase), typeof(CustomFormatPreviewPhase), - typeof(CustomFormatApiPersistencePhase), - typeof(CustomFormatLogPhase) + typeof(CustomFormatApiPersistencePhase) ) - .AsImplementedInterfaces(); + .AsImplementedInterfaces() + .OrderByRegistration(); } } diff --git a/src/Recyclarr.Cli/Pipelines/PipelineContext.cs b/src/Recyclarr.Cli/Pipelines/PipelineContext.cs new file mode 100644 index 00000000..805f2575 --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/PipelineContext.cs @@ -0,0 +1,12 @@ +using Recyclarr.Cli.Console.Settings; + +namespace Recyclarr.Cli.Pipelines; + +internal abstract class PipelineContext +{ + public abstract string PipelineDescription { get; } + + // Set from `GenericSyncPipeline.Execute()`. Not able to make this `required` because of the + // `new()` constraint. + public ISyncSettings SyncSettings { get; init; } = null!; +} diff --git a/src/Recyclarr.Cli/Pipelines/PreviewPipelinePhase.cs b/src/Recyclarr.Cli/Pipelines/PreviewPipelinePhase.cs new file mode 100644 index 00000000..09f06b3e --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/PreviewPipelinePhase.cs @@ -0,0 +1,18 @@ +namespace Recyclarr.Cli.Pipelines; + +internal abstract class PreviewPipelinePhase : IPipelinePhase + where T : PipelineContext +{ + public Task Execute(T context, CancellationToken ct) + { + if (!context.SyncSettings.Preview) + { + return Task.FromResult(true); + } + + RenderPreview(context); + return Task.FromResult(false); + } + + protected abstract void RenderPreview(T context); +} diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/Models/QualityProfileServiceData.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/Models/QualityProfileServiceData.cs new file mode 100644 index 00000000..60c6847a --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/Models/QualityProfileServiceData.cs @@ -0,0 +1,8 @@ +using Recyclarr.ServarrApi.QualityProfile; + +namespace Recyclarr.Cli.Pipelines.QualityProfile.Models; + +public record QualityProfileServiceData( + IReadOnlyList Profiles, + QualityProfileDto Schema +); diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiFetchPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiFetchPhase.cs index 3fefa2b8..2c6f2aa3 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiFetchPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiFetchPhase.cs @@ -1,20 +1,16 @@ -using Recyclarr.Cli.Pipelines.Generic; +using Recyclarr.Cli.Pipelines.QualityProfile.Models; using Recyclarr.ServarrApi.QualityProfile; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -public record QualityProfileServiceData( - IReadOnlyList Profiles, - QualityProfileDto Schema -); - -public class QualityProfileApiFetchPhase(IQualityProfileApiService api) - : IApiFetchPipelinePhase +internal class QualityProfileApiFetchPhase(IQualityProfileApiService api) + : IPipelinePhase { - public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct) + public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct) { var profiles = await api.GetQualityProfiles(ct); var schema = await api.GetSchema(ct); context.ApiFetchOutput = new QualityProfileServiceData(profiles.AsReadOnly(), schema); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiPersistencePhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiPersistencePhase.cs index 3e2ebfda..0b1ade6b 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiPersistencePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileApiPersistencePhase.cs @@ -1,13 +1,14 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.QualityProfile.Models; using Recyclarr.ServarrApi.QualityProfile; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -public class QualityProfileApiPersistencePhase(IQualityProfileApiService api) - : IApiPersistencePipelinePhase +internal class QualityProfileApiPersistencePhase( + IQualityProfileApiService api, + QualityProfileLogger logger +) : IPipelinePhase { - public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct) + public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct) { var changedProfiles = context.TransactionOutput.ChangedProfiles; foreach (var profile in changedProfiles.Select(x => x.Profile)) @@ -30,5 +31,8 @@ public class QualityProfileApiPersistencePhase(IQualityProfileApiService api) ); } } + + logger.LogPersistenceResults(context); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs index e246392f..4e61314f 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs @@ -1,5 +1,4 @@ using Recyclarr.Cli.Pipelines.CustomFormat.Models; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.QualityProfile.Models; using Recyclarr.Common.Extensions; using Recyclarr.Config.Models; @@ -7,13 +6,13 @@ using Recyclarr.TrashGuide.CustomFormat; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -public class QualityProfileConfigPhase( +internal class QualityProfileConfigPhase( ILogger log, ProcessedCustomFormatCache cache, IServiceConfiguration config -) : IConfigPipelinePhase +) : IPipelinePhase { - public Task Execute(QualityProfilePipelineContext context, CancellationToken ct) + public Task Execute(QualityProfilePipelineContext context, CancellationToken ct) { // 1. For each group of CFs that has a quality profile specified // 2. For each quality profile score config in that CF group @@ -54,7 +53,14 @@ public class QualityProfileConfigPhase( var profilesToReturn = allProfiles.Values.ToList(); PrintDiagnostics(profilesToReturn); context.ConfigOutput = profilesToReturn; - return Task.CompletedTask; + + if (!context.ConfigOutput.Any()) + { + log.Debug("No Quality Profiles to process"); + return Task.FromResult(false); + } + + return Task.FromResult(true); } private void PrintDiagnostics(IEnumerable profiles) diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfilePreviewPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfilePreviewPhase.cs index 4b7c0960..cbf9f8b3 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfilePreviewPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfilePreviewPhase.cs @@ -1,15 +1,14 @@ using System.Globalization; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.QualityProfile; using Spectre.Console; using Spectre.Console.Rendering; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -public class QualityProfilePreviewPhase(IAnsiConsole console) - : IPreviewPipelinePhase +internal class QualityProfilePreviewPhase(IAnsiConsole console) + : PreviewPipelinePhase { - public void Execute(QualityProfilePipelineContext context) + protected override void RenderPreview(QualityProfilePipelineContext context) { var tree = new Tree("Quality Profile Changes [red](Preview)[/]"); diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhase.cs index 48ece28b..ed834c3a 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhase.cs @@ -1,4 +1,3 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.QualityProfile.Models; using Recyclarr.Common.Extensions; using Recyclarr.Common.FluentValidation; @@ -7,10 +6,12 @@ using Recyclarr.ServarrApi.QualityProfile; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -public class QualityProfileTransactionPhase(QualityProfileStatCalculator statCalculator) - : ITransactionPipelinePhase +internal class QualityProfileTransactionPhase( + QualityProfileStatCalculator statCalculator, + QualityProfileLogger logger +) : IPipelinePhase { - public void Execute(QualityProfilePipelineContext context) + public Task Execute(QualityProfilePipelineContext context, CancellationToken ct) { var transactions = new QualityProfileTransactionData(); @@ -25,6 +26,9 @@ public class QualityProfileTransactionPhase(QualityProfileStatCalculator statCal AssignProfiles(transactions, updatedProfiles); context.TransactionOutput = transactions; + + logger.LogTransactionNotices(context); + return Task.FromResult(true); } private void AssignProfiles( diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileLogPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfileLogger.cs similarity index 91% rename from src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileLogPhase.cs rename to src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfileLogger.cs index 64740ea0..acae2f00 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileLogPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfileLogger.cs @@ -1,27 +1,15 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.QualityProfile.Models; using Recyclarr.Common.FluentValidation; using Recyclarr.Notifications; -namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; +namespace Recyclarr.Cli.Pipelines.QualityProfile; -public class QualityProfileLogPhase( +internal class QualityProfileLogger( ILogger log, ValidationLogger validationLogger, NotificationEmitter notificationEmitter -) : ILogPipelinePhase +) { - public bool LogConfigPhaseAndExitIfNeeded(QualityProfilePipelineContext context) - { - if (!context.ConfigOutput.Any()) - { - log.Debug("No Quality Profiles to process"); - return true; - } - - return false; - } - public void LogTransactionNotices(QualityProfilePipelineContext context) { var transactions = context.TransactionOutput; diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfilePipelineContext.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfilePipelineContext.cs index 45f12beb..4f8b80c4 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfilePipelineContext.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/QualityProfilePipelineContext.cs @@ -1,23 +1,12 @@ -using System.Diagnostics.CodeAnalysis; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Cli.Pipelines.QualityProfile.Models; -using Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; -using Recyclarr.TrashGuide; namespace Recyclarr.Cli.Pipelines.QualityProfile; -[SuppressMessage( - "Usage", - "CA2227:Collection properties should be read only", - Justification = "Context objects are similar to DTOs; for usability we want to assign not append" -)] -public class QualityProfilePipelineContext : IPipelineContext +internal class QualityProfilePipelineContext : PipelineContext { - public string PipelineDescription => "Quality Definition"; - public IReadOnlyCollection SupportedServiceTypes { get; } = - [SupportedServices.Sonarr, SupportedServices.Radarr]; + public override string PipelineDescription => "Quality Profile"; - public IList ConfigOutput { get; set; } = default!; - public QualityProfileServiceData ApiFetchOutput { get; set; } = default!; - public QualityProfileTransactionData TransactionOutput { get; set; } = default!; + public IList ConfigOutput { get; set; } = null!; + public QualityProfileServiceData ApiFetchOutput { get; set; } = null!; + public QualityProfileTransactionData TransactionOutput { get; set; } = null!; } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/Models/ProcessedQualitySizeData.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/Models/ProcessedQualitySizeData.cs deleted file mode 100644 index 6966af2d..00000000 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/Models/ProcessedQualitySizeData.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Recyclarr.TrashGuide.QualitySize; - -namespace Recyclarr.Cli.Pipelines.QualitySize.Models; - -public record ProcessedQualitySizeData( - string Type, - IReadOnlyCollection Qualities -); diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs index 1bc81569..1a6f1284 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs @@ -1,13 +1,13 @@ -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.QualityDefinition; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api) - : IApiFetchPipelinePhase +internal class QualitySizeApiFetchPhase(IQualityDefinitionApiService api) + : IPipelinePhase { - public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) + public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) { context.ApiFetchOutput = await api.GetQualityDefinition(ct); + return true; } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs index ecb72fb9..0686be55 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs @@ -1,20 +1,48 @@ -using Recyclarr.Cli.Pipelines.Generic; +using Recyclarr.Notifications; using Recyclarr.ServarrApi.QualityDefinition; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeApiPersistencePhase(ILogger log, IQualityDefinitionApiService api) - : IApiPersistencePipelinePhase +internal class QualitySizeApiPersistencePhase( + ILogger log, + IQualityDefinitionApiService api, + NotificationEmitter notificationEmitter +) : IPipelinePhase { - public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) + public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) { var sizeData = context.TransactionOutput; if (sizeData.Count == 0) { log.Debug("No size data available to persist; skipping API call"); - return; + return false; } await api.UpdateQualityDefinition(sizeData, ct); + LogPersistenceResults(context); + return true; + } + + private void LogPersistenceResults(QualitySizePipelineContext context) + { + var qualityDefinitionName = context.QualitySizeType; + + var totalCount = context.TransactionOutput.Count; + if (totalCount > 0) + { + log.Information( + "Total of {Count} sizes were synced for quality definition {Name}", + totalCount, + qualityDefinitionName + ); + notificationEmitter.SendStatistic("Quality Sizes Synced", totalCount); + } + else + { + log.Information( + "All sizes for quality definition {Name} are already up to date!", + qualityDefinitionName + ); + } } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs index e21a2bdd..75d1015f 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs @@ -1,5 +1,3 @@ -using Recyclarr.Cli.Pipelines.Generic; -using Recyclarr.Cli.Pipelines.QualitySize.Models; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases.Limits; using Recyclarr.Common.Extensions; using Recyclarr.Config.Models; @@ -7,31 +5,35 @@ using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeConfigPhase( +internal class QualitySizeConfigPhase( ILogger log, IQualitySizeGuideService guide, IServiceConfiguration config, IQualityItemLimitFactory limitFactory -) : IConfigPipelinePhase +) : IPipelinePhase { - public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) + public async Task Execute(QualitySizePipelineContext context, CancellationToken ct) { var configSizeData = config.QualityDefinition; if (configSizeData is null) { log.Debug("{Instance} has no quality definition", config.InstanceName); - return; + return false; } + ClampPreferredRatio(configSizeData); + var guideSizeData = guide .GetQualitySizeData(config.ServiceType) .FirstOrDefault(x => x.Type.EqualsIgnoreCase(configSizeData.Type)); if (guideSizeData == null) { - context.ConfigError = - $"The specified quality definition type does not exist: {configSizeData.Type}"; - return; + log.Error( + "The specified quality definition type does not exist: {Type}", + configSizeData.Type + ); + return false; } var itemLimits = await limitFactory.Create(config.ServiceType, ct); @@ -40,11 +42,37 @@ public class QualitySizeConfigPhase( .Qualities.Select(x => new QualityItemWithLimits(x, itemLimits)) .ToList(); + if (sizeDataWithThresholds.Count == 0) + { + log.Debug("No Quality Definitions to process"); + return false; + } + AdjustPreferredRatio(configSizeData, sizeDataWithThresholds); - context.ConfigOutput = new ProcessedQualitySizeData( - configSizeData.Type, - sizeDataWithThresholds + + context.QualitySizeType = configSizeData.Type; + context.Qualities = sizeDataWithThresholds; + return true; + } + + private void ClampPreferredRatio(QualityDefinitionConfig configSizeData) + { + if (configSizeData.PreferredRatio is not (< 0 or > 1)) + { + return; + } + + // Fix an out of range ratio and warn the user + var clampedRatio = Math.Clamp(configSizeData.PreferredRatio.Value, 0, 1); + + log.Warning( + "Your `preferred_ratio` of {CurrentRatio} is out of range. " + + "It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}", + configSizeData.PreferredRatio, + clampedRatio ); + + configSizeData.PreferredRatio = clampedRatio; } private void AdjustPreferredRatio( @@ -61,20 +89,6 @@ public class QualitySizeConfigPhase( "Using an explicit preferred ratio which will override values from the guide" ); - // Fix an out of range ratio and warn the user - if (configSizeData.PreferredRatio is < 0 or > 1) - { - var clampedRatio = Math.Clamp(configSizeData.PreferredRatio.Value, 0, 1); - log.Warning( - "Your `preferred_ratio` of {CurrentRatio} is out of range. " - + "It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}", - configSizeData.PreferredRatio, - clampedRatio - ); - - configSizeData.PreferredRatio = clampedRatio; - } - // Apply a calculated preferred size foreach (var quality in guideSizeData) { diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs deleted file mode 100644 index dd5ade5f..00000000 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Recyclarr.Cli.Pipelines.Generic; -using Recyclarr.Notifications; - -namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; - -public class QualitySizeLogPhase(ILogger log, NotificationEmitter notificationEmitter) - : ILogPipelinePhase -{ - public bool LogConfigPhaseAndExitIfNeeded(QualitySizePipelineContext context) - { - if (context.ConfigError is not null) - { - log.Error(context.ConfigError); - return true; - } - - if (context.ConfigOutput is not { Qualities.Count: > 0 }) - { - log.Debug("No Quality Definitions to process"); - return true; - } - - return false; - } - - public void LogTransactionNotices(QualitySizePipelineContext context) { } - - public void LogPersistenceResults(QualitySizePipelineContext context) - { - // Do not check ConfigOutput for null here since that is done for us in the LogConfigPhase method - var qualityDefinitionName = context.ConfigOutput!.Type; - - var totalCount = context.TransactionOutput.Count; - if (totalCount > 0) - { - log.Information( - "Total of {Count} sizes were synced for quality definition {Name}", - totalCount, - qualityDefinitionName - ); - notificationEmitter.SendStatistic("Quality Sizes Synced", totalCount); - } - else - { - log.Information( - "All sizes for quality definition {Name} are already up to date!", - qualityDefinitionName - ); - } - } -} diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs index 268ae21b..0ed19601 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs @@ -1,12 +1,11 @@ -using Recyclarr.Cli.Pipelines.Generic; using Spectre.Console; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizePreviewPhase(IAnsiConsole console) - : IPreviewPipelinePhase +internal class QualitySizePreviewPhase(IAnsiConsole console) + : PreviewPipelinePhase { - public void Execute(QualitySizePipelineContext context) + protected override void RenderPreview(QualitySizePipelineContext context) { var table = new Table(); @@ -16,8 +15,8 @@ public class QualitySizePreviewPhase(IAnsiConsole console) table.AddColumn("[bold]Max[/]"); table.AddColumn("[bold]Preferred[/]"); - // Do not check ConfigOutput for null here since the LogPhase checks that for us - foreach (var q in context.ConfigOutput!.Qualities) + // Do not check ConfigOutput for null here since the Config Phase checks that for us + foreach (var q in context.Qualities) { var quality = $"[dodgerblue1]{q.Item.Quality}[/]"; table.AddRow(quality, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); @@ -25,5 +24,6 @@ public class QualitySizePreviewPhase(IAnsiConsole console) console.WriteLine(); console.Write(table); + throw new NotImplementedException(); } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs index 1852f470..13aa3f6f 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs @@ -1,17 +1,15 @@ using System.Collections.ObjectModel; -using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeTransactionPhase(ILogger log) - : ITransactionPipelinePhase +internal class QualitySizeTransactionPhase(ILogger log) : IPipelinePhase { - public void Execute(QualitySizePipelineContext context) + public Task Execute(QualitySizePipelineContext context, CancellationToken ct) { - // Do not check ConfigOutput for null since the LogPhase does it for us - var guideQuality = context.ConfigOutput!.Qualities; + // Do not check ConfigOutput for null since the Config Phase does it for us + var guideQuality = context.Qualities; var serverQuality = context.ApiFetchOutput; var newQuality = new Collection(); @@ -62,6 +60,7 @@ public class QualitySizeTransactionPhase(ILogger log) } context.TransactionOutput = newQuality; + return Task.FromResult(true); } private static bool QualityIsDifferent(ServiceQualityDefinitionItem a, QualityItemWithLimits b) diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs index 67c03b71..b1263eaf 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Recyclarr.Cli.Pipelines.Generic; -using Recyclarr.Cli.Pipelines.QualitySize.Models; using Recyclarr.ServarrApi.QualityDefinition; -using Recyclarr.TrashGuide; +using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize; @@ -11,14 +9,12 @@ namespace Recyclarr.Cli.Pipelines.QualitySize; "CA2227:Collection properties should be read only", Justification = "Context objects are similar to DTOs; for usability we want to assign not append" )] -public class QualitySizePipelineContext : IPipelineContext +internal class QualitySizePipelineContext : PipelineContext { - public string PipelineDescription => "Quality Definition"; - public IReadOnlyCollection SupportedServiceTypes { get; } = - [SupportedServices.Sonarr, SupportedServices.Radarr]; + public override string PipelineDescription => "Quality Definition"; - public ProcessedQualitySizeData? ConfigOutput { get; set; } - public IList ApiFetchOutput { get; set; } = default!; - public IList TransactionOutput { get; set; } = default!; - public string? ConfigError { get; set; } + public string QualitySizeType { get; set; } = ""; + public IReadOnlyCollection Qualities { get; set; } = []; + public IList ApiFetchOutput { get; set; } = null!; + public IList TransactionOutput { get; set; } = null!; } diff --git a/src/Recyclarr.Cli/Processors/Sync/CompositeSyncPipeline.cs b/src/Recyclarr.Cli/Processors/Sync/CompositeSyncPipeline.cs index 7af78142..c43d0b7b 100644 --- a/src/Recyclarr.Cli/Processors/Sync/CompositeSyncPipeline.cs +++ b/src/Recyclarr.Cli/Processors/Sync/CompositeSyncPipeline.cs @@ -5,7 +5,7 @@ using Recyclarr.Config.Models; namespace Recyclarr.Cli.Processors.Sync; -public class CompositeSyncPipeline( +internal class CompositeSyncPipeline( ILogger log, IOrderedEnumerable pipelines, IEnumerable caches, diff --git a/tests/Recyclarr.Cli.IntegrationTests/CompositionRootTest.cs b/tests/Recyclarr.Cli.IntegrationTests/CompositionRootTest.cs index 46e7b701..dd10a69a 100644 --- a/tests/Recyclarr.Cli.IntegrationTests/CompositionRootTest.cs +++ b/tests/Recyclarr.Cli.IntegrationTests/CompositionRootTest.cs @@ -12,6 +12,7 @@ namespace Recyclarr.Cli.IntegrationTests; [TestFixture] public class CompositionRootTest { + [UsedImplicitly] private sealed class ConcreteTypeEnumerator : IEnumerable { public IEnumerator GetEnumerator() diff --git a/tests/Recyclarr.Cli.IntegrationTests/CustomFormatTransactionPhaseTest.cs b/tests/Recyclarr.Cli.IntegrationTests/CustomFormatTransactionPhaseTest.cs index e7901fb1..4f93ca8c 100644 --- a/tests/Recyclarr.Cli.IntegrationTests/CustomFormatTransactionPhaseTest.cs +++ b/tests/Recyclarr.Cli.IntegrationTests/CustomFormatTransactionPhaseTest.cs @@ -13,7 +13,7 @@ namespace Recyclarr.Cli.IntegrationTests; internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture { [Test] - public void Add_new_cf() + public async Task Add_new_cf() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start(NewConfig.Radarr()); @@ -26,7 +26,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("one", "cf1")], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -36,7 +36,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Update_cf_by_matching_name() + public async Task Update_cf_by_matching_name() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start(NewConfig.Radarr()); @@ -58,7 +58,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -77,7 +77,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Update_cf_by_matching_id_different_names() + public async Task Update_cf_by_matching_id_different_names() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start(NewConfig.Radarr()); @@ -99,7 +99,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -118,7 +118,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Update_cf_by_matching_id_same_names() + public async Task Update_cf_by_matching_id_same_names() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -146,7 +146,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -165,7 +165,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Conflicting_cf_when_new_cf_has_name_of_existing() + public async Task Conflicting_cf_when_new_cf_has_name_of_existing() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -188,7 +188,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("one", "cf1")], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -204,7 +204,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Conflicting_cf_when_cached_cf_has_name_of_existing() + public async Task Conflicting_cf_when_cached_cf_has_name_of_existing() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -227,7 +227,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("one", "cf1")], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -243,7 +243,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Updated_cf_with_matching_name_and_id() + public async Task Updated_cf_with_matching_name_and_id() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -275,7 +275,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -294,7 +294,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Unchanged_cfs_with_replace_enabled() + public async Task Unchanged_cfs_with_replace_enabled() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -313,7 +313,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("one", "cf1")], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -326,7 +326,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Unchanged_cfs_without_replace() + public async Task Unchanged_cfs_without_replace() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -345,7 +345,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("one", "cf1")], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -358,7 +358,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void Deleted_cfs_when_enabled() + public async Task Deleted_cfs_when_enabled() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -377,7 +377,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -390,7 +390,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture } [Test] - public void No_deleted_cfs_when_disabled() + public async Task No_deleted_cfs_when_disabled() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start( @@ -409,13 +409,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData()); } [Test] - public void Do_not_delete_cfs_in_config() + public async Task Do_not_delete_cfs_in_config() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start(NewConfig.Radarr()); @@ -429,13 +429,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("two", "cf2", 2)], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context.TransactionOutput.DeletedCustomFormats.Should().BeEmpty(); } [Test] - public void Add_new_cf_when_in_cache_but_not_in_service() + public async Task Add_new_cf_when_in_cache_but_not_in_service() { var scopeFactory = Resolve(); using var scope = scopeFactory.Start(NewConfig.Radarr()); @@ -449,7 +449,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture ConfigOutput = [NewCf.Data("two", "cf2", 2)], }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseRadarrTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseRadarrTest.cs index dfdf59bd..e51ff467 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseRadarrTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseRadarrTest.cs @@ -11,10 +11,10 @@ namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming; "CA2000:Dispose objects before losing scope", Justification = "Do not care about disposal in a testing context" )] -public class MediaNamingTransactionPhaseRadarrTest +internal class MediaNamingTransactionPhaseRadarrTest { [Test, AutoMockData] - public void Radarr_left_null(MediaNamingTransactionPhase sut) + public async Task Radarr_left_null(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -30,7 +30,7 @@ public class MediaNamingTransactionPhaseRadarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -38,7 +38,7 @@ public class MediaNamingTransactionPhaseRadarrTest } [Test, AutoMockData] - public void Radarr_right_null(MediaNamingTransactionPhase sut) + public async Task Radarr_right_null(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -51,7 +51,7 @@ public class MediaNamingTransactionPhaseRadarrTest ConfigOutput = new ProcessedNamingConfig { Dto = new RadarrMediaNamingDto() }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -59,7 +59,7 @@ public class MediaNamingTransactionPhaseRadarrTest } [Test, AutoMockData] - public void Radarr_right_and_left_with_rename(MediaNamingTransactionPhase sut) + public async Task Radarr_right_and_left_with_rename(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -80,7 +80,7 @@ public class MediaNamingTransactionPhaseRadarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -88,7 +88,7 @@ public class MediaNamingTransactionPhaseRadarrTest } [Test, AutoMockData] - public void Radarr_right_and_left_without_rename(MediaNamingTransactionPhase sut) + public async Task Radarr_right_and_left_without_rename(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -109,7 +109,7 @@ public class MediaNamingTransactionPhaseRadarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseSonarrTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseSonarrTest.cs index 291d7a46..3fdbc633 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseSonarrTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/MediaNaming/MediaNamingTransactionPhaseSonarrTest.cs @@ -7,10 +7,10 @@ namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming; [TestFixture] [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] -public class MediaNamingTransactionPhaseSonarrTest +internal sealed class MediaNamingTransactionPhaseSonarrTest { [Test, AutoMockData] - public void Sonarr_left_null(MediaNamingTransactionPhase sut) + public async Task Sonarr_left_null(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -29,7 +29,7 @@ public class MediaNamingTransactionPhaseSonarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -37,7 +37,7 @@ public class MediaNamingTransactionPhaseSonarrTest } [Test, AutoMockData] - public void Sonarr_right_null(MediaNamingTransactionPhase sut) + public async Task Sonarr_right_null(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -53,7 +53,7 @@ public class MediaNamingTransactionPhaseSonarrTest ConfigOutput = new ProcessedNamingConfig { Dto = new SonarrMediaNamingDto() }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -61,7 +61,7 @@ public class MediaNamingTransactionPhaseSonarrTest } [Test, AutoMockData] - public void Sonarr_right_and_left_with_rename(MediaNamingTransactionPhase sut) + public async Task Sonarr_right_and_left_with_rename(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -88,7 +88,7 @@ public class MediaNamingTransactionPhaseSonarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -96,7 +96,7 @@ public class MediaNamingTransactionPhaseSonarrTest } [Test, AutoMockData] - public void Sonarr_right_and_left_without_rename(MediaNamingTransactionPhase sut) + public async Task Sonarr_right_and_left_without_rename(MediaNamingTransactionPhase sut) { var context = new MediaNamingPipelineContext { @@ -123,7 +123,7 @@ public class MediaNamingTransactionPhaseSonarrTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhaseTest.cs index 2dd2583c..6e9ba56b 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileTransactionPhaseTest.cs @@ -7,10 +7,10 @@ using Recyclarr.ServarrApi.QualityProfile; namespace Recyclarr.Cli.Tests.Pipelines.QualityProfile.PipelinePhases; [TestFixture] -public class QualityProfileTransactionPhaseTest +internal class QualityProfileTransactionPhaseTest { [Test, AutoMockData] - public void Non_existent_profile_names_mixed_with_valid_profiles( + public async Task Non_existent_profile_names_mixed_with_valid_profiles( QualityProfileTransactionPhase sut ) { @@ -29,7 +29,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -54,7 +54,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void New_profiles(QualityProfileTransactionPhase sut) + public async Task New_profiles(QualityProfileTransactionPhase sut) { var dtos = new[] { new QualityProfileDto { Name = "irrelevant_profile" } }; @@ -89,7 +89,7 @@ public class QualityProfileTransactionPhaseTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should() @@ -129,7 +129,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Updated_scores(QualityProfileTransactionPhase sut) + public async Task Updated_scores(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -160,7 +160,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.ChangedProfiles.Should() @@ -176,7 +176,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void No_updated_profiles_when_no_custom_formats(QualityProfileTransactionPhase sut) + public async Task No_updated_profiles_when_no_custom_formats(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -207,13 +207,13 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context.TransactionOutput.Should().BeEquivalentTo(new QualityProfileTransactionData()); } [Test, AutoMockData] - public void Unchanged_scores(QualityProfileTransactionPhase sut) + public async Task Unchanged_scores(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -246,7 +246,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.UnchangedProfiles.Should() @@ -262,7 +262,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Reset_scores_with_reset_unmatched_true(QualityProfileTransactionPhase sut) + public async Task Reset_scores_with_reset_unmatched_true(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -301,7 +301,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.ChangedProfiles.Should() @@ -319,7 +319,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Reset_scores_with_reset_unmatched_false(QualityProfileTransactionPhase sut) + public async Task Reset_scores_with_reset_unmatched_false(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -355,8 +355,8 @@ public class QualityProfileTransactionPhaseTest ResetUnmatchedScores = new ResetUnmatchedScoresConfig { Enabled = false, - // Throw in some exceptions here, just to test whether or not these somehow affect the - // result despite Enable being set to false. + // Throw in some exceptions here, just to test whether these somehow + // affect the result despite Enable being set to false. Except = ["cf1"], }, }, @@ -367,7 +367,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.ChangedProfiles.Should() @@ -385,7 +385,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Reset_scores_with_reset_exceptions(QualityProfileTransactionPhase sut) + public async Task Reset_scores_with_reset_exceptions(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -431,7 +431,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.ChangedProfiles.Should() @@ -449,7 +449,9 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Reset_scores_with_invalid_except_list_items(QualityProfileTransactionPhase sut) + public async Task Reset_scores_with_invalid_except_list_items( + QualityProfileTransactionPhase sut + ) { var dtos = new[] { @@ -493,7 +495,7 @@ public class QualityProfileTransactionPhaseTest ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto()), }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.ChangedProfiles.Should() @@ -503,7 +505,7 @@ public class QualityProfileTransactionPhaseTest } [Test, AutoMockData] - public void Missing_required_qualities_are_readded(QualityProfileTransactionPhase sut) + public async Task Missing_required_qualities_are_readded(QualityProfileTransactionPhase sut) { var dtos = new[] { @@ -556,7 +558,7 @@ public class QualityProfileTransactionPhaseTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); var profiles = context.TransactionOutput.ChangedProfiles; profiles.Should().ContainSingle(); diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs index 951da5b0..a9e89e06 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs @@ -9,7 +9,7 @@ using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases; [TestFixture] -public class QualitySizeConfigPhaseTest +internal class QualitySizeConfigPhaseTest { [Test, AutoMockData] public async Task Do_nothing_if_no_quality_definition( @@ -22,7 +22,8 @@ public class QualitySizeConfigPhaseTest await sut.Execute(context, CancellationToken.None); - context.ConfigOutput.Should().BeNull(); + context.QualitySizeType.Should().BeEmpty(); + context.Qualities.Should().BeEmpty(); } [Test, AutoMockData] @@ -42,7 +43,8 @@ public class QualitySizeConfigPhaseTest await sut.Execute(context, CancellationToken.None); - context.ConfigOutput.Should().BeNull(); + context.QualitySizeType.Should().BeEmpty(); + context.Qualities.Should().BeEmpty(); } [Test] @@ -106,9 +108,8 @@ public class QualitySizeConfigPhaseTest await sut.Execute(context, CancellationToken.None); - context.ConfigOutput.Should().NotBeNull(); context - .ConfigOutput!.Qualities.Select(x => x.Item) + .Qualities.Select(x => x.Item) .Should() .BeEquivalentTo([new QualityItem("quality1", 0, 100, 50)]); } @@ -139,9 +140,8 @@ public class QualitySizeConfigPhaseTest await sut.Execute(context, CancellationToken.None); - context.ConfigOutput.Should().NotBeNull(); context - .ConfigOutput!.Qualities.Select(x => x.Item) + .Qualities.Select(x => x.Item) .Should() .BeEquivalentTo([new QualityItem("quality1", 0, 100, 90)]); } diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs index da4aefe8..100a9cc1 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs @@ -1,5 +1,4 @@ using Recyclarr.Cli.Pipelines.QualitySize; -using Recyclarr.Cli.Pipelines.QualitySize.Models; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.Tests.TestLibrary; @@ -7,45 +6,43 @@ using Recyclarr.Tests.TestLibrary; namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases; [TestFixture] -public class QualitySizeTransactionPhaseTest +internal class QualitySizeTransactionPhaseTest { [Test, AutoMockData] - public void Skip_guide_qualities_that_do_not_exist_in_service(QualitySizeTransactionPhase sut) + public async Task Skip_guide_qualities_that_do_not_exist_in_service( + QualitySizeTransactionPhase sut + ) { var context = new QualitySizePipelineContext { - ConfigOutput = new ProcessedQualitySizeData( - "", - [ - NewQualitySize.WithLimits("non_existent1", 0, 2, 1), - NewQualitySize.WithLimits("non_existent2", 0, 2, 1), - ] - ), + Qualities = + [ + NewQualitySize.WithLimits("non_existent1", 0, 2, 1), + NewQualitySize.WithLimits("non_existent2", 0, 2, 1), + ], ApiFetchOutput = new List { new() { Quality = new ServiceQualityItem { Name = "exists" } }, }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context.TransactionOutput.Should().BeEmpty(); } [Test, AutoMockData] - public void Skip_guide_qualities_that_are_not_different_from_service( + public async Task Skip_guide_qualities_that_are_not_different_from_service( QualitySizeTransactionPhase sut ) { var context = new QualitySizePipelineContext { - ConfigOutput = new ProcessedQualitySizeData( - "", - [ - NewQualitySize.WithLimits("same1", 0, 2, 1), - NewQualitySize.WithLimits("same2", 0, 2, 1), - ] - ), + Qualities = + [ + NewQualitySize.WithLimits("same1", 0, 2, 1), + NewQualitySize.WithLimits("same2", 0, 2, 1), + ], ApiFetchOutput = new List { new() @@ -65,25 +62,23 @@ public class QualitySizeTransactionPhaseTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context.TransactionOutput.Should().BeEmpty(); } [Test, AutoMockData] - public void Sync_guide_qualities_that_are_different_from_service( + public async Task Sync_guide_qualities_that_are_different_from_service( QualitySizeTransactionPhase sut ) { var context = new QualitySizePipelineContext { - ConfigOutput = new ProcessedQualitySizeData( - "", - [ - NewQualitySize.WithLimits("same1", 0, 2, 1), - NewQualitySize.WithLimits("different1", 0, 3, 1), - ] - ), + Qualities = + [ + NewQualitySize.WithLimits("same1", 0, 2, 1), + NewQualitySize.WithLimits("different1", 0, 3, 1), + ], ApiFetchOutput = new List { new() @@ -103,7 +98,7 @@ public class QualitySizeTransactionPhaseTest }, }; - sut.Execute(context); + await sut.Execute(context, CancellationToken.None); context .TransactionOutput.Should()