feat: Default config is now `recyclarr.yml`

Also:

- New "migration" system which helps perform upgrade steps if needed
- Migration step to rename `trash.yml` to `recyclarr.yml`
- Fixup the `create-config` template
pull/63/head
Robert Dailey 2 years ago
parent 7beba7eea6
commit bede26c213

@ -2,8 +2,8 @@
using CliFx.Infrastructure;
using NSubstitute;
using NUnit.Framework;
using Serilog;
using Recyclarr.Command;
using Serilog;
// ReSharper disable MethodHasAsyncOverload
@ -22,8 +22,8 @@ public class CreateConfigCommandTest
await cmd.ExecuteAsync(Substitute.For<IConsole>()).ConfigureAwait(false);
filesystem.File.Received().Exists(Arg.Is<string>(s => s.EndsWith("trash.yml")));
filesystem.File.Received().WriteAllText(Arg.Is<string>(s => s.EndsWith("trash.yml")), Arg.Any<string>());
filesystem.File.Received().Exists(Arg.Is<string>(s => s.EndsWith("recyclarr.yml")));
filesystem.File.Received().WriteAllText(Arg.Is<string>(s => s.EndsWith("recyclarr.yml")), Arg.Any<string>());
}
[Test]

@ -0,0 +1,95 @@
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Migration;
using Serilog;
namespace Recyclarr.Tests.Migration;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class MigrationExecutorTest
{
[Test]
public void Step_not_executed_if_check_returns_false()
{
var step = Substitute.For<IMigrationStep>();
var executor = new MigrationExecutor(new[] {step}, Substitute.For<ILogger>());
step.CheckIfNeeded().Returns(false);
executor.PerformAllMigrationSteps();
step.Received().CheckIfNeeded();
step.DidNotReceive().Execute(Arg.Any<ILogger>());
}
[Test]
public void Step_executed_if_check_returns_true()
{
var step = Substitute.For<IMigrationStep>();
var executor = new MigrationExecutor(new[] {step}, Substitute.For<ILogger>());
step.CheckIfNeeded().Returns(true);
executor.PerformAllMigrationSteps();
step.Received().CheckIfNeeded();
step.Received().Execute(Arg.Any<ILogger>());
}
[Test]
public void Steps_executed_in_ascending_order()
{
var steps = new[]
{
Substitute.For<IMigrationStep>(),
Substitute.For<IMigrationStep>(),
Substitute.For<IMigrationStep>()
};
steps[0].Order.Returns(20);
steps[1].Order.Returns(10);
steps[2].Order.Returns(30);
var executor = new MigrationExecutor(steps, Substitute.For<ILogger>());
executor.PerformAllMigrationSteps();
Received.InOrder(() =>
{
steps[1].CheckIfNeeded();
steps[0].CheckIfNeeded();
steps[2].CheckIfNeeded();
});
}
[Test]
public void Exception_converted_to_migration_exception()
{
var step = Substitute.For<IMigrationStep>();
var executor = new MigrationExecutor(new[] {step}, Substitute.For<ILogger>());
step.CheckIfNeeded().Returns(true);
step.When(x => x.Execute(Arg.Any<ILogger>())).Throw(new ArgumentException("test message"));
var act = () => executor.PerformAllMigrationSteps();
act.Should().Throw<MigrationException>().Which.FailureReason.Should().Be("test message");
}
[Test]
public void Migration_exceptions_are_not_converted()
{
var step = Substitute.For<IMigrationStep>();
var executor = new MigrationExecutor(new[] {step}, Substitute.For<ILogger>());
var exception = new MigrationException("a", "b");
step.CheckIfNeeded().Returns(true);
step.When(x => x.Execute(Arg.Any<ILogger>())).Throw(exception);
var act = () => executor.PerformAllMigrationSteps();
act.Should().Throw<MigrationException>().Which.Should().Be(exception);
}
}

