From 1ae34f9e4da127deb1ad63f685f65c9a51c748a3 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sun, 27 Oct 2024 09:21:15 -0500 Subject: [PATCH] refactor: Support high level CLI integration tests Code refactored to allow easier testing at the command level. The initial test added verifies the custom-format list behavior. --- src/Recyclarr.Cli/CompositionRoot.cs | 2 + src/Recyclarr.Cli/Console/CliSetup.cs | 27 ++++++++- src/Recyclarr.Cli/Program.cs | 20 +------ tests/Directory.Build.props | 2 +- .../CliCommandIntegrationTest.cs | 55 +++++++++++++++++++ .../Data/RadarrCustomFormats/10bit.json | 29 ++++++++++ .../Data/RadarrCustomFormats/4k-remaster.json | 37 +++++++++++++ .../RemoteRepoFileMapper.cs | 36 ++++++++++++ .../TrashRepoFileMapper.cs | 11 ++++ .../IntegrationTestFixture.cs | 19 +++++-- .../MockFileSystemExtensions.cs | 1 + 11 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 tests/Recyclarr.Cli.IntegrationTests/CliCommandIntegrationTest.cs create mode 100644 tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/10bit.json create mode 100644 tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/4k-remaster.json create mode 100644 tests/Recyclarr.Cli.TestLibrary/RemoteRepoFileMapper.cs create mode 100644 tests/Recyclarr.Cli.TestLibrary/TrashRepoFileMapper.cs diff --git a/src/Recyclarr.Cli/CompositionRoot.cs b/src/Recyclarr.Cli/CompositionRoot.cs index 0615cf04..298f3cca 100644 --- a/src/Recyclarr.Cli/CompositionRoot.cs +++ b/src/Recyclarr.Cli/CompositionRoot.cs @@ -16,6 +16,7 @@ using Recyclarr.Cli.Processors.Sync; using Recyclarr.Common; using Recyclarr.Logging; using Serilog.Core; +using Spectre.Console; using Spectre.Console.Cli; namespace Recyclarr.Cli; @@ -94,6 +95,7 @@ public static class CompositionRoot private static void CliRegistrations(ContainerBuilder builder) { + builder.RegisterInstance(AnsiConsole.Console); builder.RegisterType().As(); builder.RegisterType(); builder.RegisterType().As(); diff --git a/src/Recyclarr.Cli/Console/CliSetup.cs b/src/Recyclarr.Cli/Console/CliSetup.cs index c8d88aa9..c851a44e 100644 --- a/src/Recyclarr.Cli/Console/CliSetup.cs +++ b/src/Recyclarr.Cli/Console/CliSetup.cs @@ -1,11 +1,13 @@ +using Autofac; using Recyclarr.Cli.Console.Commands; +using Spectre.Console; using Spectre.Console.Cli; namespace Recyclarr.Cli.Console; public static class CliSetup { - public static void Commands(IConfigurator cli) + private static void AddCommands(IConfigurator cli) { cli.AddCommand("sync") .WithExample("sync", "radarr", "--instance", "movies") @@ -40,4 +42,27 @@ public static class CliSetup delete.AddCommand("custom-formats"); }); } + + public static async Task Run(ILifetimeScope scope, IEnumerable args) + { + var app = scope.Resolve(); + app.Configure(config => + { + #if DEBUG + config.ValidateExamples(); + #endif + + config.ConfigureConsole(scope.Resolve()); + config.PropagateExceptions(); + config.UseStrictParsing(); + + config.SetApplicationName("recyclarr"); + config.SetApplicationVersion( + $"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})"); + + AddCommands(config); + }); + + return await app.RunAsync(args); + } } diff --git a/src/Recyclarr.Cli/Program.cs b/src/Recyclarr.Cli/Program.cs index dfb50ace..43ba0435 100644 --- a/src/Recyclarr.Cli/Program.cs +++ b/src/Recyclarr.Cli/Program.cs @@ -3,7 +3,6 @@ using Autofac; using Recyclarr.Cli.Console; using Recyclarr.Cli.Processors; using Recyclarr.Cli.Processors.ErrorHandling; -using Spectre.Console.Cli; namespace Recyclarr.Cli; @@ -19,24 +18,7 @@ internal static class Program try { - var app = scope.Resolve(); - app.Configure(config => - { - #if DEBUG - config.ValidateExamples(); - #endif - - config.PropagateExceptions(); - config.UseStrictParsing(); - - config.SetApplicationName("recyclarr"); - config.SetApplicationVersion( - $"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})"); - - CliSetup.Commands(config); - }); - - return await app.RunAsync(args); + return await CliSetup.Run(scope, args); } catch (Exception e) { diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index bac1097e..d4853593 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -40,7 +40,7 @@ - + diff --git a/tests/Recyclarr.Cli.IntegrationTests/CliCommandIntegrationTest.cs b/tests/Recyclarr.Cli.IntegrationTests/CliCommandIntegrationTest.cs new file mode 100644 index 00000000..67fab971 --- /dev/null +++ b/tests/Recyclarr.Cli.IntegrationTests/CliCommandIntegrationTest.cs @@ -0,0 +1,55 @@ +using System.IO.Abstractions; +using Recyclarr.Cli.Console; +using Recyclarr.Cli.TestLibrary; +using Recyclarr.Repo; +using Recyclarr.TestLibrary; +using Spectre.Console.Cli; + +namespace Recyclarr.Cli.IntegrationTests; + +internal class CliCommandIntegrationTest : CliIntegrationFixture +{ + private static readonly TrashRepoFileMapper Mapper = new(); + + [OneTimeSetUp] + public static async Task OneTimeSetup() + { + await Mapper.DownloadFiles( + "metadata.json", + "docs/Radarr/Radarr-collection-of-custom-formats.md"); + } + + [SetUp] + public void MapFiles() + { + Mapper.AddToFilesystem(Fs, Resolve().Path); + } + + [Test] + public async Task List_custom_format_radarr_score_sets() + { + var repo = Resolve(); + var cfPath = repo.Path.SubDirectory("docs/json/radarr/cf"); + string[] cfs = ["4k-remaster.json", "10bit.json"]; + + foreach (var cf in cfs) + { + Fs.AddFileFromEmbeddedResource( + cfPath.File(cf), + typeof(CliCommandIntegrationTest), + $"Data/RadarrCustomFormats/{cf}"); + } + + var exitCode = await CliSetup.Run(Container, ["list", "custom-formats", "radarr", "--score-sets"]); + + exitCode.Should().Be(0); + Console.Output.Should().ContainAll("default", "sqp-1-1080p", "sqp-1-2160p"); + } + + [Test] + public async Task List_custom_format_score_sets_fails_without_service_type() + { + var act = () => CliSetup.Run(Container, ["list", "custom-formats", "--score-sets"]); + await act.Should().ThrowAsync(); + } +} diff --git a/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/10bit.json b/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/10bit.json new file mode 100644 index 00000000..ad5a42de --- /dev/null +++ b/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/10bit.json @@ -0,0 +1,29 @@ +{ + "trash_id": "a5d148168c4506b55cf53984107c396e", + "trash_scores": { + "sqp-1-1080p": -10000, + "sqp-1-2160p": -10000 + }, + "name": "10bit", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "10bit", + "implementation": "ReleaseTitleSpecification", + "negate": false, + "required": false, + "fields": { + "value": "10[.-]?bit" + } + }, + { + "name": "hi10p", + "implementation": "ReleaseTitleSpecification", + "negate": false, + "required": false, + "fields": { + "value": "hi10p" + } + } + ] +} diff --git a/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/4k-remaster.json b/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/4k-remaster.json new file mode 100644 index 00000000..4429ba0b --- /dev/null +++ b/tests/Recyclarr.Cli.IntegrationTests/Data/RadarrCustomFormats/4k-remaster.json @@ -0,0 +1,37 @@ +{ + "trash_id": "eca37840c13c6ef2dd0262b141a5482f", + "trash_scores": { + "default": 25 + }, + "name": "4K Remaster", + "includeCustomFormatWhenRenaming": true, + "specifications": [ + { + "name": "Remaster", + "implementation": "ReleaseTitleSpecification", + "negate": false, + "required": true, + "fields": { + "value": "Remaster" + } + }, + { + "name": "4K", + "implementation": "ReleaseTitleSpecification", + "negate": false, + "required": true, + "fields": { + "value": "4k" + } + }, + { + "name": "Not 4K Resolution", + "implementation": "ResolutionSpecification", + "negate": true, + "required": true, + "fields": { + "value": 2160 + } + } + ] +} diff --git a/tests/Recyclarr.Cli.TestLibrary/RemoteRepoFileMapper.cs b/tests/Recyclarr.Cli.TestLibrary/RemoteRepoFileMapper.cs new file mode 100644 index 00000000..3dd4c92c --- /dev/null +++ b/tests/Recyclarr.Cli.TestLibrary/RemoteRepoFileMapper.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using System.Reactive.Linq; +using Flurl.Http; + +namespace Recyclarr.Cli.TestLibrary; + +public class RemoteRepoFileMapper +{ + private Dictionary? _guideDataCache; + + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings")] + public async Task DownloadFiles(string urlPrefix, params string[] repoFilePaths) + { + var dictionary = await repoFilePaths.ToObservable() + .Select(x => Observable.FromAsync(async ct => + { + var content = await $"{urlPrefix}/{x}".GetStringAsync(cancellationToken: ct); + return (file: x, content); + })) + .Merge(8) + .ToList(); + + _guideDataCache = dictionary.ToDictionary(x => x.file, x => x.content); + } + + public void AddToFilesystem(MockFileSystem fs, IDirectoryInfo containingDir) + { + ArgumentNullException.ThrowIfNull(_guideDataCache); + + foreach (var (file, content) in _guideDataCache) + { + fs.AddFile(containingDir.File(file), new MockFileData(content)); + } + } +} diff --git a/tests/Recyclarr.Cli.TestLibrary/TrashRepoFileMapper.cs b/tests/Recyclarr.Cli.TestLibrary/TrashRepoFileMapper.cs new file mode 100644 index 00000000..07bc1f51 --- /dev/null +++ b/tests/Recyclarr.Cli.TestLibrary/TrashRepoFileMapper.cs @@ -0,0 +1,11 @@ +namespace Recyclarr.Cli.TestLibrary; + +public class TrashRepoFileMapper : RemoteRepoFileMapper +{ + private const string RepoUrlPrefix = "https://raw.githubusercontent.com/TRaSH-Guides/Guides/refs/heads/master"; + + public async Task DownloadFiles(params string[] repoFilePaths) + { + await base.DownloadFiles(RepoUrlPrefix, repoFilePaths); + } +} diff --git a/tests/Recyclarr.IntegrationTests/IntegrationTestFixture.cs b/tests/Recyclarr.IntegrationTests/IntegrationTestFixture.cs index 40d8583b..587695f2 100644 --- a/tests/Recyclarr.IntegrationTests/IntegrationTestFixture.cs +++ b/tests/Recyclarr.IntegrationTests/IntegrationTestFixture.cs @@ -19,7 +19,7 @@ public abstract class IntegrationTestFixture : IDisposable protected MockFileSystem Fs { get; } protected TestConsole Console { get; } = new(); protected TestableLogger Logger { get; } = new(); - protected IAppPaths Paths { get; } + protected IAppPaths Paths => Resolve(); protected IntegrationTestFixture() { @@ -28,8 +28,6 @@ public abstract class IntegrationTestFixture : IDisposable CreateDefaultTempDir = false }); - Paths = new AppPaths(Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr")); - // Use Lazy because we shouldn't invoke virtual methods at construction time _container = new Lazy(() => { @@ -62,7 +60,6 @@ public abstract class IntegrationTestFixture : IDisposable builder.RegisterInstance(Fs).As(); builder.RegisterInstance(Console).As(); builder.RegisterInstance(Logger).As(); - builder.RegisterInstance(Paths).As(); builder.RegisterMockFor(); builder.RegisterMockFor(); @@ -74,6 +71,20 @@ public abstract class IntegrationTestFixture : IDisposable }); } + [SetUp] + public void Setup() + { + var appDataSetup = Resolve(); + appDataSetup.SetAppDataDirectoryOverride( + Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr").FullName); + } + + [TearDown] + public void Teardown() + { + System.Console.Write(Console.Output); + } + protected T Resolve() where T : notnull { return Container.Resolve(); diff --git a/tests/Recyclarr.TestLibrary/MockFileSystemExtensions.cs b/tests/Recyclarr.TestLibrary/MockFileSystemExtensions.cs index eb39e05b..dd56b832 100644 --- a/tests/Recyclarr.TestLibrary/MockFileSystemExtensions.cs +++ b/tests/Recyclarr.TestLibrary/MockFileSystemExtensions.cs @@ -29,6 +29,7 @@ public static class MockFileSystemExtensions Type typeInAssembly, string embeddedResourcePath) { + embeddedResourcePath = embeddedResourcePath.Replace("/", "."); var resourcePath = $"{typeInAssembly.Namespace}.{embeddedResourcePath}"; fs.AddFileFromEmbeddedResource(path, typeInAssembly.Assembly, resourcePath); }