From b9db8b7ef376d96df9cc257eb5239ab56a3f8203 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sat, 17 Apr 2021 11:36:02 -0500 Subject: [PATCH] fix: Log warning if a "potential" score is found A potential score is one where a number between brackets is found (e.g. [100]) but the word "score" is not before it. A warning is logged to notify the user of a potential issue with the guide itself. Other changes: - `Serilog.Sinks.TestCorrelator` package added to test log output messages. - New StringUtils class in TestLibrary - Refactored `Regex.Match()` calls to use new extension method that allows the match to be directly used as an expression in an `if` condition while also providing the match object as output. --- CHANGELOG.md | 5 ++ src/Directory.Build.props | 1 + src/Directory.Build.targets | 1 + src/TestLibrary/StringUtils.cs | 7 +++ .../Data/include_preferred_when_renaming.md | 0 .../Data/strict_negative_scores.md | 0 .../Data/test_parse_markdown_complete_doc.md | 0 .../ReleaseProfileParserTest.cs | 57 ++++++++++++++----- src/Trash/Extensions/RegexExtensions.cs | 13 +++++ src/Trash/Sonarr/Api/SonarrApi.cs | 20 +++---- .../ReleaseProfileGuideParser.cs | 21 +++++-- src/Trash/Sonarr/SonarrConfiguration.cs | 2 +- 12 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 src/TestLibrary/StringUtils.cs rename src/Trash.Tests/Sonarr/{Guide => ReleaseProfile}/Data/include_preferred_when_renaming.md (100%) rename src/Trash.Tests/Sonarr/{Guide => ReleaseProfile}/Data/strict_negative_scores.md (100%) rename src/Trash.Tests/Sonarr/{Guide => ReleaseProfile}/Data/test_parse_markdown_complete_doc.md (100%) rename src/Trash.Tests/Sonarr/{Guide => ReleaseProfile}/ReleaseProfileParserTest.cs (63%) create mode 100644 src/Trash/Extensions/RegexExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index be0cafdd..53c99e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- A warning is now logged when we find a number in brackets (such as `[100]`) without the word + `score` before it. This represents a potential score and bug in the guide itself. + ## [1.0.0] - 2021-04-14 See the [Python Migration Guide][py-mig] for details on how to update your YAML configuration. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c7dd259f..916ed5ec 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -24,6 +24,7 @@ + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index ff50ca22..b77e6e85 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -10,6 +10,7 @@ + diff --git a/src/TestLibrary/StringUtils.cs b/src/TestLibrary/StringUtils.cs new file mode 100644 index 00000000..881723f5 --- /dev/null +++ b/src/TestLibrary/StringUtils.cs @@ -0,0 +1,7 @@ +namespace TestLibrary +{ + public static class StringUtils + { + public static string TrimmedString(string value) => value.Trim('\r', '\n'); + } +} diff --git a/src/Trash.Tests/Sonarr/Guide/Data/include_preferred_when_renaming.md b/src/Trash.Tests/Sonarr/ReleaseProfile/Data/include_preferred_when_renaming.md similarity index 100% rename from src/Trash.Tests/Sonarr/Guide/Data/include_preferred_when_renaming.md rename to src/Trash.Tests/Sonarr/ReleaseProfile/Data/include_preferred_when_renaming.md diff --git a/src/Trash.Tests/Sonarr/Guide/Data/strict_negative_scores.md b/src/Trash.Tests/Sonarr/ReleaseProfile/Data/strict_negative_scores.md similarity index 100% rename from src/Trash.Tests/Sonarr/Guide/Data/strict_negative_scores.md rename to src/Trash.Tests/Sonarr/ReleaseProfile/Data/strict_negative_scores.md diff --git a/src/Trash.Tests/Sonarr/Guide/Data/test_parse_markdown_complete_doc.md b/src/Trash.Tests/Sonarr/ReleaseProfile/Data/test_parse_markdown_complete_doc.md similarity index 100% rename from src/Trash.Tests/Sonarr/Guide/Data/test_parse_markdown_complete_doc.md rename to src/Trash.Tests/Sonarr/ReleaseProfile/Data/test_parse_markdown_complete_doc.md diff --git a/src/Trash.Tests/Sonarr/Guide/ReleaseProfileParserTest.cs b/src/Trash.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs similarity index 63% rename from src/Trash.Tests/Sonarr/Guide/ReleaseProfileParserTest.cs rename to src/Trash.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs index dcffa7a1..1462e767 100644 --- a/src/Trash.Tests/Sonarr/Guide/ReleaseProfileParserTest.cs +++ b/src/Trash.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs @@ -4,11 +4,12 @@ using FluentAssertions; using NSubstitute; using NUnit.Framework; using Serilog; +using Serilog.Sinks.TestCorrelator; using TestLibrary; using Trash.Sonarr; using Trash.Sonarr.ReleaseProfile; -namespace Trash.Tests.Sonarr.Guide +namespace Trash.Tests.Sonarr.ReleaseProfile { [TestFixture] public class ReleaseProfileParserTest @@ -17,8 +18,15 @@ namespace Trash.Tests.Sonarr.Guide { public Context() { + var logger = new LoggerConfiguration() + .WriteTo.TestCorrelator() + .MinimumLevel.Debug() + .CreateLogger(); + Config = Substitute.For(); - GuideParser = new ReleaseProfileGuideParser(Substitute.For()); + Config.ReleaseProfiles.Add(new ReleaseProfileConfig()); + + GuideParser = new ReleaseProfileGuideParser(logger); } public SonarrConfiguration Config { get; } @@ -30,8 +38,6 @@ namespace Trash.Tests.Sonarr.Guide public void Parse_IgnoredRequiredPreferredScores() { var context = new Context(); - context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig()); - var markdown = context.TestData.GetResourceData("test_parse_markdown_complete_doc.md"); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); @@ -48,30 +54,53 @@ namespace Trash.Tests.Sonarr.Guide public void Parse_IncludePreferredWhenRenaming() { var context = new Context(); - context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig()); - var markdown = context.TestData.GetResourceData("include_preferred_when_renaming.md"); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); results.Should() .ContainKey("First Release Profile") - .WhichValue.IncludePreferredWhenRenaming.Should() - .Be(true); + .WhichValue.IncludePreferredWhenRenaming.Should().Be(true); results.Should() .ContainKey("Second Release Profile") - .WhichValue.IncludePreferredWhenRenaming.Should() - .Be(false); + .WhichValue.IncludePreferredWhenRenaming.Should().Be(false); + } + + [Test] + public void Parse_PotentialScore_WarningLogged() + { + string markdown = StringUtils.TrimmedString(@" +# First Release Profile + +The below line should be a score but isn't because it's missing the word 'score'. + +Use this number [100] + +``` +abc +``` +"); + var context = new Context(); + var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); + + results.Should().ContainKey("First Release Profile") + .WhichValue.Should().BeEquivalentTo(new ProfileData()); + + const string expectedLog = + "Found a potential score on line #5 that will be ignored because the " + + "word 'score' is missing (This is probably a bug in the guide itself): \"[100]\""; + + TestCorrelator.GetLogEventsFromCurrentContext() + .Should().ContainSingle(evt => evt.RenderMessage(default) == expectedLog); } [Test] public void Parse_StrictNegativeScores() { var context = new Context(); - context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig + context.Config.ReleaseProfiles = new List { - // Pretend the user specified this option for testing purposes - StrictNegativeScores = true - }); + new() {StrictNegativeScores = true} + }; var markdown = context.TestData.GetResourceData("strict_negative_scores.md"); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); diff --git a/src/Trash/Extensions/RegexExtensions.cs b/src/Trash/Extensions/RegexExtensions.cs new file mode 100644 index 00000000..82d8f79d --- /dev/null +++ b/src/Trash/Extensions/RegexExtensions.cs @@ -0,0 +1,13 @@ +using System.Text.RegularExpressions; + +namespace Trash.Extensions +{ + public static class RegexExtensions + { + public static bool Match(this Regex re, string strToCheck, out Match match) + { + match = re.Match(strToCheck); + return match.Success; + } + } +} diff --git a/src/Trash/Sonarr/Api/SonarrApi.cs b/src/Trash/Sonarr/Api/SonarrApi.cs index 95c9e48b..84d4401d 100644 --- a/src/Trash/Sonarr/Api/SonarrApi.cs +++ b/src/Trash/Sonarr/Api/SonarrApi.cs @@ -17,16 +17,6 @@ namespace Trash.Sonarr.Api _config = config; } - private string BaseUrl() - { - if (_config.ActiveConfiguration == null) - { - throw new InvalidOperationException("No active configuration available for API method"); - } - - return _config.ActiveConfiguration.BuildUrl(); - } - public async Task GetVersion() { dynamic data = await BaseUrl() @@ -87,5 +77,15 @@ namespace Trash.Sonarr.Api .PutJsonAsync(newQuality) .ReceiveJson>(); } + + private string BaseUrl() + { + if (_config.ActiveConfiguration == null) + { + throw new InvalidOperationException("No active configuration available for API method"); + } + + return _config.ActiveConfiguration.BuildUrl(); + } } } diff --git a/src/Trash/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs b/src/Trash/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs index 9541e0ec..970c3e00 100644 --- a/src/Trash/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs +++ b/src/Trash/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs @@ -27,6 +27,7 @@ namespace Trash.Sonarr.ReleaseProfile private readonly Regex _regexHeader = new(@"^(#+)\s([\w\s\d]+)\s*$", RegexOptions.Compiled); private readonly Regex _regexHeaderReleaseProfile = BuildRegex(@"release profile"); + private readonly Regex _regexPotentialScore = BuildRegex(@"\[(-?[\d]+)\]"); private readonly Regex _regexScore = BuildRegex(@"score.*?\[(-?[\d]+)\]"); public ReleaseProfileGuideParser(ILogger logger) @@ -49,6 +50,7 @@ namespace Trash.Sonarr.ReleaseProfile var reader = new StringReader(markdown); for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) { + state.LineNumber++; if (string.IsNullOrEmpty(line)) { continue; @@ -158,9 +160,11 @@ namespace Trash.Sonarr.ReleaseProfile private void ParseMarkdownOutsideFence(string line, ParserState state, IDictionary results) { + // ReSharper disable once InlineOutVariableDeclaration + Match match; + // Header Processing - var match = _regexHeader.Match(line); - if (match.Success) + if (_regexHeader.Match(line, out match)) { var headerDepth = match.Groups[1].Length; var headerText = match.Groups[2].Value; @@ -211,20 +215,24 @@ namespace Trash.Sonarr.ReleaseProfile // return; } - match = _regexScore.Match(line); - if (match.Success) + if (_regexScore.Match(line, out match)) { state.Score = int.Parse(match.Groups[1].Value); Log.Debug(" - Score [Value: {Score}]", state.Score); } + else if (_regexPotentialScore.Match(line, out match)) + { + Log.Warning("Found a potential score on line #{Line} that will be ignored because the " + + "word 'score' is missing (This is probably a bug in the guide itself): {ScoreMatch}", + state.LineNumber, match.Groups[0].Value); + } } private TermCategory? ParseCategory(string line) { foreach (var (category, regex) in _regexCategories) { - var match = regex.Match(line); - if (match.Success) + if (regex.Match(line).Success) { return category; } @@ -252,6 +260,7 @@ namespace Trash.Sonarr.ReleaseProfile public TermCategory CurrentCategory { get; set; } public int BracketDepth { get; set; } public int CurrentHeaderDepth { get; set; } + public int LineNumber { get; set; } public bool IsValid => ProfileName != null && (CurrentCategory != TermCategory.Preferred || Score != null); diff --git a/src/Trash/Sonarr/SonarrConfiguration.cs b/src/Trash/Sonarr/SonarrConfiguration.cs index e7461233..75e9bff9 100644 --- a/src/Trash/Sonarr/SonarrConfiguration.cs +++ b/src/Trash/Sonarr/SonarrConfiguration.cs @@ -10,7 +10,7 @@ namespace Trash.Sonarr [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class SonarrConfiguration : BaseConfiguration { - public List ReleaseProfiles { get; init; } = new(); + public List ReleaseProfiles { get; set; } = new(); public SonarrQualityDefinitionType? QualityDefinition { get; init; } public override string BuildUrl()