Also: - New "migration" system which helps perform upgrade steps if needed - Migration step to rename `trash.yml` to `recyclarr.yml` - Fixup the `create-config` templatepull/63/head
parent
7beba7eea6
commit
bede26c213
@ -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$"));
|
||||
}
|
||||
}
|
@ -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?)");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue