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.
pull/356/head
Robert Dailey 4 months ago
parent 06a00cdfef
commit 1ae34f9e4d

@ -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<AutofacTypeRegistrar>().As<ITypeRegistrar>();
builder.RegisterType<CommandApp>();
builder.RegisterType<CommandSetupInterceptor>().As<ICommandInterceptor>();

@ -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<SyncCommand>("sync")
.WithExample("sync", "radarr", "--instance", "movies")
@ -40,4 +42,27 @@ public static class CliSetup
delete.AddCommand<DeleteCustomFormatsCommand>("custom-formats");
});
}
public static async Task<int> Run(ILifetimeScope scope, IEnumerable<string> args)
{
var app = scope.Resolve<CommandApp>();
app.Configure(config =>
{
#if DEBUG
config.ValidateExamples();
#endif
config.ConfigureConsole(scope.Resolve<IAnsiConsole>());
config.PropagateExceptions();
config.UseStrictParsing();
config.SetApplicationName("recyclarr");
config.SetApplicationVersion(
$"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})");
AddCommands(config);
});
return await app.RunAsync(args);
}
}

@ -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<CommandApp>();
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)
{

@ -40,7 +40,7 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="**\Data\*" />
<EmbeddedResource Include="**\Data\**" />
</ItemGroup>
<ItemGroup>

@ -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<ITrashGuidesRepo>().Path);
}
[Test]
public async Task List_custom_format_radarr_score_sets()
{
var repo = Resolve<ITrashGuidesRepo>();
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<CommandRuntimeException>();
}
}

@ -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"
}
}
]
}

@ -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
}
}
]
}

@ -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<string, string>? _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));
}
}
}

@ -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);
}
}

@ -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<IAppPaths>();
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<ILifetimeScope>(() =>
{
@ -62,7 +60,6 @@ public abstract class IntegrationTestFixture : IDisposable
builder.RegisterInstance(Fs).As<IFileSystem>();
builder.RegisterInstance(Console).As<IAnsiConsole>();
builder.RegisterInstance(Logger).As<ILogger>();
builder.RegisterInstance(Paths).As<IAppPaths>();
builder.RegisterMockFor<IEnvironment>();
builder.RegisterMockFor<IGitRepository>();
@ -74,6 +71,20 @@ public abstract class IntegrationTestFixture : IDisposable
});
}
[SetUp]
public void Setup()
{
var appDataSetup = Resolve<DefaultAppDataSetup>();
appDataSetup.SetAppDataDirectoryOverride(
Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr").FullName);
}
[TearDown]
public void Teardown()
{
System.Console.Write(Console.Output);
}
protected T Resolve<T>() where T : notnull
{
return Container.Resolve<T>();

@ -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);
}

Loading…
Cancel
Save