diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5ac81a..daf6a58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Do not exit when a YAML config has no sonarr or radarr section. +- Sonarr: Invalid release profile JSON files no longer cause the program to exit. Instead, it just + skips them and prints a warning to the user. (#87) ## [2.2.0] - 2022-06-03 diff --git a/src/TestLibrary/MockData.cs b/src/TestLibrary/MockData.cs new file mode 100644 index 00000000..7a88d70e --- /dev/null +++ b/src/TestLibrary/MockData.cs @@ -0,0 +1,17 @@ +using System.IO.Abstractions.TestingHelpers; +using Newtonsoft.Json; + +namespace TestLibrary; + +public static class MockData +{ + public static MockFileData FromJson(object json) + { + return new MockFileData(JsonConvert.SerializeObject(json)); + } + + public static MockFileData FromString(string data) + { + return new MockFileData(data); + } +} diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs index 3523a81d..ce1dfcb7 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs @@ -1,9 +1,12 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using FluentAssertions; using Newtonsoft.Json; using NSubstitute; using NUnit.Framework; +using TestLibrary; using TestLibrary.AutoFixture; using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile.Guide; @@ -48,4 +51,35 @@ public class LocalRepoReleaseProfileJsonParserTest mockData2 }); } + + [Test, AutoMockData] + public void Json_exceptions_do_not_interrupt_parsing_other_files( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen] IAppPaths paths, + LocalRepoReleaseProfileJsonParser sut) + { + paths.RepoDirectory.Returns(""); + var rootPath = fs.CurrentDirectory() + .SubDirectory("docs") + .SubDirectory("json") + .SubDirectory("sonarr"); + + var badData = "# comment"; + var goodData = new ReleaseProfileData + { + Name = "name", + TrashId = "123", + Required = new TermData[] + { + new() {Term = "abc"} + } + }; + + fs.AddFile(rootPath.File("0_bad_data.json").FullName, MockData.FromString(badData)); + fs.AddFile(rootPath.File("1_good_data.json").FullName, MockData.FromJson(goodData)); + + var results = sut.GetReleaseProfileData(); + + results.Should().BeEquivalentTo(new[] {goodData}); + } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs index 7a504063..05faf167 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs @@ -3,35 +3,61 @@ using Common.Extensions; using Common.FluentValidation; using MoreLinq; using Newtonsoft.Json; +using Serilog; namespace TrashLib.Sonarr.ReleaseProfile.Guide; public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService { - private readonly IFileSystem _fileSystem; + private readonly IFileSystem _fs; private readonly IAppPaths _paths; + private readonly ILogger _log; private readonly Lazy> _data; - public LocalRepoReleaseProfileJsonParser(IFileSystem fileSystem, IAppPaths paths) + public LocalRepoReleaseProfileJsonParser(IFileSystem fs, IAppPaths paths, ILogger log) { - _fileSystem = fileSystem; + _fs = fs; _paths = paths; + _log = log; _data = new Lazy>(GetReleaseProfileDataImpl); } private IEnumerable GetReleaseProfileDataImpl() { var converter = new TermDataConverter(); - var jsonDir = _fileSystem.Path.Combine(_paths.RepoDirectory, "docs/json/sonarr"); - var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json") - .Select(async f => - { - var json = await _fileSystem.File.ReadAllTextAsync(f); - return JsonConvert.DeserializeObject(json, converter); - }); + var jsonDir = _fs.Path.Combine(_paths.RepoDirectory, "docs/json/sonarr"); + var tasks = _fs.Directory.GetFiles(jsonDir, "*.json") + .Select(f => LoadAndParseFile(f, converter)); return Task.WhenAll(tasks).Result - .Choose(x => x is not null ? (true, x) : default); // Make non-nullable type + // Make non-nullable type and filter out null values + .Choose(x => x is not null ? (true, x) : default); + } + + private async Task LoadAndParseFile(string file, params JsonConverter[] converters) + { + try + { + var json = await _fs.File.ReadAllTextAsync(file); + return JsonConvert.DeserializeObject(json, converters); + } + catch (JsonException e) + { + HandleJsonException(e, file); + } + catch (AggregateException ae) when (ae.InnerException is JsonException e) + { + HandleJsonException(e, file); + } + + return null; + } + + private void HandleJsonException(JsonException exception, string file) + { + _log.Warning(exception, + "Failed to parse Sonarr JSON file (This likely indicates a bug that should be " + + "reported in the TRaSH repo): {File}", _fs.Path.GetFileName(file)); } public ReleaseProfileData? GetUnfilteredProfileById(string trashId)