refactor: Refactor cache logic

- `CustomFormatCache` is now a utility class for updating cache entries.
- `CustomFormatCacheData` is now what `CustomFormatCache` used to be
  (data object used for serialization).
- `CustomFormatCachePersister` is now specific to custom formats. Future
  cache types will have their own persister implementation.
pull/231/head
Robert Dailey 1 year ago
parent 7f6a5a2ff6
commit e99f4cb766

@ -6,7 +6,7 @@ public class CacheAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// Clients must register their own implementation of ICacheStoragePath
builder.RegisterType<CacheStoragePath>().As<ICacheStoragePath>();
builder.RegisterType<ServiceCache>().As<IServiceCache>();
}
}

@ -1,35 +0,0 @@
using System.Text.Json;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Cache;
public class CachePersister(ILogger log, IServiceCache serviceCache) : ICachePersister
{
public CustomFormatCache Load(IServiceConfiguration config)
{
var cache = serviceCache.Load<CustomFormatCache>(config);
if (cache == null)
{
log.Debug("Custom format cache does not exist; proceeding without it");
return new CustomFormatCache();
}
// If the version is higher OR lower, we invalidate the cache. It means there's an
// incompatibility that we do not support.
if (cache.Version != CustomFormatCache.LatestVersion)
{
log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
cache.Version, CustomFormatCache.LatestVersion);
throw new CacheException("Version mismatch");
}
return cache;
}
public void Save(IServiceConfiguration config, CustomFormatCache cache)
{
log.Debug("Saving Cache with {Mappings}", JsonSerializer.Serialize(cache.TrashIdMappings));
serviceCache.Save(cache, config);
}
}

