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.
pull/5/head
Robert Dailey 3 years ago
parent 9065932eaa
commit b9db8b7ef3

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [1.0.0] - 2021-04-14
See the [Python Migration Guide][py-mig] for details on how to update your YAML configuration. See the [Python Migration Guide][py-mig] for details on how to update your YAML configuration.

@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="AutofacContrib.NSubstitute" /> <PackageReference Include="AutofacContrib.NSubstitute" />
<PackageReference Include="GitHubActionsTestLogger" /> <PackageReference Include="GitHubActionsTestLogger" />
<PackageReference Include="Serilog.Sinks.TestCorrelator" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))"> <ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))">
<EmbeddedResource Include="**\Data\*" /> <EmbeddedResource Include="**\Data\*" />

@ -10,6 +10,7 @@
<PackageReference Update="NUnit.Analyzers" Version="3.*" /> <PackageReference Update="NUnit.Analyzers" Version="3.*" />
<PackageReference Update="NUnit" Version="3.*" /> <PackageReference Update="NUnit" Version="3.*" />
<PackageReference Update="NUnit3TestAdapter" Version="3.*" /> <PackageReference Update="NUnit3TestAdapter" Version="3.*" />
<PackageReference Update="Serilog.Sinks.TestCorrelator" Version="3.*" />
<!-- Non-Test Packages --> <!-- Non-Test Packages -->
<PackageReference Update="Autofac.Extensions.DependencyInjection" Version="7.*" /> <PackageReference Update="Autofac.Extensions.DependencyInjection" Version="7.*" />

@ -0,0 +1,7 @@
namespace TestLibrary
{
public static class StringUtils
{
public static string TrimmedString(string value) => value.Trim('\r', '\n');
}
}

@ -4,11 +4,12 @@ using FluentAssertions;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Serilog; using Serilog;
using Serilog.Sinks.TestCorrelator;
using TestLibrary; using TestLibrary;
using Trash.Sonarr; using Trash.Sonarr;
using Trash.Sonarr.ReleaseProfile; using Trash.Sonarr.ReleaseProfile;
namespace Trash.Tests.Sonarr.Guide namespace Trash.Tests.Sonarr.ReleaseProfile
{ {
[TestFixture] [TestFixture]
public class ReleaseProfileParserTest public class ReleaseProfileParserTest
@ -17,8 +18,15 @@ namespace Trash.Tests.Sonarr.Guide
{ {
public Context() public Context()
{ {
var logger = new LoggerConfiguration()
.WriteTo.TestCorrelator()
.MinimumLevel.Debug()
.CreateLogger();
Config = Substitute.For<SonarrConfiguration>(); Config = Substitute.For<SonarrConfiguration>();
GuideParser = new ReleaseProfileGuideParser(Substitute.For<ILogger>()); Config.ReleaseProfiles.Add(new ReleaseProfileConfig());
GuideParser = new ReleaseProfileGuideParser(logger);
} }
public SonarrConfiguration Config { get; } public SonarrConfiguration Config { get; }
@ -30,8 +38,6 @@ namespace Trash.Tests.Sonarr.Guide
public void Parse_IgnoredRequiredPreferredScores() public void Parse_IgnoredRequiredPreferredScores()
{ {
var context = new Context(); var context = new Context();
context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig());
var markdown = context.TestData.GetResourceData("test_parse_markdown_complete_doc.md"); var markdown = context.TestData.GetResourceData("test_parse_markdown_complete_doc.md");
var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown);
@ -48,30 +54,53 @@ namespace Trash.Tests.Sonarr.Guide
public void Parse_IncludePreferredWhenRenaming() public void Parse_IncludePreferredWhenRenaming()
{ {
var context = new Context(); var context = new Context();
context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig());
var markdown = context.TestData.GetResourceData("include_preferred_when_renaming.md"); var markdown = context.TestData.GetResourceData("include_preferred_when_renaming.md");
var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown);
results.Should() results.Should()
.ContainKey("First Release Profile") .ContainKey("First Release Profile")
.WhichValue.IncludePreferredWhenRenaming.Should() .WhichValue.IncludePreferredWhenRenaming.Should().Be(true);
.Be(true);
results.Should() results.Should()
.ContainKey("Second Release Profile") .ContainKey("Second Release Profile")
.WhichValue.IncludePreferredWhenRenaming.Should() .WhichValue.IncludePreferredWhenRenaming.Should().Be(false);
.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] [Test]
public void Parse_StrictNegativeScores() public void Parse_StrictNegativeScores()
{ {
var context = new Context(); var context = new Context();
context.Config.ReleaseProfiles.Add(new ReleaseProfileConfig context.Config.ReleaseProfiles = new List<ReleaseProfileConfig>
{ {
// Pretend the user specified this option for testing purposes new() {StrictNegativeScores = true}
StrictNegativeScores = true };
});
var markdown = context.TestData.GetResourceData("strict_negative_scores.md"); var markdown = context.TestData.GetResourceData("strict_negative_scores.md");
var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown);

@ -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;
}
}
}

@ -17,16 +17,6 @@ namespace Trash.Sonarr.Api
_config = config; _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<Version> GetVersion() public async Task<Version> GetVersion()
{ {
dynamic data = await BaseUrl() dynamic data = await BaseUrl()
@ -87,5 +77,15 @@ namespace Trash.Sonarr.Api
.PutJsonAsync(newQuality) .PutJsonAsync(newQuality)
.ReceiveJson<List<SonarrQualityDefinitionItem>>(); .ReceiveJson<List<SonarrQualityDefinitionItem>>();
} }
private string BaseUrl()
{
if (_config.ActiveConfiguration == null)
{
throw new InvalidOperationException("No active configuration available for API method");
}
return _config.ActiveConfiguration.BuildUrl();
}
} }
} }

