diff --git a/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs b/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs new file mode 100644 index 0000000..7cba656 --- /dev/null +++ b/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs @@ -0,0 +1,79 @@ +using PanoramicData.IssueAssessment.Mapping; +using PanoramicData.IssueAssessment.Rubrics; + +namespace PanoramicData.IssueAssessment.Tests.Rubrics; + +/// +/// MS-23199 Section headings are credited whatever their level (h1 to h4), case or wording variant. +/// +public class HeadingKeywordTests : TestBase +{ + private static readonly IssueAssessor Assessor = new(BuiltInRubrics.PanoramicDataTicketQuality); + + [Theory] + [InlineData("Current Behavior", "Expected Behavior")] + [InlineData("Current Behaviour", "Expected Behaviour")] + [InlineData("Actual Behavior", "Expected Behavior")] + [InlineData("Actual Result", "Expected Result")] + [InlineData("Current Behavior", "Desired Behavior")] + [InlineData("current behavior", "EXPECTED BEHAVIOR")] + public async Task CurrentAndExpectedHeadings_ScoreFull_WhateverTheVariantOrCase(string current, string expected) + { + int score = await RawScore("CurrentVsExpected", $"h2. {current}\r\nRows with line breaks are missing.\r\n\r\nh2. {expected}\r\nEvery row is exported."); + + score.Should().Be(100); + } + + [Theory] + [InlineData("h1.")] + [InlineData("h2.")] + [InlineData("h3.")] + [InlineData("h4.")] + public async Task CurrentAndExpectedHeadings_ScoreFull_AtEveryJiraHeadingLevel(string marker) + { + int score = await RawScore("CurrentVsExpected", $"{marker} Current Behavior\r\nRows are missing.\r\n\r\n{marker} Expected Behavior\r\nEvery row is exported."); + + score.Should().Be(100); + } + + [Fact] + public async Task CurrentHeadingAlone_StillScoresHalf() + { + int score = await RawScore("CurrentVsExpected", "h2. Actual Result\r\nRows are missing."); + + score.Should().Be(50); + } + + [Theory] + [InlineData("Acceptance Criteria")] + [InlineData("Success Criteria")] + [InlineData("Definition of Done")] + [InlineData("ACCEPTANCE CRITERIA")] + public async Task CriteriaHeadings_RaiseTestabilityAboveADescriptionWithNone(string heading) + { + const string Body = "Rows with line breaks are missing from the export."; + + int without = await RawScore("Testability", Body); + int with = await RawScore("Testability", $"{Body}\r\n\r\nh2. {heading}\r\n# Every row is exported\r\n# Line breaks are preserved"); + + with.Should().BeGreaterThan(without); + } + + private static async Task RawScore(string name, string description) + { + NormalizedIssue issue = new() + { + Key = "OPS-1", + Project = "OPS", + Title = "Export to CSV drops rows that contain line breaks", + Description = description, + IssueType = "Bug", + Status = "In Progress", + Platform = "Jira", + Labels = [], + }; + using JsonDocument document = issue.ToJsonDocument(); + AssessmentReport report = AssessmentReport.From(await Assessor.AssessAsync(document, CancellationToken)); + return (int)report.Categories.SelectMany(category => category.Assessments).Single(item => item.Name == name).RawScore!.Value; + } +} diff --git a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json index caea93f..02ec636 100644 --- a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json +++ b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json @@ -165,7 +165,7 @@ "measures": { "scope": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "scope:", "h[1-6]\\.[^\\r\\n]*scope", "\\*\\*[^\\r\\n]*scope", "questions?\\s+to\\s+(answer|investigate)", "investigate\\s+(whether|if|how|why)", "determine\\s+(whether|if|how|why)", "find\\s+out", "understand\\s+(how|why|what)" ] }, "deliverables": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "deliverable", "findings", "recommendation", "report", "document", "conclusion", "outcome", "result\\s+of\\s+(investigation|research|analysis)" ] }, - "criteria": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "should\\s+(be|display|show|return|have|not)", "must\\s+(be|display|show|return|have|not)", "verify\\s+that", "acceptance\\s+criteria", "h[23]\\.\\s*acceptance\\s+criteria", "expected\\s+(result|behavior|behaviour|outcome)", "pass\\s+criteria", "success\\s+criteria" ] }, + "criteria": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "should\\s+(be|display|show|return|have|not)", "must\\s+(be|display|show|return|have|not)", "verify\\s+that", "acceptance\\s+criteria", "h[1-4]\\.\\s*acceptance\\s+criteria", "expected\\s+(result|behavior|behaviour|outcome)", "pass\\s+criteria", "success\\s+criteria", "definition\\s+of\\s+done" ] }, "criteriaHeadings": { "type": "criteriaHeadingCount", "source": { "paths": [ "$.description" ] } } }, "tiers": [ @@ -215,8 +215,8 @@ "type": "tiered", "measures": { "context": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "background:", "h[1-6]\\.[^\\r\\n]*background", "context:", "h[1-6]\\.[^\\r\\n]*context", "\\*\\*[^\\r\\n]*(background|context)", "motivation:", "why\\s+it\\s+matters", "why\\s+(we|this)", "reason\\s+for", "because" ] }, - "current": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "current(ly)?(\\s+behavior|\\s+behaviour)?:", "h[23]\\.\\s*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)", "\\*\\*current\\s*(behavior|behaviour)\\*\\*", "\\*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)?\\*", "\\*actual\\s*(behavior|behaviour)?\\*", "actual(\\s+behavior|\\s+behaviour)?:", "what\\s+happens:", "observed:", "bug:", "issue:", "problem:" ] }, - "expected": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "expected(\\s+behavior|\\s+behaviour)?:", "h[23]\\.\\s*expected\\s*(behavior|behaviour)", "\\*\\*expected\\s*(behavior|behaviour)\\*\\*", "\\*expected\\s*(behavior|behaviour)?\\*", "should\\s+be:", "desired:", "correct\\s+behavior:", "fix:", "solution:", "h2\\.\\s*acceptance\\s+criteria", "h3\\.\\s*acceptance\\s+criteria", "acceptance\\s+criteria:", "\\*\\*acceptance\\s+criteria\\*\\*" ] } + "current": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "current(ly)?(\\s+behavior|\\s+behaviour)?:", "h[1-4]\\.\\s*(current|actual)(\\s*/\\s*(actual|current))?\\s*(behavior|behaviour|result)", "\\*\\*current\\s*(behavior|behaviour)\\*\\*", "\\*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)?\\*", "\\*actual\\s*(behavior|behaviour)?\\*", "actual(\\s+behavior|\\s+behaviour)?:", "what\\s+happens:", "observed:", "bug:", "issue:", "problem:" ] }, + "expected": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "expected(\\s+behavior|\\s+behaviour)?:", "h[1-4]\\.\\s*(expected|desired)\\s*(behavior|behaviour|result|outcome)", "\\*\\*expected\\s*(behavior|behaviour)\\*\\*", "\\*expected\\s*(behavior|behaviour)?\\*", "should\\s+be:", "desired:", "correct\\s+behavior:", "fix:", "solution:", "h2\\.\\s*acceptance\\s+criteria", "h3\\.\\s*acceptance\\s+criteria", "acceptance\\s+criteria:", "\\*\\*acceptance\\s+criteria\\*\\*" ] } }, "tiers": [ { "when": { "type": "all", "conditions": [ { "type": "ref", "name": "isInvestigation" }, { "type": "measure", "measure": "context", "atLeast": 2 } ] }, "score": 100, "finding": "✓ Investigation Context: Well documented" },