@ -0,0 +1,61 @@
using System.IO.Abstractions.TestingHelpers;
using System.Text.RegularExpressions;
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Migration;
using Recyclarr.Migration.Steps;
using Serilog;
using TestLibrary.AutoFixture;
namespace Recyclarr.Tests.Migration.Steps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class MigrateTrashYmlTest
{
private static readonly string BasePath = AppContext.BaseDirectory;
[Test, AutoMockData]
public void Migration_check_returns_true_if_trash_yml_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
MigrateTrashYml sut)
{
fs.AddFile(Path.Combine(BasePath, "trash.yml"), MockFileData.NullObject);
sut.CheckIfNeeded().Should().BeTrue();
}
[Test, AutoMockData]
public void Migration_check_returns_false_if_trash_yml_doesnt_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
MigrateTrashYml sut)
{
sut.CheckIfNeeded().Should().BeFalse();
}
[Test, AutoMockData]
public void Migration_throws_if_recyclarr_yml_already_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
MigrateTrashYml sut)
{
fs.AddFile(Path.Combine(BasePath, "recyclarr.yml"), MockFileData.NullObject);
var act = () => sut.Execute(Substitute.For<ILogger>());
act.Should().Throw<MigrationException>().WithMessage("*already exist*");
}
[Test, AutoMockData]
public void Migration_success(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
MigrateTrashYml sut)
{
const string expectedData = "fake contents";
fs.AddFile(Path.Combine(BasePath, "trash.yml"), expectedData);
sut.Execute(Substitute.For<ILogger>());
fs.AllFiles.Should().ContainSingle(x => Regex.IsMatch(x, @"[/\\]recyclarr\.yml$"));
}
}

@ -5,7 +5,7 @@ internal static class AppPaths
public static string AppDataPath { get; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "trash-updater");
public static string DefaultConfigPath { get; } = Path.Combine(AppContext.BaseDirectory, "trash.yml");
public static string DefaultConfigPath { get; } = Path.Combine(AppContext.BaseDirectory, "recyclarr.yml");
public static string DefaultSettingsPath { get; } = Path.Combine(AppDataPath, "settings.yml");

@ -25,14 +25,14 @@ public class CreateConfigCommand : ICommand
[CommandOption("path", 'p', Description =
"Path where the new YAML file should be created. Must include the filename (e.g. path/to/config.yml). " +
"File must not already exist. If not specified, uses the default path of `trash.yml` right next to the " +
"File must not already exist. If not specified, uses the default path of `recyclarr.yml` right next to the " +
"executable.")]
public string Path { get; [UsedImplicitly] set; } = AppPaths.DefaultConfigPath;
public ValueTask ExecuteAsync(IConsole console)
{
var reader = new ResourceDataReader(typeof(Program));
var ymlData = reader.ReadData("trash-config-template.yml");
var ymlData = reader.ReadData("config-template.yml");
if (_fileSystem.File.Exists(Path))
{

@ -36,7 +36,7 @@ public abstract class ServiceCommand : ICommand, IServiceCommand
[CommandOption("config", 'c', Description =
"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 `trash.yml` in the same directory as the executable.")]
"If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")]
public ICollection<string> Config { get; [UsedImplicitly] set; } =
new List<string> {AppPaths.DefaultConfigPath};

@ -7,6 +7,7 @@ using CliFx.Infrastructure;
using Common;
using Recyclarr.Command.Helpers;
using Recyclarr.Config;
using Recyclarr.Migration;
using Serilog;
using Serilog.Core;
using TrashLib.Cache;
@ -88,6 +89,10 @@ public static class CompositionRoot
builder.RegisterType<CacheStoragePath>().As<ICacheStoragePath>();
builder.RegisterType<RepoUpdater>().As<IRepoUpdater>();
// Automatically register all migration steps
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).AssignableTo<IMigrationStep>();
builder.RegisterType<MigrationExecutor>().As<IMigrationExecutor>();
ConfigurationRegistrations(builder);
CommandRegistrations(builder);