@ -27,6 +27,7 @@ namespace Trash.Sonarr.ReleaseProfile
private readonly Regex _regexHeader = new(@"^(#+)\s([\w\s\d]+)\s*$", RegexOptions.Compiled); private readonly Regex _regexHeader = new(@"^(#+)\s([\w\s\d]+)\s*$", RegexOptions.Compiled);
private readonly Regex _regexHeaderReleaseProfile = BuildRegex(@"release profile"); private readonly Regex _regexHeaderReleaseProfile = BuildRegex(@"release profile");
private readonly Regex _regexPotentialScore = BuildRegex(@"\[(-?[\d]+)\]");
private readonly Regex _regexScore = BuildRegex(@"score.*?\[(-?[\d]+)\]"); private readonly Regex _regexScore = BuildRegex(@"score.*?\[(-?[\d]+)\]");
public ReleaseProfileGuideParser(ILogger logger) public ReleaseProfileGuideParser(ILogger logger)
@ -49,6 +50,7 @@ namespace Trash.Sonarr.ReleaseProfile
var reader = new StringReader(markdown); var reader = new StringReader(markdown);
for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) for (var line = reader.ReadLine(); line != null; line = reader.ReadLine())
{ {
state.LineNumber++;
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
{ {
continue; continue;
@ -158,9 +160,11 @@ namespace Trash.Sonarr.ReleaseProfile
private void ParseMarkdownOutsideFence(string line, ParserState state, IDictionary<string, ProfileData> results) private void ParseMarkdownOutsideFence(string line, ParserState state, IDictionary<string, ProfileData> results)
{ {
// ReSharper disable once InlineOutVariableDeclaration
Match match;
// Header Processing // Header Processing
var match = _regexHeader.Match(line); if (_regexHeader.Match(line, out match))
if (match.Success)
{ {
var headerDepth = match.Groups[1].Length; var headerDepth = match.Groups[1].Length;
var headerText = match.Groups[2].Value; var headerText = match.Groups[2].Value;
@ -211,20 +215,24 @@ namespace Trash.Sonarr.ReleaseProfile
// return; // return;
} }
match = _regexScore.Match(line); if (_regexScore.Match(line, out match))
if (match.Success)
{ {
state.Score = int.Parse(match.Groups[1].Value); state.Score = int.Parse(match.Groups[1].Value);
Log.Debug(" - Score [Value: {Score}]", state.Score); 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) private TermCategory? ParseCategory(string line)
{ {
foreach (var (category, regex) in _regexCategories) foreach (var (category, regex) in _regexCategories)
{ {
var match = regex.Match(line); if (regex.Match(line).Success)
if (match.Success)
{ {
return category; return category;
} }
@ -252,6 +260,7 @@ namespace Trash.Sonarr.ReleaseProfile
public TermCategory CurrentCategory { get; set; } public TermCategory CurrentCategory { get; set; }
public int BracketDepth { get; set; } public int BracketDepth { get; set; }
public int CurrentHeaderDepth { get; set; } public int CurrentHeaderDepth { get; set; }
public int LineNumber { get; set; }
public bool IsValid => ProfileName != null && (CurrentCategory != TermCategory.Preferred || Score != null); public bool IsValid => ProfileName != null && (CurrentCategory != TermCategory.Preferred || Score != null);

@ -10,7 +10,7 @@ namespace Trash.Sonarr
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class SonarrConfiguration : BaseConfiguration public class SonarrConfiguration : BaseConfiguration
{ {
public List<ReleaseProfileConfig> ReleaseProfiles { get; init; } = new(); public List<ReleaseProfileConfig> ReleaseProfiles { get; set; } = new();
public SonarrQualityDefinitionType? QualityDefinition { get; init; } public SonarrQualityDefinitionType? QualityDefinition { get; init; }
public override string BuildUrl() public override string BuildUrl()

Loading…
Cancel
Save