diff --git a/doc/spec/design.md b/doc/spec/design.md index 3a3c819a..10c25835 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -1047,6 +1047,23 @@ Covers: Needs: impl, utest +#### Markdown "Covers" Single Line +`dsn~md.covers-single-line~1` + +The Markdown and reStructuredText Importers support defining covered specification items inline on the same line as the "Covers:" keyword. + + covers-single-line = "Covers:" *WSP reference *("," *WSP reference) + +Rationale: + +When a specification item covers only one requirement, writing an unordered bullet list requires multiple lines and markdownlint requires blank lines around lists. Allowing an inline notation is more compact and convenient. + +Covers: + +* `req~markdown-standard-syntax~1` + +Needs: impl, utest + #### Markdown "Depends" List `dsn~md.depends-list~1` diff --git a/doc/user_guide/use_cases/writing_a_specification.md b/doc/user_guide/use_cases/writing_a_specification.md index da5a3614..d390db4c 100644 --- a/doc/user_guide/use_cases/writing_a_specification.md +++ b/doc/user_guide/use_cases/writing_a_specification.md @@ -112,7 +112,23 @@ The `Status` keyword takes a single value from `draft`, `proposed`, `approved`, ##### `Covers` -The `Covers` keyword states which items are covered by the current specification item. It is followed by a list of items that are covered, each one written on a new line starting with a bullet character (`+`, `*`, or `-`) followed by the referenced specification item id. +The `Covers` keyword states which items are covered by the current specification item. It comes in two flavors: as one-liner or as list. + +**Variant a) one-line `covers`** + + Covers: feat~rubber-ducky~1 + +The referenced IDs can optionally be enclosed in backticks: + + Covers: `feat~rubber-ducky~1` + +Or with multiple comma-separated items (with or without backticks): + + Covers: `feat~rubber-ducky~1`, `feat~bathtub~1` + +**Variant b) as List** + +It is followed by a list of items that are covered, each one written on a new line starting with a bullet character (`+`, `*`, or `-`) followed by the referenced specification item id. Given the Feature `feat~rubber-ducky~1` exists and needs a `req`. A requirement that covers that feature could be written as diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java index f1825d91..06bd482b 100644 --- a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java +++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.lightweightmarkup; import java.util.logging.Logger; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.itsallcode.openfasttrace.api.core.*; @@ -274,10 +275,35 @@ protected void addCoverage() this.listener.addCoveredId(locatedId(idText, SpecificationItemId.parseId(idText))); } + /** + * Add covered specification item IDs found on an inline "Covers:" line. + */ + // [impl->dsn~md.covers-single-line~1] + protected void addInlineCoverage() + { + final String line = this.currentContext.currentLine(); + final Matcher matcher = SpecificationItemId.ID_PATTERN.matcher(line); + final int colonIndex = line.indexOf(':'); + if (colonIndex >= 0) + { + matcher.region(colonIndex + 1, line.length()); + } + while (matcher.find()) + { + final String idText = matcher.group(); + final SpecificationItemId id = SpecificationItemId.parseId(idText); + this.listener.addCoveredId(locatedId(idText, id, matcher.start())); + } + } + private LocatedSpecificationItemId locatedId(final String idText, final SpecificationItemId id) + { + return locatedId(idText, id, this.currentContext.currentLine().indexOf(idText)); + } + + private LocatedSpecificationItemId locatedId(final String idText, final SpecificationItemId id, final int column) { // [impl->dsn~located-specification-item-id-text-ranges~1] - final int column = this.currentContext.currentLine().indexOf(idText); final SourceRange range = new SourceRange(new SourcePosition(this.currentContext.lineNumber() - 1, column), new SourcePosition(this.currentContext.lineNumber() - 1, column + idText.length())); final int artifactTypeEnd = idText.indexOf(SpecificationItemId.ARTIFACT_TYPE_SEPARATOR); diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstants.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstants.java index d0720d6e..acf7efb9 100644 --- a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstants.java +++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstants.java @@ -27,6 +27,11 @@ public final class PatternConstants + "(" + SpecificationItemId.ID_PATTERN + ")" + "(?:\\W.*)?"; + // [impl->dsn~md.covers-single-line~1] + /** Matches an inline requirement reference. Capture group 1 holds the first ID. */ + public static final String REFERENCE_INLINE = "\\s*.*?(" + + SpecificationItemId.ID_PATTERN + ").*"; + private PatternConstants() { // not instantiable diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstantsTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstantsTest.java index 9816bb79..63df6067 100644 --- a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstantsTest.java +++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/PatternConstantsTest.java @@ -87,4 +87,23 @@ void testReferenceAfterBulletRejectsInvalid(final String input) { assertThat(PatternConstants.REFERENCE_AFTER_BULLET, not(PatternMatcher.patternMatches(input))); } + + // [utest->dsn~md.covers-single-line~1] + @ParameterizedTest + @ValueSource(strings = { " req~name~1", " `req~name~1`", "[link](#req~name~1)", + " req~name~1, req~name~2", " `req~name~1`, `req~name~2`" }) + void testReferenceInlineMatchesValid(final String input) + { + final Matcher matcher = Pattern.compile(PatternConstants.REFERENCE_INLINE).matcher(input); + assertAll(() -> assertThat(matcher.matches(), is(true)), + () -> assertThat(matcher.group(1), equalTo("req~name~1"))); + } + + // [utest->dsn~md.covers-single-line~1] + @ParameterizedTest + @ValueSource(strings = { "", " ", "invalid", "not~an~id" }) + void testReferenceInlineRejectsInvalid(final String input) + { + assertThat(PatternConstants.REFERENCE_INLINE, not(PatternMatcher.patternMatches(input))); + } } diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java index b1087b55..61a6d0e2 100644 --- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java +++ b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java @@ -71,6 +71,7 @@ protected Transition[] configureTransitions() transition(SPEC_ITEM , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), transition(SPEC_ITEM , RATIONALE , MdPattern.RATIONALE , this::beginRationale ), transition(SPEC_ITEM , COMMENT , MdPattern.COMMENT , this::beginComment ), + transition(SPEC_ITEM , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(SPEC_ITEM , COVERS , MdPattern.COVERS , () -> {} ), transition(SPEC_ITEM , DEPENDS , MdPattern.DEPENDS , () -> {} ), transition(SPEC_ITEM , SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), @@ -85,6 +86,7 @@ protected Transition[] configureTransitions() transition(DESCRIPTION, TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), transition(DESCRIPTION, RATIONALE , MdPattern.RATIONALE , this::beginRationale ), transition(DESCRIPTION, COMMENT , MdPattern.COMMENT , this::beginComment ), + transition(DESCRIPTION, COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(DESCRIPTION, COVERS , MdPattern.COVERS , () -> {} ), transition(DESCRIPTION, DEPENDS , MdPattern.DEPENDS , () -> {} ), transition(DESCRIPTION, SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), @@ -97,6 +99,7 @@ protected Transition[] configureTransitions() transition(RATIONALE , SPEC_ITEM , MdPattern.ID , this::beginItem ), transition(RATIONALE , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), transition(RATIONALE , COMMENT , MdPattern.COMMENT , this::beginComment ), + transition(RATIONALE , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(RATIONALE , COVERS , MdPattern.COVERS , () -> {} ), transition(RATIONALE , DEPENDS , MdPattern.DEPENDS , () -> {} ), transition(RATIONALE , SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), @@ -107,6 +110,7 @@ protected Transition[] configureTransitions() transition(COMMENT , SPEC_ITEM , MdPattern.ID , this::beginItem ), transition(COMMENT , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), + transition(COMMENT , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(COMMENT , COVERS , MdPattern.COVERS , () -> {} ), transition(COMMENT , DEPENDS , MdPattern.DEPENDS , () -> {} ), transition(COMMENT , SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), @@ -116,9 +120,11 @@ protected Transition[] configureTransitions() transition(COMMENT , TAGS , MdPattern.TAGS , () -> {} ), transition(COMMENT , COMMENT , MdPattern.EVERYTHING , this::appendComment ), + // [impl->dsn~md.covers-single-line~1] // [impl->dsn~md.covers-list~1] transition(COVERS , SPEC_ITEM , MdPattern.ID , this::beginItem ), transition(COVERS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), + transition(COVERS , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(COVERS , COVERS , MdPattern.COVERS_REF , this::addCoverage ), transition(COVERS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ), transition(COVERS , COMMENT , MdPattern.COMMENT , this::beginComment ), @@ -140,6 +146,7 @@ protected Transition[] configureTransitions() transition(DEPENDS , SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), transition(DEPENDS , NEEDS_LIST , MdPattern.NEEDS , () -> {} ), transition(DEPENDS , DEPENDS , MdPattern.EMPTY , () -> {} ), + transition(DEPENDS , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(DEPENDS , COVERS , MdPattern.COVERS , () -> {} ), transition(DEPENDS , TAGS , MdPattern.TAGS_INT , this::addTag ), transition(DEPENDS , TAGS , MdPattern.TAGS , () -> {} ), @@ -154,6 +161,7 @@ protected Transition[] configureTransitions() transition(NEEDS_LIST , DEPENDS , MdPattern.DEPENDS , () -> {} ), transition(NEEDS_LIST , NEEDS_LIST , MdPattern.NEEDS_REF , this::addNeeds ), transition(NEEDS_LIST , DESCRIPTION, MdPattern.EMPTY , () -> {} ), + transition(NEEDS_LIST , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(NEEDS_LIST , COVERS , MdPattern.COVERS , () -> {} ), transition(NEEDS_LIST , TAGS , MdPattern.TAGS_INT , this::addTag ), transition(NEEDS_LIST , TAGS , MdPattern.TAGS , () -> {} ), @@ -169,6 +177,7 @@ protected Transition[] configureTransitions() transition(TAGS , SPEC_ITEM , MdPattern.NEEDS_INT , this::addNeeds ), transition(TAGS , NEEDS_LIST , MdPattern.NEEDS , () -> {} ), transition(TAGS , SPEC_ITEM , MdPattern.EMPTY , () -> {} ), + transition(TAGS , COVERS , MdPattern.COVERS_INT , this::addInlineCoverage ), transition(TAGS , COVERS , MdPattern.COVERS , () -> {} ), transition(TAGS , TAGS , MdPattern.TAGS , () -> {} ), transition(TAGS , TAGS , MdPattern.TAGS_INT , this::addTag ), diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java index 391507d1..4986e29e 100644 --- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java +++ b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java @@ -15,11 +15,13 @@ enum MdPattern // [impl->dsn~md.specification-item-title~1] // [impl->dsn~md.artifact-forwarding-notation~1] // [impl->dsn~md.tags-format~1] + // [impl->dsn~md.covers-single-line~1] // @formatter:off CODE_BEGIN(" *[`~]{3,30}\\w*\\s*"), CODE_END(" *[`~]{3,30}\\s*"), COMMENT("Comment:\\s*"), + COVERS_INT("Covers:" + PatternConstants.REFERENCE_INLINE), COVERS("Covers:\\s*"), COVERS_REF(PatternConstants.REFERENCE_AFTER_BULLET), DEPENDS("Depends:\\s*"), diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java index 7793879e..7d19d30b 100644 --- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java +++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java @@ -95,6 +95,25 @@ void testIdentifyNonTags(final String text) MarkdownAsserts.assertMismatch(MdPattern.TAGS_INT, text); } + // [utest->dsn~md.covers-single-line~1] + @ParameterizedTest + @CsvSource( + { "Covers: req~foo~1", "Covers: `req~foo~1`", "Covers:req~foo~1", "'Covers: \treq~foo~1 '", + "Covers: req~foo~1, req~bar~2" }) + void testIdentifyCoversInline(final String text) + { + MarkdownAsserts.assertMatch(MdPattern.COVERS_INT, text); + } + + // [utest->dsn~md.covers-single-line~1] + @ParameterizedTest + @CsvSource( + { "Covers:", "#Covers: req~foo~1", "' Covers: req~foo~1'", "Covers: abc" }) + void testIdentifyNonCoversInline(final String text) + { + MarkdownAsserts.assertMismatch(MdPattern.COVERS_INT, text); + } + @Test void testRunImportHandlesIOException(@Mock final InputFile fileMock, @Mock final ImportEventListener listenerMock, @Mock final BufferedReader readerMock) throws IOException diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java index 5b3fcefa..43ed36a3 100644 --- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java +++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java @@ -89,6 +89,23 @@ void testImportsLocatedDeclarationCoverageAndDependencyIds() () -> assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18)))); } + // [utest->dsn~located-specification-item-id-text-ranges~1] + @Test + void testImportsLocatedDeclarationCoverageAndDependencyIdsInline() + { + final SpecificationItem item = importText(""" + req~subject~1 + Covers: req~covered~2 + Depends: + * req~dependency~3 + """).get(0); + + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(1, 8, 21))), + () -> assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(3, 2, 18)))); + } + private static List importText(final String source) { return ImportAssertions.runImporterOnText(Path.of("located.md"), source, importerFactory); diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java index c7c7416c..8b87a999 100644 --- a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java +++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java @@ -81,6 +81,7 @@ protected Transition[] configureTransitions() transition(SPEC_ITEM , RATIONALE , RstPattern.RATIONALE , this::beginRationale ), transition(SPEC_ITEM , COMMENT , RstPattern.COMMENT , this::beginComment ), transition(SPEC_ITEM , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), + transition(SPEC_ITEM , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(SPEC_ITEM , COVERS , RstPattern.COVERS , () -> {} ), transition(SPEC_ITEM , DEPENDS , RstPattern.DEPENDS , () -> {} ), transition(SPEC_ITEM , SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), @@ -95,6 +96,7 @@ protected Transition[] configureTransitions() transition(DESCRIPTION, TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), transition(DESCRIPTION, RATIONALE , RstPattern.RATIONALE , this::beginRationale ), transition(DESCRIPTION, COMMENT , RstPattern.COMMENT , this::beginComment ), + transition(DESCRIPTION, COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(DESCRIPTION, COVERS , RstPattern.COVERS , () -> {} ), transition(DESCRIPTION, DEPENDS , RstPattern.DEPENDS , () -> {} ), transition(DESCRIPTION, SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), @@ -108,6 +110,7 @@ protected Transition[] configureTransitions() transition(RATIONALE , SPEC_ITEM , RstPattern.ID , this::beginItem ), transition(RATIONALE , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), transition(RATIONALE , COMMENT , RstPattern.COMMENT , this::beginComment ), + transition(RATIONALE , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(RATIONALE , COVERS , RstPattern.COVERS , () -> {} ), transition(RATIONALE , DEPENDS , RstPattern.DEPENDS , () -> {} ), transition(RATIONALE , SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), @@ -118,6 +121,7 @@ protected Transition[] configureTransitions() transition(COMMENT , SPEC_ITEM , RstPattern.ID , this::beginItem ), transition(COMMENT , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), + transition(COMMENT , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(COMMENT , COVERS , RstPattern.COVERS , () -> {} ), transition(COMMENT , DEPENDS , RstPattern.DEPENDS , () -> {} ), transition(COMMENT , SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), @@ -127,9 +131,11 @@ protected Transition[] configureTransitions() transition(COMMENT , TAGS , RstPattern.TAGS , () -> {} ), transition(COMMENT , COMMENT , RstPattern.EVERYTHING , this::appendComment ), + // [impl->dsn~md.covers-single-line~1] // [impl->dsn~md.covers-list~1] transition(COVERS , SPEC_ITEM , RstPattern.ID , this::beginItem ), transition(COVERS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}), + transition(COVERS , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(COVERS , COVERS , RstPattern.COVERS_REF , this::addCoverage ), transition(COVERS , RATIONALE , RstPattern.RATIONALE , this::beginRationale ), transition(COVERS , COMMENT , RstPattern.COMMENT , this::beginComment ), @@ -151,6 +157,7 @@ protected Transition[] configureTransitions() transition(DEPENDS , SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), transition(DEPENDS , NEEDS_LINE , RstPattern.NEEDS , () -> {} ), transition(DEPENDS , DEPENDS , RstPattern.EMPTY , () -> {} ), + transition(DEPENDS , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(DEPENDS , COVERS , RstPattern.COVERS , () -> {} ), transition(DEPENDS , TAGS , RstPattern.TAGS_INT , this::addTag ), transition(DEPENDS , TAGS , RstPattern.TAGS , () -> {} ), @@ -165,6 +172,7 @@ protected Transition[] configureTransitions() transition(NEEDS_LINE , DEPENDS , RstPattern.DEPENDS , () -> {} ), transition(NEEDS_LINE , NEEDS_LINE , RstPattern.NEEDS_REF , this::addNeeds ), transition(NEEDS_LINE , NEEDS_LINE , RstPattern.EMPTY , () -> {} ), + transition(NEEDS_LINE , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(NEEDS_LINE , COVERS , RstPattern.COVERS , () -> {} ), transition(NEEDS_LINE , TAGS , RstPattern.TAGS_INT , this::addTag ), transition(NEEDS_LINE , TAGS , RstPattern.TAGS , () -> {} ), @@ -180,6 +188,7 @@ protected Transition[] configureTransitions() transition(TAGS , SPEC_ITEM , RstPattern.NEEDS_INT , this::addNeeds ), transition(TAGS , NEEDS_LINE , RstPattern.NEEDS , () -> {} ), transition(TAGS , SPEC_ITEM , RstPattern.EMPTY , () -> {} ), + transition(TAGS , COVERS , RstPattern.COVERS_INT , this::addInlineCoverage ), transition(TAGS , COVERS , RstPattern.COVERS , () -> {} ), transition(TAGS , TAGS , RstPattern.TAGS , () -> {} ), transition(TAGS , TAGS , RstPattern.TAGS_INT , this::addTag ), diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java index cbc84e34..cb31ba5d 100644 --- a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java +++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java @@ -15,9 +15,11 @@ enum RstPattern // [impl->dsn~md.specification-item-title~1] // [impl->dsn~md.artifact-forwarding-notation~1] // [impl->dsn~md.tags-format~1] + // [impl->dsn~md.covers-single-line~1] // @formatter:off COMMENT("Comment:\\s*"), + COVERS_INT("Covers:" + PatternConstants.REFERENCE_INLINE), COVERS("Covers:\\s*"), COVERS_REF(PatternConstants.REFERENCE_AFTER_BULLET), DEPENDS("Depends:\\s*"), diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java index 2aa6da41..5118f8f1 100644 --- a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java +++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java @@ -170,6 +170,55 @@ void testSpecificationItemCoversList() .build())); } + // [utest -> dsn~md.covers-single-line~1] + @Test + void testSpecificationItemCoversInline() + { + assertImport(PATH, """ + `req~item-a~2` + + Covers: feat~item-b~1 + """, + contains(item() + .id("req", "item-a", 2) + .addCoveredId("feat", "item-b", 1) + .location(PATH.toString(), 1) + .build())); + } + + // [utest -> dsn~md.covers-single-line~1] + @Test + void testSpecificationItemCoversInlineWithBackticks() + { + assertImport(PATH, """ + `req~item-a~2` + + Covers: `feat~item-b~1` + """, + contains(item() + .id("req", "item-a", 2) + .addCoveredId("feat", "item-b", 1) + .location(PATH.toString(), 1) + .build())); + } + + // [utest -> dsn~md.covers-single-line~1] + @Test + void testSpecificationItemCoversInlineMultipleOnSingleLine() + { + assertImport(PATH, """ + `req~item-a~2` + + Covers: `feat~item-b~1`, `feat~item-c~2` + """, + contains(item() + .id("req", "item-a", 2) + .addCoveredId("feat", "item-b", 1) + .addCoveredId("feat", "item-c", 2) + .location(PATH.toString(), 1) + .build())); + } + // [utest -> dsn~md.depends-list~1] @Test void testSpecificationItemDependsList()