refactor: Greatly simplify DI support code

- Get rid of `IServiceLocatorProxy`
- Get rid of `ICompositionRoot`

In addition, all unit tests avoid using `BaseCommand` directly, as it
does its own composition root setup and overrides the IntegrationFixture
test setup.
pull/139/head
Robert Dailey 2 years ago
parent 88bdc5a92e
commit 835aae860b

@ -22,13 +22,15 @@ public abstract class IntegrationFixture : IDisposable
{
protected IntegrationFixture()
{
var compRoot = new CompositionRoot();
ServiceLocator = compRoot.Setup(builder =>
Paths = new AppPaths(Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr"));
Logger = CreateLogger();
Container = CompositionRoot.Setup(builder =>
{
builder.RegisterInstance(Fs).As<IFileSystem>();
builder.RegisterInstance(new AppPaths(Fs.CurrentDirectory())).As<IAppPaths>();
builder.RegisterInstance(Paths).As<IAppPaths>();
builder.RegisterInstance(Console).As<IConsole>();
builder.Register(_ => CreateLogger()).As<ILogger>().SingleInstance();
builder.RegisterInstance(Logger).As<ILogger>().SingleInstance();
RegisterMockFor<IServiceCommand>(builder);
RegisterMockFor<IGitRepository>(builder);
@ -45,20 +47,21 @@ public abstract class IntegrationFixture : IDisposable
{
return new LoggerConfiguration()
.MinimumLevel.Is(LogEventLevel.Debug)
.WriteTo.Console()
.WriteTo.TestCorrelator()
.CreateLogger();
}
private void SetupMetadataJson()
{
var paths = Resolve<IAppPaths>();
var metadataFile = paths.RepoDirectory.File("metadata.json");
var metadataFile = Paths.RepoDirectory.File("metadata.json");
Fs.AddFileFromResource(metadataFile, "metadata.json");
}
protected MockFileSystem Fs { get; } = new();
protected FakeInMemoryConsole Console { get; } = new();
protected IServiceLocatorProxy ServiceLocator { get; }
protected ILifetimeScope Container { get; }
protected IAppPaths Paths { get; }
protected ILogger Logger { get; }
private static void RegisterMockFor<T>(ContainerBuilder builder) where T : class
{
@ -67,13 +70,13 @@ public abstract class IntegrationFixture : IDisposable
protected T Resolve<T>(Action<ContainerBuilder> customRegistrations) where T : notnull
{
var childScope = ServiceLocator.Container.BeginLifetimeScope(customRegistrations);
var childScope = Container.BeginLifetimeScope(customRegistrations);
return childScope.Resolve<T>();
}
protected T Resolve<T>() where T : notnull
{
return ServiceLocator.Resolve<T>();
return Container.Resolve<T>();
}
protected virtual void Dispose(bool disposing)
@ -83,7 +86,7 @@ public abstract class IntegrationFixture : IDisposable
return;
}
ServiceLocator.Dispose();
Container.Dispose();
Console.Dispose();
}

@ -5,7 +5,6 @@ using NUnit.Framework;
using Recyclarr.Command.Setup;
using Recyclarr.TestLibrary;
using TrashLib.Config.Settings;
using TrashLib.Startup;
namespace Recyclarr.Tests;
@ -27,54 +26,50 @@ public class BaseCommandSetupIntegrationTest : IntegrationFixture
[Test]
public void Log_janitor_cleans_up_user_specified_max_files()
{
var paths = Resolve<IAppPaths>();
const int maxFiles = 25;
Fs.AddFile(paths.SettingsPath.FullName, new MockFileData($@"
Fs.AddFile(Paths.SettingsPath.FullName, new MockFileData($@"
log_janitor:
max_files: {maxFiles}
"));
for (var i = 0; i < maxFiles + 20; ++i)
{
Fs.AddFile(paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
Fs.AddFile(Paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
}
var sut = Resolve<JanitorCleanupTask>();
sut.OnFinish();
Fs.AllFiles.Where(x => x.StartsWith(paths.LogDirectory.FullName))
Fs.AllFiles.Where(x => x.StartsWith(Paths.LogDirectory.FullName))
.Should().HaveCount(maxFiles);
}
[Test]
public void Log_janitor_cleans_up_default_max_files()
{
var paths = Resolve<IAppPaths>();
var settingsProvider = Resolve<ISettingsProvider>();
var maxFiles = settingsProvider.Settings.LogJanitor.MaxFiles;
for (var i = 0; i < maxFiles + 20; ++i)
{
Fs.AddFile(paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
Fs.AddFile(Paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
}
var sut = Resolve<JanitorCleanupTask>();
sut.OnFinish();
maxFiles.Should().BeGreaterThan(0);
Fs.AllFiles.Where(x => x.StartsWith(paths.LogDirectory.FullName))
Fs.AllFiles.Where(x => x.StartsWith(Paths.LogDirectory.FullName))
.Should().HaveCount(maxFiles);
}
[Test]
public void App_paths_setup_creates_initial_directories()
{
var paths = Resolve<IAppPaths>();
for (var i = 0; i < 50; ++i)
{
Fs.AddFile(paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
Fs.AddFile(Paths.LogDirectory.File($"logfile-{i}.log").FullName, new MockFileData(""));
}
var sut = Resolve<AppPathSetupTask>();
@ -82,9 +77,9 @@ log_janitor:
var expectedDirs = new[]
{
paths.LogDirectory.FullName,
paths.RepoDirectory.FullName,
paths.CacheDirectory.FullName
Paths.LogDirectory.FullName,
Paths.RepoDirectory.FullName,
Paths.CacheDirectory.FullName
};
expectedDirs.Should().IntersectWith(Fs.AllDirectories);

@ -1,58 +1,43 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using CliFx.Infrastructure;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Command;
using TestLibrary.AutoFixture;
using TrashLib.TestLibrary;
using Recyclarr.TestLibrary;
// ReSharper disable MethodHasAsyncOverload
namespace Recyclarr.Tests.Command;
[TestFixture]
// Cannot be parallelized due to static CompositionRoot property
public class CreateConfigCommandTest
[Parallelizable(ParallelScope.All)]
public class CreateConfigCommandTest : IntegrationFixture
{
[Test, AutoMockData]
public async Task Config_file_created_when_using_default_path(
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceLocatorProxy container,
ICompositionRoot compositionRoot,
CreateConfigCommand sut)
[Test]
public async Task Config_file_created_when_using_default_path()
{
BaseCommand.CompositionRoot = compositionRoot;
var sut = new CreateConfigCommand();
await sut.Process(container);
await sut.Process(Container);
var file = fs.GetFile(paths.ConfigPath.FullName);
var file = Fs.GetFile(Paths.ConfigPath.FullName);
file.Should().NotBeNull();
file.Contents.Should().NotBeEmpty();
}
[Test, AutoMockData]
public async Task Config_file_created_when_using_user_specified_path(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
ICompositionRoot compositionRoot,
CreateConfigCommand sut)
[Test]
public async Task Config_file_created_when_using_user_specified_path()
{
BaseCommand.CompositionRoot = compositionRoot;
var ymlPath = fs.CurrentDirectory()
var sut = new CreateConfigCommand();
var ymlPath = Fs.CurrentDirectory()
.SubDirectory("user")
.SubDirectory("specified")
.File("file.yml").FullName;
sut.AppDataDirectory = ymlPath;
await sut.ExecuteAsync(Substitute.For<IConsole>());
await sut.Process(Container);
var file = fs.GetFile(ymlPath);
var file = Fs.GetFile(ymlPath);
file.Should().NotBeNull();
file.Contents.Should().NotBeEmpty();
}

@ -0,0 +1,67 @@
{
"name": "Optionals",
"trash_id": "76e060895c5b8a765c310933da0a5357",
"ignored": [{
"name": "Golden rule",
"trash_id": "cec8880b847dd5d31d29167ee0112b57",
"term": "/^(?=.*(1080|720))(?=.*((x|h)[ ._-]?265|hevc)).*/i"
}, {
"name": "Ignore Dolby Vision without HDR10 fallback.",
"trash_id": "436f5a7d08fbf02ba25cb5e5dfe98e55",
"term": "/^(?!.*(HDR|HULU|REMUX))(?=.*\\b(DV|Dovi|Dolby[- .]?Vision)\\b).*/i"
}, {
"name": "Ignore The Group -SCENE",
"trash_id": "f3f0f3691c6a1988d4a02963e69d11f2",
"term": "/\\b(-scene)\\b/i"
}, {
"name": "Ignore so called scene releases",
"trash_id": "5bc23c3a055a1a5d8bbe4fb49d80e0cb",
"term": "/^(?!.*(web[ ]dl|-deflate|-inflate))(?=.*([_. ]WEB[_. ]|-CAKES\\b|-GGEZ\\b|-GGWP\\b|-GLHF\\b|-GOSSIP\\b|-KOGI\\b|-PECULATE\\b)).*/i"
}, {
"name": "Dislike Bad Dual Audio Groups",
"trash_id": "538bad00ee6f8aced8e0db5218b8484c",
"term": "/\\b(-alfaHD|-BAT|-BNd|-C\\.A\\.A|-Cory|-FF|-FOXX|-G4RiS|-GUEIRA|-N3G4N|-PD|-RiPER|-RK|-SiGLA|-Tars|-WTV|-Yatogam1|-YusukeFLA)\\b/i"
}],
"required": [],
"preferred": [{
"score": 15,
"terms": [{
"name": "Prefer Season Packs",
"trash_id": "ea83f4740cec4df8112f3d6dd7c82751",
"term": "/\\bS\\d+\\b(?!E\\d+\\b)/i"
}]
}, {
"score": 10,
"terms": [{
"name": "Prefer HDR",
"trash_id": "bc7a6383cbe88c3ee2d6396e1aacc0b3",
"term": "/\\bHDR(\\b|\\d)/i"
}]
}, {
"score": 100,
"terms": [{
"name": "Prefer Dolby Vision",
"trash_id": "fa47da3377076d82d07c4e95b3f13d07",
"term": "/\\b(dv|dovi|dolby[ .]?vision)\\b/i"
}]
}, {
"score": -25,
"terms": [{
"name": "Dislike retags: rartv, rarbg, eztv, TGx",
"trash_id": "6f2aefa61342a63387f2a90489e90790",
"term": "/(\\[rartv\\]|\\[rarbg\\]|\\[eztv\\]|\\[TGx\\])/i"
}, {
"name": "Dislike retagged groups",
"trash_id": "19cd5ecc0a24bf493a75e80a51974cdd",
"term": "/(-4P|-4Planet|-AsRequested|-BUYMORE|-CAPTCHA|-Chamele0n|-GEROV|-iNC0GNiTO|-NZBGeek|-Obfuscated|-postbot|-Rakuv|-Scrambled|-WhiteRev|-WRTEAM|-xpost)\\b/i"
}, {
"name": "Dislike release ending: en",
"trash_id": "6a7b462c6caee4a991a9d8aa38ce2405",
"term": "/\\s?\\ben\\b$/i"
}, {
"name": "Dislike release containing: 1-",
"trash_id": "236a3626a07cacf5692c73cc947bc280",
"term": "/(?<!\\d\\.)(1-.+)$/i"
}]
}]
}

@ -1,18 +1,19 @@
using AutoFixture.NUnit3;
using System.IO.Abstractions;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using Common.TestLibrary;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Command;
using Recyclarr.TestLibrary;
using TestLibrary.AutoFixture;
using TrashLib.Services.Sonarr;
using TrashLib.Repo;
namespace Recyclarr.Tests.Command;
[TestFixture]
// Cannot be parallelized due to static CompositionRoot property
public class SonarrCommandTest
[Parallelizable(ParallelScope.All)]
public class SonarrCommandTest : IntegrationFixture
{
[Test, AutoMockData]
public async Task List_terms_without_value_fails(
@ -24,7 +25,7 @@ public class SonarrCommandTest
// When `--list-terms` is specified on the command line without a value, it gets a `null` value assigned.
sut.ListTerms = null;
var act = async () => await sut.ExecuteAsync(console);
var act = async () => await sut.Process(Container);
await act.Should().ThrowAsync<CommandException>();
}
@ -39,42 +40,40 @@ public class SonarrCommandTest
// If the user specifies a blank string as the value, it should still fail.
sut.ListTerms = "";
var act = async () => await sut.ExecuteAsync(console);
var act = async () => await sut.Process(Container);
await act.Should().ThrowAsync<CommandException>();
}
[Test, AutoMockData]
public async Task List_terms_uses_specified_trash_id(
[Frozen] ISonarrGuideDataLister lister,
IConsole console,
ICompositionRoot compositionRoot,
SonarrCommand sut)
[Test]
public async Task List_terms_uses_specified_trash_id()
{
BaseCommand.CompositionRoot = compositionRoot;
sut.ListReleaseProfiles = false;
var repoPaths = Resolve<IRepoPathsFactory>().Create();
var cfDir = repoPaths.SonarrReleaseProfilePaths.First();
Fs.AddFileFromResource(cfDir.File("optionals.json"), "optionals.json");
sut.ListTerms = "some_id";
var sut = new SonarrCommand
{
ListReleaseProfiles = false,
ListTerms = "76e060895c5b8a765c310933da0a5357"
};
await sut.ExecuteAsync(console);
await sut.Process(Container);
lister.Received().ListTerms("some_id");
Console.ReadOutputString().Should().Contain("List of Terms");
}
[Test, AutoMockData]
public async Task List_release_profiles_is_invoked(
[Frozen] ISonarrGuideDataLister lister,
IConsole console,
ICompositionRoot compositionRoot,
SonarrCommand sut)
[Test]
public async Task List_release_profiles_is_invoked()
{
BaseCommand.CompositionRoot = compositionRoot;
sut.ListReleaseProfiles = true;
sut.ListTerms = null;
var sut = new SonarrCommand
{
ListReleaseProfiles = true,
ListTerms = null
};
await sut.ExecuteAsync(console);
await sut.Process(Container);
lister.Received().ListReleaseProfiles();
Console.ReadOutputString().Should().Contain("List of Release Profiles");
}
}

@ -43,7 +43,7 @@ public class CompositionRootTest
{
var act = () =>
{
using var container = new CompositionRoot().Setup().Container;
using var container = CompositionRoot.Setup();
service.Instantiate(container);
};
@ -61,7 +61,7 @@ public class CompositionRootTest
public ConcreteTypeEnumerator()
{
_container = new CompositionRoot().Setup().Container;
_container = CompositionRoot.Setup();
}
public IEnumerator GetEnumerator()
@ -91,7 +91,7 @@ public class CompositionRootTest
[TestCaseSource(typeof(ConcreteTypeEnumerator))]
public void Service_should_be_instantiable(Type service)
{
using var container = new CompositionRoot().Setup(RegisterAdditionalServices).Container;
using var container = CompositionRoot.Setup(RegisterAdditionalServices);
container.Resolve(service).Should().NotBeNull();
}
}

@ -16,7 +16,7 @@ public class MigrationExecutorTest
[Test]
public void Migration_steps_are_in_expected_order()
{
var container = new CompositionRoot().Setup(builder =>
var container = CompositionRoot.Setup(builder =>
{
builder.RegisterInstance(Substitute.For<IAppPaths>());
});

@ -3,7 +3,6 @@ using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using TrashLib.Config.Settings;
using TrashLib.Startup;
namespace Recyclarr.Tests;
@ -15,7 +14,6 @@ public class ServiceCompatibilityIntegrationTest : IntegrationFixture
public void Load_settings_yml_correctly_when_file_exists()
{
var sut = Resolve<ISettingsProvider>();
var paths = Resolve<IAppPaths>();
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects.
// This test only ensures that the data deserialized is from the actual correct file.
@ -24,7 +22,7 @@ repository:
clone_url: http://the_url.com
";
Fs.AddFile(paths.SettingsPath.FullName, new MockFileData(yamlData));
Fs.AddFile(Paths.SettingsPath.FullName, new MockFileData(yamlData));
var settings = sut.Settings;

@ -1,7 +1,6 @@
using Autofac;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using JetBrains.Annotations;
using MoreLinq.Extensions;
@ -23,8 +22,6 @@ public abstract class BaseCommand : ICommand
"Display additional logs useful for development/debug purposes.")]
public bool Debug { get; [UsedImplicitly] set; } = false;
public static ICompositionRoot? CompositionRoot { get; set; }
protected virtual void RegisterServices(ContainerBuilder builder)
{
}
@ -34,12 +31,7 @@ public abstract class BaseCommand : ICommand
// Must happen first because everything can use the logger.
var logLevel = Debug ? LogEventLevel.Debug : LogEventLevel.Information;
if (CompositionRoot is null)
{
throw new CommandException("CompositionRoot must not be null");
}
using var container = CompositionRoot.Setup(builder =>
await using var container = CompositionRoot.Setup(builder =>
{
builder.RegisterInstance(console).As<IConsole>().ExternallyOwned();
@ -67,5 +59,5 @@ public abstract class BaseCommand : ICommand
}
}
public abstract Task Process(IServiceLocatorProxy container);
public abstract Task Process(ILifetimeScope container);
}

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using Autofac;
using CliFx.Attributes;
using CliFx.Exceptions;
using Common;
@ -18,7 +19,7 @@ public class CreateConfigCommand : BaseCommand
"directory")]
public override string? AppDataDirectory { get; set; }
public override async Task Process(IServiceLocatorProxy container)
public override async Task Process(ILifetimeScope container)
{
var fs = container.Resolve<IFileSystem>();
var paths = container.Resolve<IAppPaths>();

@ -1,4 +1,5 @@
using System.Text;
using Autofac;
using CliFx.Attributes;
using CliFx.Exceptions;
using JetBrains.Annotations;
@ -15,7 +16,7 @@ public class MigrateCommand : BaseCommand
"Mainly for usage in Docker; not recommended for normal use.")]
public override string? AppDataDirectory { get; set; }
public override Task Process(IServiceLocatorProxy container)
public override Task Process(ILifetimeScope container)
{
var migration = container.Resolve<IMigrationExecutor>();

@ -28,7 +28,7 @@ internal class RadarrCommand : ServiceCommand
public override string Name => "Radarr";
public override async Task Process(IServiceLocatorProxy container)
public override async Task Process(ILifetimeScope container)
{
await base.Process(container);
@ -51,7 +51,7 @@ internal class RadarrCommand : ServiceCommand
foreach (var config in configLoader.LoadMany(Config, "radarr"))
{
await using var scope = container.Container.BeginLifetimeScope(builder =>
await using var scope = container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).As<IServiceConfiguration>();
});

@ -69,7 +69,7 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
}
}
public override Task Process(IServiceLocatorProxy container)
public override Task Process(ILifetimeScope container)
{
var log = container.Resolve<ILogger>();
var settingsProvider = container.Resolve<ISettingsProvider>();

@ -42,7 +42,7 @@ public class SonarrCommand : ServiceCommand
public override string Name => "Sonarr";
public override async Task Process(IServiceLocatorProxy container)
public override async Task Process(ILifetimeScope container)
{
await base.Process(container);
@ -86,7 +86,7 @@ public class SonarrCommand : ServiceCommand
foreach (var config in configLoader.LoadMany(Config, "sonarr"))
{
await using var scope = container.Container.BeginLifetimeScope(builder =>
await using var scope = container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).As<IServiceConfiguration>();
});

@ -24,14 +24,14 @@ using YamlDotNet.Serialization;
namespace Recyclarr;
public class CompositionRoot : ICompositionRoot
public static class CompositionRoot
{
public IServiceLocatorProxy Setup(Action<ContainerBuilder>? extraRegistrations = null)
public static ILifetimeScope Setup(Action<ContainerBuilder>? extraRegistrations = null)
{
return Setup(new ContainerBuilder(), extraRegistrations);
}
private IServiceLocatorProxy Setup(ContainerBuilder builder, Action<ContainerBuilder>? extraRegistrations = null)
private static ILifetimeScope Setup(ContainerBuilder builder, Action<ContainerBuilder>? extraRegistrations = null)
{
RegisterAppPaths(builder);
RegisterLogger(builder);
@ -59,7 +59,7 @@ public class CompositionRoot : ICompositionRoot
extraRegistrations?.Invoke(builder);
return new ServiceLocatorProxy(builder.Build());
return builder.Build();
}
private static void RegisterLogger(ContainerBuilder builder)

@ -1,8 +0,0 @@
using Autofac;
namespace Recyclarr;
public interface ICompositionRoot
{
IServiceLocatorProxy Setup(Action<ContainerBuilder>? extraRegistrations = null);
}

@ -1,15 +0,0 @@
using Autofac;
namespace Recyclarr;
/// <remarks>
/// This class exists to make unit testing easier. Many methods for ILifetimeScope are extension
/// methods and make unit testing more difficult.
/// This class wraps Autofac to make it more
/// "mockable".
/// </remarks>
public interface IServiceLocatorProxy : IDisposable
{
ILifetimeScope Container { get; }
T Resolve<T>() where T : notnull;
}

@ -2,7 +2,6 @@ using System.Diagnostics;
using System.Text;
using Autofac;
using CliFx;
using Recyclarr.Command;
namespace Recyclarr;
@ -12,8 +11,6 @@ internal static class Program
public static async Task<int> Main()
{
BaseCommand.CompositionRoot = new CompositionRoot();
var status = await new CliApplicationBuilder()
.AddCommands(GetAllCommandTypes())
.SetExecutableName(ExecutableName)

@ -1,23 +0,0 @@
using Autofac;
namespace Recyclarr;
public sealed class ServiceLocatorProxy : IServiceLocatorProxy
{
public ServiceLocatorProxy(ILifetimeScope container)
{
Container = container;
}
public ILifetimeScope Container { get; }
public T Resolve<T>() where T : notnull
{
return Container.Resolve<T>();
}
public void Dispose()
{
Container.Dispose();
}
}

@ -1,3 +1,4 @@
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
@ -16,7 +17,7 @@ public class ServiceConfigurationTest : IntegrationFixture
// default construct which should yield default values (invalid) for all required properties
var config = new ServiceConfiguration();
var validator = ServiceLocator.Resolve<IValidator<ServiceConfiguration>>();
var validator = Container.Resolve<IValidator<ServiceConfiguration>>();
var result = validator.Validate(config);
@ -45,7 +46,7 @@ public class ServiceConfigurationTest : IntegrationFixture
}
};
var validator = ServiceLocator.Resolve<IValidator<ServiceConfiguration>>();
var validator = Container.Resolve<IValidator<ServiceConfiguration>>();
var result = validator.Validate(config);

@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
@ -25,7 +26,7 @@ public class RadarrConfigurationTest : IntegrationFixture
}
};
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();
@ -37,7 +38,7 @@ public class RadarrConfigurationTest : IntegrationFixture
{
// default construct which should yield default values (invalid) for all required properties
var config = new RadarrConfiguration();
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
@ -79,7 +80,7 @@ public class RadarrConfigurationTest : IntegrationFixture
}
};
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
@ -10,7 +11,6 @@ using TestLibrary.AutoFixture;
using TrashLib.Repo;
using TrashLib.Services.Sonarr.ReleaseProfile;
using TrashLib.Services.Sonarr.ReleaseProfile.Guide;
using TrashLib.Startup;
namespace TrashLib.Tests.Sonarr.ReleaseProfile.Guide;
@ -21,7 +21,6 @@ public class LocalRepoSonarrGuideServiceTest
[Test, AutoMockData]
public void Get_custom_format_json_works(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] IAppPaths appPaths,
[Frozen] IRepoPaths repoPaths,
LocalRepoSonarrGuideService sut)
{
@ -40,11 +39,8 @@ public class LocalRepoSonarrGuideServiceTest
var mockData1 = MakeMockObject("first");
var mockData2 = MakeMockObject("second");
var baseDir = appPaths.RepoDirectory
.SubDirectory("docs")
.SubDirectory("json")
.SubDirectory("sonarr");
var baseDir = fs.CurrentDirectory().SubDirectory("files");
baseDir.Create();
fs.AddFile(baseDir.File("first.json").FullName, MockFileData(mockData1));
fs.AddFile(baseDir.File("second.json").FullName, MockFileData(mockData2));
@ -63,14 +59,11 @@ public class LocalRepoSonarrGuideServiceTest
[Test, AutoMockData]
public void Json_exceptions_do_not_interrupt_parsing_other_files(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] IAppPaths appPaths,
[Frozen] IRepoPaths repoPaths,
LocalRepoSonarrGuideService sut)
{
var rootPath = appPaths.RepoDirectory
.SubDirectory("docs")
.SubDirectory("json")
.SubDirectory("sonarr");
var rootPath = fs.CurrentDirectory().SubDirectory("files");
rootPath.Create();
var badData = "# comment";
var goodData = new ReleaseProfileData

@ -1,3 +1,4 @@
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
@ -22,7 +23,7 @@ public class SonarrConfigurationTest : IntegrationFixture
ReleaseProfiles = new[] {new ReleaseProfileConfig()}
};
var validator = ServiceLocator.Resolve<IValidator<SonarrConfiguration>>();
var validator = Container.Resolve<IValidator<SonarrConfiguration>>();
var result = validator.Validate(config);
@ -50,7 +51,7 @@ public class SonarrConfigurationTest : IntegrationFixture
}
};
var validator = ServiceLocator.Resolve<IValidator<SonarrConfiguration>>();
var validator = Container.Resolve<IValidator<SonarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();

@ -3,4 +3,5 @@ namespace TrashLib.Repo;
public interface IRepoPathsFactory
{
IRepoPaths Create();
RepoMetadata Metadata { get; }
}

@ -8,6 +8,8 @@ public class RepoPathsFactory : IRepoPathsFactory
private readonly IAppPaths _paths;
private readonly Lazy<RepoMetadata> _metadata;
public RepoMetadata Metadata => _metadata.Value;
public RepoPathsFactory(IRepoMetadataParser parser, IAppPaths paths)
{
_paths = paths;
@ -18,7 +20,6 @@ public class RepoPathsFactory : IRepoPathsFactory
{
return listOfDirectories
.Select(x => _paths.RepoDirectory.SubDirectory(x))
.Where(x => x.Exists)
.ToList();
}

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using Common;
using Common.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@ -17,8 +18,7 @@ internal class QualityGuideParser<T> where T : class
public ICollection<T> GetQualities(IEnumerable<IDirectoryInfo> jsonDirectories)
{
return jsonDirectories
.SelectMany(x => x.GetFiles("*.json"))
return JsonUtils.GetJsonFilesInDirectories(jsonDirectories, _log)
.Select(ParseQuality)
.NotNull()
.ToList();

@ -1,6 +1,7 @@
using System.IO.Abstractions;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Common;
using Common.Extensions;
using Newtonsoft.Json;
using Serilog;
@ -26,7 +27,7 @@ public class CustomFormatLoader : ICustomFormatLoader
IFileInfo collectionOfCustomFormats)
{
var categories = _categoryParser.Parse(collectionOfCustomFormats).AsReadOnly();
var jsonFiles = jsonPaths.SelectMany(x => x.GetFiles("*.json"));
var jsonFiles = JsonUtils.GetJsonFilesInDirectories(jsonPaths, _log);
return jsonFiles.ToObservable()
.Select(x => Observable.Defer(() => LoadJsonFromFile(x, categories)))
.Merge(8)

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using Common;
using Common.Extensions;
using MoreLinq;
using Newtonsoft.Json;
@ -47,8 +48,7 @@ public class LocalRepoSonarrGuideService : ISonarrGuideService
{
var converter = new TermDataConverter();
var paths = _pathsFactory.Create();
var tasks = paths.SonarrReleaseProfilePaths
.SelectMany(x => x.GetFiles("*.json"))
var tasks = JsonUtils.GetJsonFilesInDirectories(paths.SonarrReleaseProfilePaths, _log)
.Select(x => LoadAndParseFile(x, converter));
var data = Task.WhenAll(tasks).Result

Loading…
Cancel
Save