@ -0,0 +1,6 @@
namespace Recyclarr.Migration;
public interface IMigrationExecutor
{
void PerformAllMigrationSteps();
}

@ -0,0 +1,11 @@
using Serilog;
namespace Recyclarr.Migration;
public interface IMigrationStep
{
int Order { get; }
string Description { get; }
bool CheckIfNeeded();
void Execute(ILogger log);
}

@ -0,0 +1,16 @@
namespace Recyclarr.Migration;
public class MigrationException : Exception
{
public MigrationException(string operationDescription, string failureReason)
{
OperationDescription = operationDescription;
FailureReason = failureReason;
}
public string OperationDescription { get; }
public string FailureReason { get; }
public override string Message =>
$"Fatal exception during migration step [Desc: {OperationDescription}] [Reason: {FailureReason}]";
}

@ -0,0 +1,41 @@
using Serilog;
namespace Recyclarr.Migration;
public class MigrationExecutor : IMigrationExecutor
{
private readonly ILogger _log;
private readonly List<IMigrationStep> _migrationSteps;
public MigrationExecutor(IEnumerable<IMigrationStep> migrationSteps, ILogger log)
{
_log = log;
_migrationSteps = migrationSteps.OrderBy(x => x.Order).ToList();
}
public void PerformAllMigrationSteps()
{
_log.Debug("Performing migration steps...");
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var step in _migrationSteps)
{
// Do not use LINQ to filter using CheckIfNeeded(). If it returns true, then 'Execute()' must be invoked to
// cause the necessary changes to happen. Those changes may be required in order for the *next* step's
// CheckIfNeeded() to work properly!
if (!step.CheckIfNeeded())
{
continue;
}
try
{
step.Execute(_log);
}
catch (Exception e) when (e is not MigrationException)
{
throw new MigrationException(step.Description, e.Message);
}
}
}
}

@ -0,0 +1,47 @@
using System.IO.Abstractions;
using JetBrains.Annotations;
using Serilog;
namespace Recyclarr.Migration.Steps;
/// <summary>
/// Rename `trash.yml` to `recyclarr.yml`.
/// </summary>
/// <remarks>
/// Implemented on 4/30/2022.
/// </remarks>
[UsedImplicitly]
public class MigrateTrashYml : IMigrationStep
{
private readonly IFileSystem _fileSystem;
private readonly string _oldConfigPath = Path.Combine(AppContext.BaseDirectory, "trash.yml");
// Do not use AppPaths class here since that may change yet again in the future and break this migration step.
private readonly string _newConfigPath = Path.Combine(AppContext.BaseDirectory, "recyclarr.yml");
public MigrateTrashYml(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public int Order => 1;
public string Description => "Migration from 'trash.yml' to 'recyclarr.yml'";
public bool CheckIfNeeded() => _fileSystem.File.Exists(_oldConfigPath);
public void Execute(ILogger log)
{
try
{
_fileSystem.File.Move(_oldConfigPath, _newConfigPath);
log.Information("Migration: Default configuration renamed from {Old} to {New}", _oldConfigPath,
_newConfigPath);
}
catch (IOException)
{
throw new MigrationException(Description,
"Unable to move due to IO Exception (does 'recyclarr.yml' already exist next to the executable?)");
}
}
}

@ -4,6 +4,7 @@ using Autofac;
using CliFx;
using CliFx.Infrastructure;
using Recyclarr.Command.Helpers;
using Recyclarr.Migration;
namespace Recyclarr;
@ -16,6 +17,10 @@ internal static class Program
public static async Task<int> Main()
{
_container = CompositionRoot.Setup();
var migration = _container.Resolve<IMigrationExecutor>();
migration.PerformAllMigrationSteps();
return await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.SetExecutableName(ExecutableName)

@ -28,6 +28,6 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="trash-config-template.yml" />
<EmbeddedResource Include="config-template.yml" />
</ItemGroup>
</Project>

Loading…
Cancel
Save