@ -5,7 +5,7 @@ using System.Text;
using Recyclarr.Config.Models;
using Recyclarr.Platform;
namespace Recyclarr.Cli.Console.Helpers;
namespace Recyclarr.Cli.Cache;
public class CacheStoragePath(IAppPaths paths) : ICacheStoragePath
{

@ -1,64 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Pipelines.CustomFormat;
using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.Cli.Cache;
[CacheObjectName("custom-format-cache")]
public record CustomFormatCache
{
public const int LatestVersion = 1;
public int Version { get; init; } = LatestVersion;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public string? InstanceName { get; init; }
public IReadOnlyList<TrashIdMapping> TrashIdMappings { get; init; } = new List<TrashIdMapping>();
public CustomFormatCache Update(CustomFormatTransactionData transactions)
{
// Assume that RemoveStale() is called before this method, and that TrashIdMappings contains existing CFs
// in the remote service that we want to keep and update.
var existingCfs = transactions.UpdatedCustomFormats
.Concat(transactions.UnchangedCustomFormats)
.Concat(transactions.NewCustomFormats);
return this with
{
TrashIdMappings = TrashIdMappings
.DistinctBy(x => x.CustomFormatId)
.Where(x => transactions.DeletedCustomFormats.All(y => y.CustomFormatId != x.CustomFormatId))
.FullOuterJoin(existingCfs, JoinType.Hash,
l => l.CustomFormatId,
r => r.Id,
// Keep existing service CFs, even if they aren't in user config
l => l,
// Add a new mapping for CFs in user's config
r => new TrashIdMapping(r.TrashId, r.Name, r.Id),
// Update existing mappings for CFs in user's config
(l, r) => l with {TrashId = r.TrashId, CustomFormatName = r.Name})
.Where(x => x.CustomFormatId != 0)
.OrderBy(x => x.CustomFormatId)
.ToList()
};
}
public CustomFormatCache RemoveStale(IEnumerable<CustomFormatData> serviceCfs)
{
return this with
{
TrashIdMappings = TrashIdMappings
.Where(x => x.CustomFormatId != 0 && serviceCfs.Any(y => y.Id == x.CustomFormatId))
.ToList()
};
}
public int? FindId(CustomFormatData cf)
{
return TrashIdMappings.FirstOrDefault(c => c.TrashId == cf.TrashId)?.CustomFormatId;
}
}
public record TrashIdMapping(string TrashId, string CustomFormatName, int CustomFormatId);

@ -1,7 +1,7 @@
using System.IO.Abstractions;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Console.Helpers;
namespace Recyclarr.Cli.Cache;
public interface ICacheStoragePath
{

@ -2,7 +2,6 @@ using System.IO.Abstractions;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Common.Extensions;
using Recyclarr.Config.Models;
using Recyclarr.Json;
@ -23,12 +22,10 @@ public partial class ServiceCache(ICacheStoragePath storagePath, ILogger log) :
return null;
}
using var stream = path.OpenText();
var json = stream.ReadToEnd();
try
{
return JsonSerializer.Deserialize<T>(json, _jsonSettings);
using var stream = path.OpenRead();
return JsonSerializer.Deserialize<T>(stream, _jsonSettings);
}
catch (JsonException e)
{

@ -4,7 +4,6 @@ using Autofac;
using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration;
@ -58,7 +57,6 @@ public static class CompositionRoot
builder.RegisterModule<PlatformAutofacModule>();
builder.RegisterModule<CommonAutofacModule>();
builder.RegisterType<CacheStoragePath>().As<ICacheStoragePath>();
builder.RegisterType<FileSystem>().As<IFileSystem>();
builder.Register(_ => new ResourceDataReader(thisAssembly)).As<IResourceDataReader>();

@ -0,0 +1,46 @@
using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public class CustomFormatCache(IEnumerable<TrashIdMapping> mappings)
{
private List<TrashIdMapping> _mappings = mappings.ToList(); // Deep clone with ToList()
public IReadOnlyList<TrashIdMapping> Mappings => _mappings;
public void Update(CustomFormatTransactionData transactions)
{
// Assume that RemoveStale() is called before this method, and that TrashIdMappings contains existing CFs
// in the remote service that we want to keep and update.
var existingCfs = transactions.UpdatedCustomFormats
.Concat(transactions.UnchangedCustomFormats)
.Concat(transactions.NewCustomFormats);
_mappings = _mappings
.DistinctBy(x => x.CustomFormatId)
.Where(x => transactions.DeletedCustomFormats.All(y => y.CustomFormatId != x.CustomFormatId))
.FullOuterJoin(existingCfs, JoinType.Hash,
l => l.CustomFormatId,
r => r.Id,
// Keep existing service CFs, even if they aren't in user config
l => l,
// Add a new mapping for CFs in user's config
r => new TrashIdMapping(r.TrashId, r.Name, r.Id),
// Update existing mappings for CFs in user's config
(l, r) => l with {TrashId = r.TrashId, CustomFormatName = r.Name})
.Where(x => x.CustomFormatId != 0)
.OrderBy(x => x.CustomFormatId)
.ToList();
}
public void RemoveStale(IEnumerable<CustomFormatData> serviceCfs)
{
_mappings.RemoveAll(x => x.CustomFormatId == 0 || serviceCfs.All(y => y.Id != x.CustomFormatId));
}
public int? FindId(CustomFormatData cf)
{
return _mappings.Find(c => c.TrashId == cf.TrashId)?.CustomFormatId;
}
}

@ -0,0 +1,11 @@
using Recyclarr.Cli.Cache;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public record TrashIdMapping(string TrashId, string CustomFormatName, int CustomFormatId);
[CacheObjectName("custom-format-cache")]
public record CustomFormatCacheData(
int Version,
string InstanceName,
IReadOnlyCollection<TrashIdMapping> TrashIdMappings);

@ -0,0 +1,38 @@
using System.Text.Json;
using Recyclarr.Cli.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public class CustomFormatCachePersister(ILogger log, IServiceCache serviceCache) : ICustomFormatCachePersister
{
public const int LatestVersion = 1;
public CustomFormatCache Load(IServiceConfiguration config)
{
var cacheData = serviceCache.Load<CustomFormatCacheData>(config);
if (cacheData == null)
{
log.Debug("Custom format cache does not exist; proceeding without it");
cacheData = new CustomFormatCacheData(LatestVersion, config.InstanceName, []);
}
// If the version is higher OR lower, we invalidate the cache. It means there's an
// incompatibility that we do not support.
if (cacheData.Version != LatestVersion)
{
log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
cacheData.Version, LatestVersion);
throw new CacheException("Version mismatch");
}
return new CustomFormatCache(cacheData.TrashIdMappings);
}
public void Save(IServiceConfiguration config, CustomFormatCache cache)
{
var data = new CustomFormatCacheData(LatestVersion, config.InstanceName, cache.Mappings);
log.Debug("Saving Custom Format Cache with {Mappings}", JsonSerializer.Serialize(data.TrashIdMappings));
serviceCache.Save(data, config);
}
}

@ -1,8 +1,8 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Cache;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public interface ICachePersister
public interface ICustomFormatCachePersister
{
CustomFormatCache Load(IServiceConfiguration config);
void Save(IServiceConfiguration config, CustomFormatCache cache);

@ -1,6 +1,6 @@
using Autofac;
using Autofac.Extras.AggregateService;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
@ -12,7 +12,7 @@ public class CustomFormatAutofacModule : Module
{
builder.RegisterType<ProcessedCustomFormatCache>().As<IPipelineCache>().AsSelf().InstancePerLifetimeScope();
builder.RegisterType<CachePersister>().As<ICachePersister>();
builder.RegisterType<CustomFormatCachePersister>().As<ICustomFormatCachePersister>();
builder.RegisterType<CustomFormatDataLister>();
builder.RegisterAggregateService<ICustomFormatPipelinePhases>();

@ -1,6 +1,6 @@
using System.Collections.ObjectModel;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Settings;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.Common.Extensions;
@ -29,7 +29,7 @@ public record CustomFormatTransactionData
public class CustomFormatSyncPipeline(
ILogger log,
ICachePersister cachePersister,
ICustomFormatCachePersister cachePersister,
ICustomFormatPipelinePhases phases)
: ISyncPipeline
{
@ -46,7 +46,7 @@ public class CustomFormatSyncPipeline(
var serviceData = await phases.ApiFetchPhase.Execute(config);
cache = cache.RemoveStale(serviceData);
cache.RemoveStale(serviceData);
var transactions = phases.TransactionPhase.Execute(config, guideCfs, serviceData, cache);
@ -59,9 +59,7 @@ public class CustomFormatSyncPipeline(
await phases.ApiPersistencePhase.Execute(config, transactions);
cachePersister.Save(config, cache.Update(transactions) with
{
InstanceName = config.InstanceName
});
cache.Update(transactions);
cachePersister.Save(config, cache);
}
}

@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Common.Extensions;
using Recyclarr.Config.Models;
@ -47,7 +47,7 @@ public class CustomFormatTransactionPhase(ILogger log)
if (config.DeleteOldCustomFormats)
{
transactions.DeletedCustomFormats.AddRange(cache.TrashIdMappings
transactions.DeletedCustomFormats.AddRange(cache.Mappings
// Custom format must be in the cache but NOT in the user's config
.Where(map => guideCfs.All(cf => cf.TrashId != map.TrashId))
// Also, that cache-only CF must exist in the service (otherwise there is nothing to delete)

@ -1,5 +1,5 @@
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.Tests.TestLibrary;
@ -22,7 +22,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
var serviceData = Array.Empty<CustomFormatData>();
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var config = NewConfig.Radarr();
@ -58,7 +58,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "one"}
};
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var config = NewConfig.Radarr();
@ -97,13 +97,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "different2", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf1", "", 2)
}
};
]);
var config = NewConfig.Radarr();
@ -142,7 +138,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "different1", Id = 2}
};
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var config = NewConfig.Radarr() with
{
@ -179,7 +175,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "two", Id = 1}
};
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var config = NewConfig.Radarr() with
{
@ -213,13 +209,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "two", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf1", "one", 1)
}
};
]);
var config = NewConfig.Radarr() with
{
@ -259,13 +251,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf1", "one", 1)
}
};
]);
var config = NewConfig.Radarr() with
{
@ -298,7 +286,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var config = NewConfig.Radarr() with
{
@ -328,13 +316,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf1", "one", 1)
}
};
]);
var config = NewConfig.Radarr() with
{
@ -361,13 +345,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "two", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf2", "two", 2)
}
};
]);
var config = NewConfig.Radarr() with
{
@ -397,13 +377,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "two", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf2", "two", 2)
}
};
]);
var config = NewConfig.Radarr() with
{
@ -430,13 +406,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
new CustomFormatData {Name = "two", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf2", "two", 2)
}
};
]);
var config = NewConfig.Radarr();
@ -457,13 +429,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
var serviceData = Array.Empty<CustomFormatData>();
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("cf2", "two", 200)
}
};
]);
var config = NewConfig.Radarr();

