parent
175aa6733b
commit
5bb2bfa8a0
@ -0,0 +1,45 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Startup;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class ConfigIncludeProcessor : IIncludeProcessor
|
||||||
|
{
|
||||||
|
private readonly IFileSystem _fs;
|
||||||
|
private readonly IAppPaths _paths;
|
||||||
|
|
||||||
|
public ConfigIncludeProcessor(IFileSystem fs, IAppPaths paths)
|
||||||
|
{
|
||||||
|
_fs = fs;
|
||||||
|
_paths = paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanProcess(IYamlInclude includeDirective)
|
||||||
|
{
|
||||||
|
return includeDirective is ConfigYamlInclude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType)
|
||||||
|
{
|
||||||
|
var include = (ConfigYamlInclude) includeDirective;
|
||||||
|
|
||||||
|
if (include.Config is null)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException("`config` property is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var rooted = _fs.Path.IsPathRooted(include.Config);
|
||||||
|
|
||||||
|
var configFile = rooted
|
||||||
|
? _fs.FileInfo.New(include.Config)
|
||||||
|
: _paths.ConfigsDirectory.File(include.Config);
|
||||||
|
|
||||||
|
if (!configFile.Exists)
|
||||||
|
{
|
||||||
|
var pathType = rooted ? "Absolute" : "Relative";
|
||||||
|
throw new YamlIncludeException($"{pathType} include path does not exist: {configFile.FullName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return configFile;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public interface IIncludeProcessor
|
||||||
|
{
|
||||||
|
bool CanProcess(IYamlInclude includeDirective);
|
||||||
|
IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType);
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public interface IYamlIncludeResolver
|
||||||
|
{
|
||||||
|
IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Yaml;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class PolymorphicIncludeYamlBehavior : IYamlBehavior
|
||||||
|
{
|
||||||
|
public void Setup(DeserializerBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithTypeDiscriminatingNodeDeserializer(o => o
|
||||||
|
.AddUniqueKeyTypeDiscriminator<IYamlInclude>(new Dictionary<string, Type>
|
||||||
|
{
|
||||||
|
[nameof(ConfigYamlInclude.Config).ToSnakeCase()] = typeof(ConfigYamlInclude),
|
||||||
|
[nameof(TemplateYamlInclude.Template).ToSnakeCase()] = typeof(TemplateYamlInclude)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class RadarrConfigMerger : ServiceConfigMerger<RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "WithExpressionModifiesAllMembers")]
|
||||||
|
public abstract class ServiceConfigMerger<T> where T : ServiceConfigYaml
|
||||||
|
{
|
||||||
|
protected static TVal? Combine<TVal>(TVal? a, TVal? b, Func<TVal, TVal, TVal?> combine)
|
||||||
|
{
|
||||||
|
if (b is null)
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a is null)
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return combine(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual T Merge(T a, T b)
|
||||||
|
{
|
||||||
|
return a with
|
||||||
|
{
|
||||||
|
CustomFormats = Combine(a.CustomFormats, b.CustomFormats, (x, y) => x.Concat(y).ToList()),
|
||||||
|
QualityProfiles = MergeQualityProfiles(a.QualityProfiles, b.QualityProfiles),
|
||||||
|
QualityDefinition = Combine(a.QualityDefinition, b.QualityDefinition,
|
||||||
|
(x, y) => x with
|
||||||
|
{
|
||||||
|
Type = y.Type ?? x.Type,
|
||||||
|
PreferredRatio = y.PreferredRatio ?? x.PreferredRatio
|
||||||
|
}),
|
||||||
|
|
||||||
|
DeleteOldCustomFormats =
|
||||||
|
b.DeleteOldCustomFormats ?? a.DeleteOldCustomFormats,
|
||||||
|
|
||||||
|
ReplaceExistingCustomFormats =
|
||||||
|
b.ReplaceExistingCustomFormats ?? a.ReplaceExistingCustomFormats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyCollection<QualityProfileConfigYaml>? MergeQualityProfiles(
|
||||||
|
IReadOnlyCollection<QualityProfileConfigYaml>? a,
|
||||||
|
IReadOnlyCollection<QualityProfileConfigYaml>? b)
|
||||||
|
{
|
||||||
|
return Combine(a, b, (x1, y1) =>
|
||||||
|
{
|
||||||
|
return x1
|
||||||
|
.FullOuterJoin(y1, JoinType.Hash,
|
||||||
|
x => x.Name,
|
||||||
|
x => x.Name,
|
||||||
|
l => l,
|
||||||
|
r => r,
|
||||||
|
MergeQualityProfile,
|
||||||
|
StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QualityProfileConfigYaml MergeQualityProfile(QualityProfileConfigYaml a, QualityProfileConfigYaml b)
|
||||||
|
{
|
||||||
|
return a with
|
||||||
|
{
|
||||||
|
Upgrade = Combine(a.Upgrade, b.Upgrade, (x1, y1) => x1 with
|
||||||
|
{
|
||||||
|
Allowed = y1.Allowed ?? x1.Allowed,
|
||||||
|
UntilQuality = y1.UntilQuality ?? x1.UntilQuality,
|
||||||
|
UntilScore = y1.UntilScore ?? x1.UntilScore
|
||||||
|
}),
|
||||||
|
MinFormatScore = b.MinFormatScore ?? a.MinFormatScore,
|
||||||
|
QualitySort = b.QualitySort ?? a.QualitySort,
|
||||||
|
ScoreSet = b.ScoreSet ?? a.ScoreSet,
|
||||||
|
ResetUnmatchedScores = Combine(a.ResetUnmatchedScores, b.ResetUnmatchedScores, (x1, y1) => x1 with
|
||||||
|
{
|
||||||
|
Enabled = y1.Enabled ?? x1.Enabled,
|
||||||
|
Except = Combine(x1.Except, y1.Except, (x2, y2) => Combine(x2, y2, (x3, y3) => x3
|
||||||
|
.Concat(y3)
|
||||||
|
.Distinct(StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.ToList()))
|
||||||
|
}),
|
||||||
|
Qualities = Combine(a.Qualities, b.Qualities, (x1, y1) => x1.Concat(y1).ToList())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class SonarrConfigMerger : ServiceConfigMerger<SonarrConfigYaml>
|
||||||
|
{
|
||||||
|
public override SonarrConfigYaml Merge(SonarrConfigYaml a, SonarrConfigYaml b)
|
||||||
|
{
|
||||||
|
return base.Merge(a, b) with
|
||||||
|
{
|
||||||
|
ReleaseProfiles = Combine(a.ReleaseProfiles, b.ReleaseProfiles,
|
||||||
|
(x, y) => x.Concat(y).ToList())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class TemplateIncludeProcessor : IIncludeProcessor
|
||||||
|
{
|
||||||
|
private readonly IConfigTemplateGuideService _templates;
|
||||||
|
|
||||||
|
public TemplateIncludeProcessor(IConfigTemplateGuideService templates)
|
||||||
|
{
|
||||||
|
_templates = templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanProcess(IYamlInclude includeDirective)
|
||||||
|
{
|
||||||
|
return includeDirective is TemplateYamlInclude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType)
|
||||||
|
{
|
||||||
|
var include = (TemplateYamlInclude) includeDirective;
|
||||||
|
|
||||||
|
if (include.Template is null)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException("`template` property is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var includePath = _templates.GetIncludeData()
|
||||||
|
.Where(x => x.Service == serviceType)
|
||||||
|
.FirstOrDefault(x => x.Id.EqualsIgnoreCase(include.Template));
|
||||||
|
|
||||||
|
if (includePath is null)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException(
|
||||||
|
$"For service type '{serviceType}', unable to find config template with name '{include.Template}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return includePath.TemplateFile;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification =
|
||||||
|
"Used for type-discriminating node deserializer")]
|
||||||
|
public interface IYamlInclude
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||||
|
public record ConfigYamlInclude : IYamlInclude
|
||||||
|
{
|
||||||
|
public string? Config { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||||
|
public record TemplateYamlInclude : IYamlInclude
|
||||||
|
{
|
||||||
|
public string? Template { get; init; }
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class YamlIncludeException : Exception
|
||||||
|
{
|
||||||
|
public YamlIncludeException(string? message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
public class YamlIncludeResolver : IYamlIncludeResolver
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyCollection<IIncludeProcessor> _includeProcessors;
|
||||||
|
|
||||||
|
public YamlIncludeResolver(IReadOnlyCollection<IIncludeProcessor> includeProcessors)
|
||||||
|
{
|
||||||
|
_includeProcessors = includeProcessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType)
|
||||||
|
{
|
||||||
|
var processor = _includeProcessors.FirstOrDefault(x => x.CanProcess(includeType));
|
||||||
|
if (processor is null)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException("Include type is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamlFile = processor.GetPathToConfig(includeType, serviceType);
|
||||||
|
if (!yamlFile.Exists)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException($"Included YAML file does not exist: {yamlFile.FullName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return yamlFile;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing;
|
||||||
|
|
||||||
|
public class IncludePostProcessor : IConfigPostProcessor
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly ConfigParser _parser;
|
||||||
|
private readonly ConfigValidationExecutor _validator;
|
||||||
|
private readonly IYamlIncludeResolver _includeResolver;
|
||||||
|
|
||||||
|
public IncludePostProcessor(
|
||||||
|
ILogger log,
|
||||||
|
ConfigParser parser,
|
||||||
|
ConfigValidationExecutor validator,
|
||||||
|
IYamlIncludeResolver includeResolver)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_parser = parser;
|
||||||
|
_validator = validator;
|
||||||
|
_includeResolver = includeResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootConfigYaml Process(RootConfigYaml config)
|
||||||
|
{
|
||||||
|
return new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = ProcessIncludes(config.Radarr, new RadarrConfigMerger(), SupportedServices.Radarr),
|
||||||
|
Sonarr = ProcessIncludes(config.Sonarr, new SonarrConfigMerger(), SupportedServices.Sonarr)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<string, T>? ProcessIncludes<T>(
|
||||||
|
IReadOnlyDictionary<string, T>? configs,
|
||||||
|
ServiceConfigMerger<T> merger,
|
||||||
|
SupportedServices serviceType)
|
||||||
|
where T : ServiceConfigYaml, new()
|
||||||
|
{
|
||||||
|
if (configs is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedConfigs = new Dictionary<string, T>();
|
||||||
|
|
||||||
|
foreach (var (key, config) in configs)
|
||||||
|
{
|
||||||
|
if (config.Include is null)
|
||||||
|
{
|
||||||
|
mergedConfigs.Add(key, config);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all includes together first
|
||||||
|
var aggregateInclude = config.Include
|
||||||
|
.Select(x => LoadYamlInclude<T>(x, serviceType))
|
||||||
|
.Aggregate(new T(), merger.Merge);
|
||||||
|
|
||||||
|
// Merge the config into the aggregated includes so that root config values overwrite included values.
|
||||||
|
mergedConfigs.Add(key, merger.Merge(aggregateInclude, config) with
|
||||||
|
{
|
||||||
|
// No reason to keep these around anymore now that they have been merged
|
||||||
|
Include = null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T LoadYamlInclude<T>(IYamlInclude includeType, SupportedServices serviceType)
|
||||||
|
where T : ServiceConfigYaml
|
||||||
|
{
|
||||||
|
var yamlFile = _includeResolver.GetIncludePath(includeType, serviceType);
|
||||||
|
using var logScope = LogContext.PushProperty(LogProperty.Scope, $"Include {yamlFile.Name}");
|
||||||
|
|
||||||
|
var configToMerge = _parser.Load<T>(yamlFile);
|
||||||
|
if (configToMerge is null)
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException($"Failed to parse include file: {yamlFile.FullName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_validator.Validate(configToMerge))
|
||||||
|
{
|
||||||
|
throw new YamlIncludeException($"Validation of included YAML failed: {yamlFile.FullName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configToMerge.BaseUrl is not null)
|
||||||
|
{
|
||||||
|
_log.Warning("`base_url` is not allowed in YAML includes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configToMerge.ApiKey is not null)
|
||||||
|
{
|
||||||
|
_log.Warning("`api_key` is not allowed in YAML includes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configToMerge.Include is not null)
|
||||||
|
{
|
||||||
|
_log.Warning("Nested `include` directives are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
return configToMerge;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
|
||||||
|
public static class YamlValidatorRuleSets
|
||||||
|
{
|
||||||
|
public const string RootConfig = "RootConfig";
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
|
||||||
|
public class PostProcessingException : Exception
|
||||||
|
{
|
||||||
|
public PostProcessingException(string? message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
using Recyclarr.TrashLib.Startup;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ConfigIncludeProcessorTest
|
||||||
|
{
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Can_process_expected_type(
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var result = sut.CanProcess(new ConfigYamlInclude());
|
||||||
|
result.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_null_include_path(
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var includeDirective = new ConfigYamlInclude
|
||||||
|
{
|
||||||
|
Config = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Get_relative_config_include_path(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
[Frozen] IAppPaths paths,
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
fs.AddEmptyFile(paths.ConfigsDirectory.File("foo/bar/config.yml"));
|
||||||
|
|
||||||
|
var includeDirective = new ConfigYamlInclude
|
||||||
|
{
|
||||||
|
Config = "foo/bar/config.yml"
|
||||||
|
};
|
||||||
|
|
||||||
|
var path = sut.GetPathToConfig(includeDirective, default);
|
||||||
|
|
||||||
|
path.FullName.Should().Be(paths.ConfigsDirectory.File("foo/bar/config.yml").FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Get_absolute_config_include_path(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var absolutePath = fs.CurrentDirectory().File("foo/bar/config.yml");
|
||||||
|
fs.AddEmptyFile(absolutePath);
|
||||||
|
|
||||||
|
var includeDirective = new ConfigYamlInclude
|
||||||
|
{
|
||||||
|
Config = absolutePath.FullName
|
||||||
|
};
|
||||||
|
|
||||||
|
var path = sut.GetPathToConfig(includeDirective, default);
|
||||||
|
|
||||||
|
path.FullName.Should().Be(absolutePath.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_relative_config_include_path_does_not_exist(
|
||||||
|
// Freeze the mock FS even though we don't use it so that the "Exists" check works right.
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var includeDirective = new ConfigYamlInclude
|
||||||
|
{
|
||||||
|
Config = "foo/bar/config.yml"
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("Relative*not exist*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_absolute_config_include_path_does_not_exist(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
ConfigIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var absolutePath = fs.CurrentDirectory().File("foo/bar/config.yml");
|
||||||
|
|
||||||
|
var includeDirective = new ConfigYamlInclude
|
||||||
|
{
|
||||||
|
Config = absolutePath.FullName
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("Absolute*not exist*");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,525 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using FluentAssertions.Execution;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ServiceConfigMergerTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Merge_api_key_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "a"
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_api_key_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
// API Key should not be merged!
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "b"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_api_key_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "a"
|
||||||
|
};
|
||||||
|
|
||||||
|
// API Key should not be merged!
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "b"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_base_url_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
BaseUrl = "a"
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_base_url_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
// BaseUrl should not be merged!
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
BaseUrl = "b"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_base_url_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
BaseUrl = "a"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Baseurl should not be merged!
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
BaseUrl = "b"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_quality_definition_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "type1",
|
||||||
|
PreferredRatio = 0.5m
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_quality_definition_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "type1",
|
||||||
|
PreferredRatio = 0.5m
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(rightConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_quality_definition_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "type1",
|
||||||
|
PreferredRatio = 0.5m
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "type2",
|
||||||
|
PreferredRatio = 1.0m
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(rightConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_custom_formats_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1", "id2"},
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_custom_formats_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1", "id2"},
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(rightConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_custom_formats_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1", "id2"},
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id3", "id4"},
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id5", "id6"},
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
CustomFormats = leftConfig.CustomFormats.Concat(rightConfig.CustomFormats).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_quality_profiles_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileConfigYaml
|
||||||
|
{
|
||||||
|
Name = "e",
|
||||||
|
QualitySort = QualitySortAlgorithm.Top,
|
||||||
|
MinFormatScore = 100,
|
||||||
|
ScoreSet = "set1",
|
||||||
|
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Except = new[] {"except1"}
|
||||||
|
},
|
||||||
|
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||||
|
{
|
||||||
|
Allowed = true,
|
||||||
|
UntilQuality = "quality1",
|
||||||
|
UntilScore = 200
|
||||||
|
},
|
||||||
|
Qualities = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality1",
|
||||||
|
Qualities = new[] {"quality"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_quality_profiles_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileConfigYaml
|
||||||
|
{
|
||||||
|
Name = "e",
|
||||||
|
QualitySort = QualitySortAlgorithm.Top,
|
||||||
|
MinFormatScore = 100,
|
||||||
|
ScoreSet = "set1",
|
||||||
|
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Except = new[] {"except1"}
|
||||||
|
},
|
||||||
|
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||||
|
{
|
||||||
|
Allowed = true,
|
||||||
|
UntilQuality = "quality1",
|
||||||
|
UntilScore = 200
|
||||||
|
},
|
||||||
|
Qualities = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality1",
|
||||||
|
Qualities = new[] {"quality"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(rightConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||||
|
public void Merge_quality_profiles_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileConfigYaml
|
||||||
|
{
|
||||||
|
Name = "e",
|
||||||
|
QualitySort = QualitySortAlgorithm.Top,
|
||||||
|
MinFormatScore = 100,
|
||||||
|
ScoreSet = "set1",
|
||||||
|
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Except = new[] {"except1"}
|
||||||
|
},
|
||||||
|
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||||
|
{
|
||||||
|
Allowed = true,
|
||||||
|
UntilQuality = "quality1",
|
||||||
|
UntilScore = 200
|
||||||
|
},
|
||||||
|
Qualities = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality1",
|
||||||
|
Qualities = new[] {"quality"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileConfigYaml
|
||||||
|
{
|
||||||
|
Name = "e",
|
||||||
|
ScoreSet = "set2",
|
||||||
|
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||||
|
{
|
||||||
|
Except = new[] {"except2", "except3"}
|
||||||
|
},
|
||||||
|
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||||
|
{
|
||||||
|
UntilQuality = "quality2"
|
||||||
|
},
|
||||||
|
Qualities = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Name = "quality2",
|
||||||
|
Qualities = new[] {"quality3"}
|
||||||
|
},
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality4",
|
||||||
|
Qualities = new[] {"quality5", "quality6"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
using var scope = new AssertionScope().UsingLineBreaks;
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
QualityProfiles = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileConfigYaml
|
||||||
|
{
|
||||||
|
Name = "e",
|
||||||
|
QualitySort = QualitySortAlgorithm.Top,
|
||||||
|
MinFormatScore = 100,
|
||||||
|
ScoreSet = "set2",
|
||||||
|
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Except = new[] {"except1", "except2", "except3"}
|
||||||
|
},
|
||||||
|
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||||
|
{
|
||||||
|
Allowed = true,
|
||||||
|
UntilQuality = "quality2",
|
||||||
|
UntilScore = 200
|
||||||
|
},
|
||||||
|
Qualities = new[]
|
||||||
|
{
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality1",
|
||||||
|
Qualities = new[] {"quality"}
|
||||||
|
},
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Name = "quality2",
|
||||||
|
Qualities = new[] {"quality3"}
|
||||||
|
},
|
||||||
|
new QualityProfileQualityConfigYaml
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Name = "quality4",
|
||||||
|
Qualities = new[] {"quality5", "quality6"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using FluentAssertions.Execution;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class SonarrConfigMergerTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Merge_release_profiles_from_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new[]
|
||||||
|
{
|
||||||
|
new ReleaseProfileConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1"},
|
||||||
|
Filter = new ReleaseProfileFilterConfigYaml
|
||||||
|
{
|
||||||
|
Exclude = new[] {"exclude"},
|
||||||
|
Include = new[] {"include"}
|
||||||
|
},
|
||||||
|
Tags = new[] {"tag1", "tag2"},
|
||||||
|
StrictNegativeScores = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(leftConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_release_profiles_from_non_empty_right_to_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml();
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new[]
|
||||||
|
{
|
||||||
|
new ReleaseProfileConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1"},
|
||||||
|
Filter = new ReleaseProfileFilterConfigYaml
|
||||||
|
{
|
||||||
|
Exclude = new[] {"exclude"},
|
||||||
|
Include = new[] {"include"}
|
||||||
|
},
|
||||||
|
Tags = new[] {"tag1", "tag2"},
|
||||||
|
StrictNegativeScores = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(rightConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||||
|
public void Merge_release_profiles_from_non_empty_right_to_non_empty_left()
|
||||||
|
{
|
||||||
|
var leftConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new[]
|
||||||
|
{
|
||||||
|
new ReleaseProfileConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id1"},
|
||||||
|
Filter = new ReleaseProfileFilterConfigYaml
|
||||||
|
{
|
||||||
|
Exclude = new[] {"exclude1"},
|
||||||
|
Include = new[] {"include1"}
|
||||||
|
},
|
||||||
|
Tags = new[] {"tag1", "tag2"},
|
||||||
|
StrictNegativeScores = true
|
||||||
|
},
|
||||||
|
new ReleaseProfileConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id2", "id3"},
|
||||||
|
Filter = new ReleaseProfileFilterConfigYaml
|
||||||
|
{
|
||||||
|
Exclude = new[] {"exclude2"},
|
||||||
|
Include = new[] {"include2"}
|
||||||
|
},
|
||||||
|
Tags = new[] {"tag3"},
|
||||||
|
StrictNegativeScores = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var rightConfig = new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new[]
|
||||||
|
{
|
||||||
|
new ReleaseProfileConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"id4"},
|
||||||
|
Filter = new ReleaseProfileFilterConfigYaml
|
||||||
|
{
|
||||||
|
Exclude = new[] {"exclude3"},
|
||||||
|
Include = new[] {"include3"}
|
||||||
|
},
|
||||||
|
Tags = new[] {"tag4", "tag5"},
|
||||||
|
StrictNegativeScores = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new SonarrConfigMerger();
|
||||||
|
|
||||||
|
var result = sut.Merge(leftConfig, rightConfig);
|
||||||
|
|
||||||
|
using var scope = new AssertionScope().UsingLineBreaks;
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||||
|
{
|
||||||
|
ReleaseProfiles = leftConfig.ReleaseProfiles.Concat(rightConfig.ReleaseProfiles).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class TemplateIncludeProcessorTest
|
||||||
|
{
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Can_process_expected_type(
|
||||||
|
TemplateIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var result = sut.CanProcess(new TemplateYamlInclude());
|
||||||
|
result.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Obtain_path_from_template(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
[Frozen] IConfigTemplateGuideService templates,
|
||||||
|
TemplateIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||||
|
templates.GetIncludeData().Returns(new[]
|
||||||
|
{
|
||||||
|
new TemplatePath
|
||||||
|
{
|
||||||
|
Id = "my-template",
|
||||||
|
Service = SupportedServices.Radarr,
|
||||||
|
TemplateFile = templatePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var includeDirective = new TemplateYamlInclude {Template = "my-template"};
|
||||||
|
|
||||||
|
var path = sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||||
|
|
||||||
|
path.FullName.Should().Be(templatePath.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_template_is_null(
|
||||||
|
TemplateIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var includeDirective = new TemplateYamlInclude {Template = null};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*template*is required*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_service_types_are_mixed(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
[Frozen] IConfigTemplateGuideService templates,
|
||||||
|
TemplateIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||||
|
templates.GetIncludeData().Returns(new[]
|
||||||
|
{
|
||||||
|
new TemplatePath
|
||||||
|
{
|
||||||
|
Id = "my-template",
|
||||||
|
Service = SupportedServices.Radarr,
|
||||||
|
TemplateFile = templatePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var includeDirective = new TemplateYamlInclude {Template = "my-template"};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Sonarr);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*unable to find*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_no_template_found(
|
||||||
|
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||||
|
[Frozen] IConfigTemplateGuideService templates,
|
||||||
|
TemplateIncludeProcessor sut)
|
||||||
|
{
|
||||||
|
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||||
|
templates.GetIncludeData().Returns(new[]
|
||||||
|
{
|
||||||
|
new TemplatePath
|
||||||
|
{
|
||||||
|
Id = "my-template",
|
||||||
|
Service = SupportedServices.Radarr,
|
||||||
|
TemplateFile = templatePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var includeDirective = new TemplateYamlInclude {Template = "template-does-not-exist"};
|
||||||
|
|
||||||
|
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*unable to find*");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class YamlIncludeResolverTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Find_and_return_processor()
|
||||||
|
{
|
||||||
|
var processors = new[]
|
||||||
|
{
|
||||||
|
Substitute.For<IIncludeProcessor>(),
|
||||||
|
Substitute.For<IIncludeProcessor>()
|
||||||
|
};
|
||||||
|
|
||||||
|
processors[1].CanProcess(default!).ReturnsForAnyArgs(true);
|
||||||
|
processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ =>
|
||||||
|
{
|
||||||
|
var fileInfo = Substitute.For<IFileInfo>();
|
||||||
|
fileInfo.Exists.Returns(true);
|
||||||
|
fileInfo.FullName.Returns("the_path");
|
||||||
|
return fileInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
var sut = new YamlIncludeResolver(processors);
|
||||||
|
var result = sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||||
|
|
||||||
|
result.FullName.Should().Be("the_path");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_when_no_matching_processor()
|
||||||
|
{
|
||||||
|
var processors = new[]
|
||||||
|
{
|
||||||
|
Substitute.For<IIncludeProcessor>(),
|
||||||
|
Substitute.For<IIncludeProcessor>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new YamlIncludeResolver(processors);
|
||||||
|
var act = () => sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*type is not supported*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_when_path_does_not_exist()
|
||||||
|
{
|
||||||
|
var processors = new[]
|
||||||
|
{
|
||||||
|
Substitute.For<IIncludeProcessor>(),
|
||||||
|
Substitute.For<IIncludeProcessor>()
|
||||||
|
};
|
||||||
|
|
||||||
|
processors[1].CanProcess(default!).ReturnsForAnyArgs(true);
|
||||||
|
processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ =>
|
||||||
|
{
|
||||||
|
var fileInfo = Substitute.For<IFileInfo>();
|
||||||
|
fileInfo.Exists.Returns(false);
|
||||||
|
return fileInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
var sut = new YamlIncludeResolver(processors);
|
||||||
|
var act = () => sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*does not exist*");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||||
|
using Recyclarr.TrashLib.TestLibrary;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class IncludePostProcessorIntegrationTest : TrashLibIntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void No_change_when_no_includes()
|
||||||
|
{
|
||||||
|
var sut = Resolve<IncludePostProcessor>();
|
||||||
|
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
["service1"] = new()
|
||||||
|
{
|
||||||
|
ApiKey = "asdf",
|
||||||
|
BaseUrl = "fdsa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = sut.Process(config);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_when_unable_to_parse()
|
||||||
|
{
|
||||||
|
var sut = Resolve<IncludePostProcessor>();
|
||||||
|
|
||||||
|
var configPath = Fs.CurrentDirectory().File("my-include.yml");
|
||||||
|
|
||||||
|
Fs.AddFile(configPath, new MockFileData(
|
||||||
|
"""
|
||||||
|
asdf: invalid
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
["service1"] = new()
|
||||||
|
{
|
||||||
|
Include = new[]
|
||||||
|
{
|
||||||
|
new ConfigYamlInclude {Config = configPath.FullName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => sut.Process(config);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*parse include file*my-include.yml*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_when_unable_to_validate()
|
||||||
|
{
|
||||||
|
var sut = Resolve<IncludePostProcessor>();
|
||||||
|
|
||||||
|
var configPath = Fs.CurrentDirectory().File("my-include.yml");
|
||||||
|
|
||||||
|
Fs.AddFile(configPath, new MockFileData(
|
||||||
|
"""
|
||||||
|
custom_formats:
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
["service1"] = new()
|
||||||
|
{
|
||||||
|
Include = new[]
|
||||||
|
{
|
||||||
|
new ConfigYamlInclude {Config = configPath.FullName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => sut.Process(config);
|
||||||
|
|
||||||
|
act.Should().Throw<YamlIncludeException>().WithMessage("*Validation*failed*my-include.yml*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Merge_works()
|
||||||
|
{
|
||||||
|
var sut = Resolve<IncludePostProcessor>();
|
||||||
|
|
||||||
|
var configPath1 = Fs.CurrentDirectory().File("my-include1.yml");
|
||||||
|
Fs.AddFile(configPath1, new MockFileData(
|
||||||
|
"""
|
||||||
|
custom_formats:
|
||||||
|
- trash_ids:
|
||||||
|
- 496f355514737f7d83bf7aa4d24f8169
|
||||||
|
|
||||||
|
quality_definition:
|
||||||
|
type: anime
|
||||||
|
preferred_ratio: 0.75
|
||||||
|
|
||||||
|
delete_old_custom_formats: false
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var configPath2 = Fs.CurrentDirectory().File("sub_dir/my-include2.yml");
|
||||||
|
Fs.AddFile(configPath2, new MockFileData(
|
||||||
|
"""
|
||||||
|
custom_formats:
|
||||||
|
- trash_ids:
|
||||||
|
- 240770601cc226190c367ef59aba7463
|
||||||
|
|
||||||
|
delete_old_custom_formats: true
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
["service1"] = new()
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml
|
||||||
|
{
|
||||||
|
TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "series"
|
||||||
|
},
|
||||||
|
Include = new[]
|
||||||
|
{
|
||||||
|
new ConfigYamlInclude {Config = configPath1.FullName},
|
||||||
|
new ConfigYamlInclude {Config = configPath2.FullName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = sut.Process(config);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
["service1"] = new()
|
||||||
|
{
|
||||||
|
CustomFormats = new[]
|
||||||
|
{
|
||||||
|
new CustomFormatConfigYaml {TrashIds = new[] {"496f355514737f7d83bf7aa4d24f8169"}},
|
||||||
|
new CustomFormatConfigYaml {TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}},
|
||||||
|
new CustomFormatConfigYaml {TrashIds = new[] {"240770601cc226190c367ef59aba7463"}}
|
||||||
|
},
|
||||||
|
QualityDefinition = new QualitySizeConfigYaml
|
||||||
|
{
|
||||||
|
Type = "series",
|
||||||
|
PreferredRatio = 0.75m
|
||||||
|
},
|
||||||
|
DeleteOldCustomFormats = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue