refactor: Separate config finding logic from ServiceCommand

Reason: Exception would be thrown when no config files are found when
invoking the various `--list` arguments. No configuration should not
prevent these from working.
pull/139/head
Robert Dailey 2 years ago
parent f251bbeba4
commit a853f0830d

@ -37,12 +37,18 @@ public abstract class IntegrationFixture : IDisposable
RegisterMockFor<IGitRepositoryFactory>(builder);
RegisterMockFor<IRepositoryStaticWrapper>(builder);
RegisterExtraTypes(builder);
builder.RegisterSource<AnyConcreteTypeNotAlreadyRegisteredSource>();
});
SetupMetadataJson();
}
protected virtual void RegisterExtraTypes(ContainerBuilder builder)
{
}
private static ILogger CreateLogger()
{
return new LoggerConfiguration()

@ -1,214 +0,0 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using CliFx.Attributes;
using CliFx.Exceptions;
using FluentAssertions;
using JetBrains.Annotations;
using NUnit.Framework;
using Recyclarr.Command;
using Recyclarr.Config;
using Recyclarr.TestLibrary;
using TestLibrary.AutoFixture;
using TrashLib;
using TrashLib.Config.Services;
using TrashLib.Startup;
namespace Recyclarr.Tests.Command;
[UsedImplicitly]
public class TestConfiguration : ServiceConfiguration
{
public string ServiceName { get; set; }
}
[Command]
[UsedImplicitly]
public class TestServiceCommand : ServiceCommand
{
private readonly ConfigurationLoader<TestConfiguration> _loader;
public override string Name => nameof(TestServiceCommand);
public IEnumerable<TestConfiguration> LoadedConfigs { get; private set; } = Array.Empty<TestConfiguration>();
public TestServiceCommand(ConfigurationLoader<TestConfiguration> loader)
{
_loader = loader;
}
public async Task Process(IServiceLocatorProxy container, string[] configSections)
{
await base.Process(container);
LoadedConfigs = configSections.SelectMany(x =>
{
return _loader.LoadMany(Config, x).Select(y =>
{
y.ServiceName = x;
return y;
});
});
}
}
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ServiceCommandTest : IntegrationFixture
{
private static string[] GetYamlPaths(IAppPaths paths)
{
return new[]
{
paths.ConfigPath.FullName,
paths.ConfigsDirectory.File("b.yml").FullName,
paths.ConfigsDirectory.File("c.yml").FullName
};
}
[Test, AutoMockData]
public async Task Use_configs_dir_and_file_if_no_cli_argument(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
var yamlPaths = GetYamlPaths(paths);
foreach (var path in yamlPaths)
{
fs.AddFile(path, new MockFileData(""));
}
await sut.Process(container);
sut.Config.Should().BeEquivalentTo(yamlPaths);
}
[Test, AutoMockData]
public async Task Use_paths_from_cli_instead_of_configs_dir_and_file_if_argument_specified(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
var yamlPaths = GetYamlPaths(paths);
foreach (var path in yamlPaths)
{
fs.AddFile(path, new MockFileData(""));
}
var manualConfig = fs.CurrentDirectory().File("manual-config.yml");
fs.AddFile(manualConfig.FullName, new MockFileData(""));
sut.Config = new[] {manualConfig.FullName};
await sut.Process(container);
sut.Config.Should().BeEquivalentTo(manualConfig.FullName);
}
[Test, AutoMockData]
public async Task Non_existent_files_are_skipped(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
var yamlPaths = GetYamlPaths(paths);
fs.AddFile(yamlPaths[0], new MockFileData(""));
fs.AddFile(yamlPaths[1], new MockFileData(""));
sut.Config = yamlPaths.Take(2).ToList();
await sut.Process(container);
sut.Config.Should().BeEquivalentTo(yamlPaths.Take(2));
}
[Test, AutoMockData]
public async Task No_recyclarr_yml_when_not_exists(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
var testFile = paths.ConfigsDirectory.File("test.yml").FullName;
fs.AddFile(testFile, new MockFileData(""));
await sut.Process(container);
sut.Config.Should().BeEquivalentTo(testFile);
}
[Test, AutoMockData]
public async Task Only_add_recyclarr_yml_when_exists(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
fs.AddFile(paths.ConfigPath.FullName, new MockFileData(""));
await sut.Process(container);
sut.Config.Should().BeEquivalentTo(paths.ConfigPath.FullName);
}
[Test, AutoMockData]
public async Task Throw_when_no_configs_found(
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
TestServiceCommand sut)
{
var act = () => sut.Process(container);
await act.Should().ThrowAsync<CommandException>();
}
private static string MakeYamlData(string service, string url)
{
return $@"
{service}:
- base_url: {url}
api_key: abc123
";
}
[Test]
public async Task Correct_yaml_loaded_multi_config(
// [Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
// [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
// IServiceLocatorProxy container,
// TestServiceCommand sut
)
{
var sut = Resolve<TestServiceCommand>();
var paths = Resolve<IAppPaths>();
var yamlPaths = GetYamlPaths(paths);
var data = new[]
{
MakeYamlData("sonarr", "a"),
MakeYamlData("radarr", "b") + MakeYamlData("sonarr", "c"),
MakeYamlData("radarr", "d")
};
foreach (var (path, yaml) in yamlPaths.Zip(data))
{
Fs.AddFile(path, new MockFileData(yaml));
}
await sut.Process(ServiceLocator, new[] {"sonarr", "radarr"});
sut.LoadedConfigs.Should().BeEquivalentTo(new[]
{
new {ServiceName = "sonarr", BaseUrl = "a"},
new {ServiceName = "radarr", BaseUrl = "b"},
new {ServiceName = "sonarr", BaseUrl = "c"},
new {ServiceName = "radarr", BaseUrl = "d"}
});
}
}

@ -0,0 +1,139 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using CliFx.Exceptions;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Config;
using TestLibrary.AutoFixture;
using TrashLib;
using TrashLib.Startup;
namespace Recyclarr.Tests.Config;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigurationFinderTest
{
private static string[] GetYamlPaths(IAppPaths paths)
{
return new[]
{
paths.ConfigPath.FullName,
paths.ConfigsDirectory.File("b.yml").FullName,
paths.ConfigsDirectory.File("c.yml").FullName
};
}
[Test, AutoMockData]
public void Use_default_configs_if_explicit_list_null(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var yamlPaths = GetYamlPaths(paths);
foreach (var path in yamlPaths)
{
fs.AddFile(path, new MockFileData(""));
}
var result = sut.GetConfigFiles(null);
result.Should().BeEquivalentTo(yamlPaths);
}
[Test, AutoMockData]
public void Use_default_configs_if_explicit_list_empty(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var yamlPaths = GetYamlPaths(paths);
foreach (var path in yamlPaths)
{
fs.AddFile(path, new MockFileData(""));
}
var result = sut.GetConfigFiles(new List<string>());
result.Should().BeEquivalentTo(yamlPaths);
}
[Test, AutoMockData]
public void Use_explicit_paths_instead_of_default(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var yamlPaths = GetYamlPaths(paths);
foreach (var path in yamlPaths)
{
fs.AddFile(path, new MockFileData(""));
}
var manualConfig = fs.CurrentDirectory().File("manual-config.yml");
fs.AddFile(manualConfig.FullName, new MockFileData(""));
var result = sut.GetConfigFiles(new[] {manualConfig.FullName});
result.Should().BeEquivalentTo(manualConfig.FullName);
}
[Test, AutoMockData]
public void Non_existent_files_are_skipped(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var yamlPaths = GetYamlPaths(paths);
fs.AddFile(yamlPaths[0], new MockFileData(""));
fs.AddFile(yamlPaths[1], new MockFileData(""));
var result = sut.GetConfigFiles(yamlPaths);
result.Should().BeEquivalentTo(yamlPaths.Take(2));
}
[Test, AutoMockData]
public void No_recyclarr_yml_when_not_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var testFile = paths.ConfigsDirectory.File("test.yml").FullName;
fs.AddFile(testFile, new MockFileData(""));
var result = sut.GetConfigFiles(Array.Empty<string>());
result.Should().BeEquivalentTo(testFile);
}
[Test, AutoMockData]
public void Only_add_recyclarr_yml_when_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
fs.AddFile(paths.ConfigPath.FullName, new MockFileData(""));
var result = sut.GetConfigFiles(Array.Empty<string>());
result.Should().BeEquivalentTo(paths.ConfigPath.FullName);
}
[Test, AutoMockData]
public void Throw_when_no_configs_found(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
var act = () => sut.GetConfigFiles(Array.Empty<string>());
act.Should().Throw<CommandException>();
}
}

@ -18,7 +18,7 @@ using TestLibrary.AutoFixture;
using TrashLib.Config.Services;
using TrashLib.Services.Sonarr.Config;
namespace Recyclarr.Tests.Config.Services;
namespace Recyclarr.Tests.Config;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -34,7 +34,6 @@ internal class RadarrCommand : ServiceCommand
var lister = container.Resolve<IRadarrGuideDataLister>();
var log = container.Resolve<ILogger>();
var configLoader = container.Resolve<IConfigurationLoader<RadarrConfiguration>>();
var guideService = container.Resolve<IRadarrGuideService>();
if (ListCustomFormats)
@ -49,7 +48,9 @@ internal class RadarrCommand : ServiceCommand
return;
}
foreach (var config in configLoader.LoadMany(Config, "radarr"))
var configFinder = container.Resolve<IConfigurationFinder>();
var configLoader = container.Resolve<IConfigurationLoader<RadarrConfiguration>>();
foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "radarr"))
{
await using var scope = container.BeginLifetimeScope(builder =>
{

@ -1,4 +1,3 @@
using System.IO.Abstractions;
using System.Text;
using Autofac;
using CliFx.Attributes;
@ -14,7 +13,6 @@ using Serilog;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Repo;
using TrashLib.Startup;
using YamlDotNet.Core;
namespace Recyclarr.Command;
@ -30,7 +28,7 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
"One or more YAML config files to use. All configs will be used and settings are additive. " +
"If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")]
// ReSharper disable once MemberCanBeProtected.Global
public ICollection<string> Config { get; [UsedImplicitly] set; } = new List<string>();
public IReadOnlyCollection<string> Config { get; [UsedImplicitly] set; } = new List<string>();
[CommandOption("app-data", Description =
"Explicitly specify the location of the recyclarr application data directory. " +
@ -70,29 +68,11 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
}
}
private static ICollection<string> FindAllConfigFiles(IAppPaths paths)
{
var configs = new List<string>();
if (paths.ConfigsDirectory.Exists)
{
configs.AddRange(paths.ConfigsDirectory.EnumerateFiles("*.yml").Select(x => x.FullName));
}
if (paths.ConfigPath.Exists)
{
configs.Add(paths.ConfigPath.FullName);
}
return configs;
}
public override Task Process(ILifetimeScope container)
{
var log = container.Resolve<ILogger>();
var settingsProvider = container.Resolve<ISettingsProvider>();
var repoUpdater = container.Resolve<IRepoUpdater>();
var paths = container.Resolve<IAppPaths>();
var migration = container.Resolve<IMigrationExecutor>();
// Will throw if migration is required, otherwise just a warning is issued.
@ -101,29 +81,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
SetupHttp(log, settingsProvider);
repoUpdater.UpdateRepo();
if (!Config.Any())
{
Config = FindAllConfigFiles(paths);
}
else
{
var fs = container.Resolve<IFileSystem>();
var split = Config.ToLookup(x => fs.File.Exists(x));
foreach (var nonExistentConfig in split[false])
{
log.Warning("Configuration file does not exist {File}", nonExistentConfig);
}
Config = split[true].ToList();
}
if (Config.Count == 0)
{
throw new CommandException("No configuration YAML files found");
}
log.Debug("Using config files: {ConfigFiles}", Config);
return Task.CompletedTask;
}

@ -47,7 +47,6 @@ public class SonarrCommand : ServiceCommand
await base.Process(container);
var lister = container.Resolve<ISonarrGuideDataLister>();
var configLoader = container.Resolve<IConfigurationLoader<SonarrConfiguration>>();
var log = container.Resolve<ILogger>();
var guideService = container.Resolve<ISonarrGuideService>();
@ -84,7 +83,9 @@ public class SonarrCommand : ServiceCommand
return;
}
foreach (var config in configLoader.LoadMany(Config, "sonarr"))
var configFinder = container.Resolve<IConfigurationFinder>();
var configLoader = container.Resolve<IConfigurationLoader<SonarrConfiguration>>();
foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "sonarr"))
{
await using var scope = container.BeginLifetimeScope(builder =>
{

@ -81,6 +81,7 @@ public static class CompositionRoot
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterType<DefaultObjectFactory>().As<IObjectFactory>();
builder.RegisterType<ConfigurationFinder>().As<IConfigurationFinder>();
builder.RegisterGeneric(typeof(ConfigurationLoader<>))
.WithProperty(new AutowiringParameter())

@ -0,0 +1,63 @@
using System.IO.Abstractions;
using CliFx.Exceptions;
using Serilog;
using TrashLib.Startup;
namespace Recyclarr.Config;
public class ConfigurationFinder : IConfigurationFinder
{
private readonly IAppPaths _paths;
private readonly IFileSystem _fs;
private readonly ILogger _log;
public ConfigurationFinder(IAppPaths paths, IFileSystem fs, ILogger log)
{
_paths = paths;
_fs = fs;
_log = log;
}
private IReadOnlyCollection<string> FindDefaultConfigFiles()
{
var configs = new List<string>();
if (_paths.ConfigsDirectory.Exists)
{
configs.AddRange(_paths.ConfigsDirectory.EnumerateFiles("*.yml").Select(x => x.FullName));
}
if (_paths.ConfigPath.Exists)
{
configs.Add(_paths.ConfigPath.FullName);
}
return configs;
}
public IReadOnlyCollection<string> GetConfigFiles(IReadOnlyCollection<string>? configs)
{
if (configs is null || !configs.Any())
{
configs = FindDefaultConfigFiles();
}
else
{
var split = configs.ToLookup(x => _fs.File.Exists(x));
foreach (var nonExistentConfig in split[false])
{
_log.Warning("Configuration file does not exist {File}", nonExistentConfig);
}
configs = split[true].ToList();
}
if (configs.Count == 0)
{
throw new CommandException("No configuration YAML files found");
}
_log.Debug("Using config files: {ConfigFiles}", configs);
return configs;
}
}

@ -0,0 +1,6 @@
namespace Recyclarr.Config;
public interface IConfigurationFinder
{
IReadOnlyCollection<string> GetConfigFiles(IReadOnlyCollection<string>? configs);
}

@ -7,7 +7,7 @@ public sealed class InlineAutoMockDataAttribute : InlineAutoDataAttribute
{
[SuppressMessage("Design", "CA1019", MessageId = "Define accessors for attribute arguments",
Justification = "The parameter is forwarded to the base class and not used directly")]
public InlineAutoMockDataAttribute(params object[] parameters)
public InlineAutoMockDataAttribute(params object?[] parameters)
: base(NSubstituteFixture.Create, parameters)
{
}

Loading…
Cancel
Save