@ -1,85 +0,0 @@
using System.Collections.ObjectModel;
using Recyclarr.Cli.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Cache;
[TestFixture]
public class CachePersisterTest
{
private sealed class Context
{
public Context()
{
var log = Substitute.For<ILogger>();
ServiceCache = Substitute.For<IServiceCache>();
Persister = new CachePersister(log, ServiceCache);
}
public CachePersister Persister { get; }
public IServiceCache ServiceCache { get; }
}
[TestCase(CustomFormatCache.LatestVersion - 1)]
[TestCase(CustomFormatCache.LatestVersion + 1)]
public void Throw_when_versions_mismatch(int versionToTest)
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCache
{
Version = versionToTest,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var act = () => ctx.Persister.Load(config);
act.Should().Throw<CacheException>();
}
[Test]
public void Accept_loaded_cache_when_versions_match()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCache
{
Version = CustomFormatCache.LatestVersion,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var result = ctx.Persister.Load(config);
result.Should().NotBeNull();
}
[Test]
public void Cache_is_valid_after_successful_load()
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
var config = Substitute.For<IServiceConfiguration>();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var result = ctx.Persister.Load(config);
result.Should().BeSameAs(testCfObj);
}
[Test]
public void Save_works_with_valid_cf_cache()
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
var config = Substitute.For<IServiceConfiguration>();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var result = ctx.Persister.Load(config);
ctx.Persister.Save(config, result);
ctx.ServiceCache.Received().Save(testCfObj, config);
}
}

@ -1,7 +1,7 @@
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Console.Helpers;
namespace Recyclarr.Cli.Tests.Cache;
[TestFixture]
public class CacheStoragePathTest

@ -1,5 +1,5 @@
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Tests.TestLibrary;
namespace Recyclarr.Cli.Tests.Cache;
@ -27,10 +27,10 @@ public class CustomFormatCacheTest
}
};
var cache = new CustomFormatCache();
var result = cache.Update(transactions);
var cache = new CustomFormatCache([]);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),
@ -55,18 +55,14 @@ public class CustomFormatCacheTest
}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("3", "three", 3),
new TrashIdMapping("4", "four", 4)
}
};
]);
var result = cache.Update(transactions);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),
@ -83,20 +79,16 @@ public class CustomFormatCacheTest
NewCf.Data("two", "2", 2)
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),
new TrashIdMapping("3", "three", 3),
new TrashIdMapping("4", "four", 4)
}
};
]);
var result = cache.RemoveStale(serviceCfs);
cache.RemoveStale(serviceCfs);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2)
@ -119,11 +111,11 @@ public class CustomFormatCacheTest
}
};
var cache = new CustomFormatCache();
var cache = new CustomFormatCache([]);
var result = cache.Update(transactions);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2)
@ -150,20 +142,16 @@ public class CustomFormatCacheTest
}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),
new TrashIdMapping("3", "three", 3),
new TrashIdMapping("4", "four", 4)
}
};
]);
var result = cache.Update(transactions);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one_new", 1),
new TrashIdMapping("2", "two_new", 2),
@ -177,21 +165,17 @@ public class CustomFormatCacheTest
{
var transactions = new CustomFormatTransactionData();
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("12", "one2", 1),
new TrashIdMapping("2", "two", 2),
new TrashIdMapping("3", "three", 3),
new TrashIdMapping("4", "four", 4)
}
};
]);
var result = cache.Update(transactions);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),
@ -205,20 +189,16 @@ public class CustomFormatCacheTest
{
var transactions = new CustomFormatTransactionData();
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
var cache = new CustomFormatCache([
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("3", "three", 3),
new TrashIdMapping("4", "four", 4),
new TrashIdMapping("2", "two", 2)
}
};
]);
var result = cache.Update(transactions);
cache.Update(transactions);
result.TrashIdMappings.Should().BeEquivalentTo(new[]
cache.Mappings.Should().BeEquivalentTo(new[]
{
new TrashIdMapping("1", "one", 1),
new TrashIdMapping("2", "two", 2),

@ -1,7 +1,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Cache;
@ -205,7 +205,7 @@ public class ServiceCacheTest
fs.AddFile("cacheFile.json", new MockFileData(cacheJson));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
var result = sut.Load<CustomFormatCache>(config);
var result = sut.Load<CustomFormatCacheData>(config);
result.Should().BeEquivalentTo(new
{

@ -0,0 +1,70 @@
using AutoFixture;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Pipelines.CustomFormat.Cache;
[TestFixture]
public class CustomFormatCachePersisterTest
{
[TestCase(CustomFormatCachePersister.LatestVersion - 1)]
[TestCase(CustomFormatCachePersister.LatestVersion + 1)]
public void Throw_when_versions_mismatch(int versionToTest)
{
var fixture = NSubstituteFixture.Create();
var serviceCache = fixture.Freeze<IServiceCache>();
var sut = fixture.Create<CustomFormatCachePersister>();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCacheData(versionToTest, "",
[
new TrashIdMapping("", "", 5)
]);
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
var act = () => sut.Load(config);
act.Should().Throw<CacheException>();
}
[Test]
public void Accept_loaded_cache_when_versions_match()
{
var fixture = NSubstituteFixture.Create();
var serviceCache = fixture.Freeze<IServiceCache>();
var sut = fixture.Create<CustomFormatCachePersister>();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCacheData(CustomFormatCachePersister.LatestVersion, "",
[
new TrashIdMapping("", "", 5)
]);
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
var result = sut.Load(config);
result.Should().NotBeNull();
}
[Test]
public void Cache_is_valid_after_successful_load()
{
var fixture = NSubstituteFixture.Create();
var serviceCache = fixture.Freeze<IServiceCache>();
var sut = fixture.Create<CustomFormatCachePersister>();
TrashIdMapping[] mappings = [new TrashIdMapping("abc", "name", 123)];
var config = Substitute.For<IServiceConfiguration>();
serviceCache.Load<CustomFormatCacheData>(config).Returns(new CustomFormatCacheData(1, "", mappings));
var result = sut.Load(config);
result.Mappings.Should().BeEquivalentTo(mappings);
}
}
Loading…
Cancel
Save