diff --git a/doc/changes/changes_4.7.0.md b/doc/changes/changes_4.7.0.md index 22c995bc..8e39b9c4 100644 --- a/doc/changes/changes_4.7.0.md +++ b/doc/changes/changes_4.7.0.md @@ -1,4 +1,4 @@ -# OpenFastTrace 4.7.0, released 2026-07-?? +# OpenFastTrace 4.7.0, released 2026-07-28 Code name: Gherkin Importer @@ -9,6 +9,9 @@ OpenFastTrace can now import traced specifications directly from Gherkin optional `Covers` and `Needs` metadata; existing coverage tags remain supported when written in Gherkin comments. +Markdown and RST documentation can now also cover specification items with coverage tags in their native comments. + ## New Features * #563: Added Gherkin `.feature` specification import for annotated scenarios and scenario outlines. +* #562: Added coverage tag import from Markdown and RST comments. diff --git a/doc/spec/design.md b/doc/spec/design.md index 58bcafd1..7f9df698 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -100,7 +100,7 @@ For each specification artifact type OFT uses an importer. The importer uses the ### Shared Coverage Tag Parser -The `importer/tag-importer-common` module provides the reusable line scanning and coverage-tag parsing used by importers. Its public API consists of `LineReader`, which forwards input lines to a consumer, and `CoverageTagParser`, which recognizes full coverage tags and optionally configured short coverage tags. +The `importer/tag-importer-common` module provides the reusable line scanning and coverage-tag parsing used by importers. Its public API consists of `LineReader`, which forwards input lines to a consumer, and `CoverageTagParser`, which recognizes coverage tags. The tag importer remains responsible for selecting its input files and creating the shared parser. Parsing implementation classes remain encapsulated in the shared module so that future importers can reuse the same coverage-tag semantics without depending on tag-importer internals. @@ -382,6 +382,28 @@ Needs: impl, utest, itest RST and Markdown share a common underlying parser that operates on a line-by-line basis. +##### Markdown Comment Coverage Tags +`dsn~markdown.comment-coverage-tags~1` + +For a Markdown input file, the lightweight-markup importer delegates a line to the shared coverage-tag parser only when the complete line is a single-line HTML comment, apart from optional surrounding whitespace. The dedicated Markdown importer keeps its priority ahead of the Tag Importer, and the existing markup state machine continues to process every input line. + +Covers: + +* `scn~markdown.comment-coverage-tags~1` + +Needs: impl, utest, itest + +##### RST Comment Coverage Tags +`dsn~rst.comment-coverage-tags~1` + +For an RST input file, the lightweight-markup importer delegates a line to the shared coverage-tag parser only when it begins with optional whitespace, `..`, and whitespace, and is not an RST directive. The dedicated RST importer keeps its priority ahead of the Tag Importer, and the existing markup state machine continues to process every input line. + +Covers: + +* `scn~rst.comment-coverage-tags~1` + +Needs: impl, utest, itest + ##### Disabling OFT Parsing for Parts of a Markup File `dsn~disabling-oft-parsing-for-parts-of-a-markup-file~1` @@ -953,7 +975,7 @@ OFT imports coverage tags in the full tag format: Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -973,7 +995,7 @@ Especially when used for design document files like UML models, requiring covera Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -991,7 +1013,7 @@ An item can cover multiple IDs. This avoids creating multiple IDs for the same i Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -1011,7 +1033,7 @@ Specifying an explicit revision in coverage tags allows incrementing the revisio Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -1035,7 +1057,7 @@ Specifying an explicit name in coverage tags allows overriding the auto-generate Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -1050,7 +1072,7 @@ When you need to cover these items it's important that the name is predictable a Covers: -* `req~import.full-coverage-tag-format~1` +* `req~import.full-coverage-tag-format~2` Needs: impl, utest @@ -1070,7 +1092,7 @@ During import of short tags OFT requires the following configuration: Covers: -* `req~import.short-coverage-tag-format~1` +* `req~import.short-coverage-tag-format~2` Needs: impl, utest diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index 0ae66c8f..95b5428b 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -115,13 +115,13 @@ One of the OpenFastTrace design goals is to provide a Java-based drop-in replace Needs: req ### Coverage Tag Import -`feat~coverage-tag-import~1` +`feat~coverage-tag-import~2` -OFT imports coverage tags from source code files. +OFT imports coverage tags from source code files and supported documentation files. Rationale: -Coverage tags indicate parts of the source code that implements a certain requirement. +Coverage tags indicate parts of source code or documentation that cover a certain requirement. Needs: req @@ -395,27 +395,61 @@ Needs: dsn #### Coverage Tags -Developers add coverage tags as comments to the source code to indicate where certain specification items are covered. +Developers add coverage tags as comments to source code or supported documentation to indicate where certain specification items are covered. ##### Import Full Coverage Tag Format -`req~import.full-coverage-tag-format~1` +`req~import.full-coverage-tag-format~2` -OFT imports coverage tags from source files in a full format that contains all necessary information for tracing. +OFT imports coverage tags from source files and supported documentation files in a full format that contains all necessary information for tracing. Covers: -* [feat~coverage-tag-import~1](#coverage-tag-import) +* [feat~coverage-tag-import~2](#coverage-tag-import) -Needs: dsn +Needs: scn, dsn ##### Import Short Coverage Tag Format -`req~import.short-coverage-tag-format~1` +`req~import.short-coverage-tag-format~2` OFT imports coverage tags from source files in a short format that requires additional configuration during import. Covers: -* [feat~coverage-tag-import~1](#coverage-tag-import) +* [feat~coverage-tag-import~2](#coverage-tag-import) + +Needs: scn, dsn + +##### Import Coverage Tags from Markdown Comments +`scn~markdown.comment-coverage-tags~1` + +**Given** a Markdown documentation artifact with coverage tags in standalone, +single-line HTML comments, +**when** OFT imports the artifact, +**then** it creates coverage items at the tag-line location while continuing to +import ordinary Markdown specification items unchanged. Text that resembles a +coverage tag outside such a comment does not create a coverage item. + +Covers: + +* `req~import.full-coverage-tag-format~2` +* `req~import.short-coverage-tag-format~2` + +Needs: dsn + +##### Import Coverage Tags from RST Comments +`scn~rst.comment-coverage-tags~1` + +**Given** an RST documentation artifact with coverage tags in standalone, +single-line RST comments, +**when** OFT imports the artifact, +**then** it creates coverage items at the tag-line location while continuing to +import ordinary RST specification items unchanged. Text that resembles a +coverage tag outside such a comment does not create a coverage item. + +Covers: + +* `req~import.full-coverage-tag-format~2` +* `req~import.short-coverage-tag-format~2` Needs: dsn diff --git a/doc/user_guide.md b/doc/user_guide.md index 7c2c3ce4..94722316 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -736,6 +736,20 @@ private validate(final AuthenticationRequest request){ } ``` +##### Tags in Markdown and RST Documentation + +Markdown documentation files (`.md` and `.markdown`) and RST files (`.rst`) can cover specification items without being routed through the Tag Importer. Place a full tag in a standalone, single-line native comment: + +```markdown + +``` + +```rst +.. [doc->req~user-guide~1] +``` + +Only complete, standalone Markdown HTML comments and single-line RST comments are recognized. Inline or multi-line comments and RST directives do not import coverage tags. Text outside a native comment that merely resembles a tag is also ignored. + ##### Optional Elements Tags can optionally specify a revision number or name and revision number: diff --git a/importer/lightweightmarkup/pom.xml b/importer/lightweightmarkup/pom.xml index 37fe269b..38f75827 100644 --- a/importer/lightweightmarkup/pom.xml +++ b/importer/lightweightmarkup/pom.xml @@ -18,6 +18,10 @@ org.itsallcode.openfasttrace openfasttrace-api + + org.itsallcode.openfasttrace + openfasttrace-importer-tag-importer-common + org.itsallcode.openfasttrace openfasttrace-testutil diff --git a/importer/lightweightmarkup/src/main/java/module-info.java b/importer/lightweightmarkup/src/main/java/module-info.java index 5a7b7667..6d35087a 100644 --- a/importer/lightweightmarkup/src/main/java/module-info.java +++ b/importer/lightweightmarkup/src/main/java/module-info.java @@ -9,4 +9,5 @@ requires java.logging; requires transitive org.itsallcode.openfasttrace.api; + requires org.itsallcode.openfasttrace.importer.tag.common; } 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 5a05de32..415f5b02 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,13 +1,13 @@ package org.itsallcode.openfasttrace.importer.lightweightmarkup; -import org.itsallcode.openfasttrace.api.core.ItemStatus; -import org.itsallcode.openfasttrace.api.core.SpecificationItem; -import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.Importer; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader.*; import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*; +import org.itsallcode.openfasttrace.importer.tag.common.CoverageTagParser; +import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer; /** * Base class for importers of lightweight markup text. @@ -20,13 +20,14 @@ public abstract class AbstractLightWeightMarkupImporter implements Importer, Lin protected final ImportEventListener listener; /** State machine for a line-by-line parser */ protected final LineParserStateMachine stateMachine; + private final LineConsumer coverageTagParser; private String lastTitle; private boolean inSpecificationItem; private LineContext currentContext; /** * Create a new {@link AbstractLightWeightMarkupImporter}. - * + * * @param file * input file * @param listener @@ -40,6 +41,7 @@ protected AbstractLightWeightMarkupImporter(final InputFile file, final ImportEv this.file = file; this.listener = listener; this.stateMachine = new LineParserStateMachine(configureTransitions()); + this.coverageTagParser = CoverageTagParser.create(null, file, listener); } @Override @@ -50,21 +52,37 @@ public void runImport() /** * Define the transitions of the parser statemachine. - * + * * @return parser statemachine transitions */ protected abstract Transition[] configureTransitions(); + /** + * Tells whether a line is a native comment that can contain a coverage tag. + * + * @param context + * current line and neighboring source lines + * @return {@code true} if the current line is a coverage-tag comment + * candidate + */ + protected abstract boolean isCoverageTagCommentCandidate(LineContext context); + @Override public void nextLine(final LineContext context) { this.currentContext = context; + if (isCoverageTagCommentCandidate(context)) + { + // [impl->dsn~markdown.comment-coverage-tags~1] + // [impl->dsn~rst.comment-coverage-tags~1] + this.coverageTagParser.readLine(context.lineNumber(), context.currentLine()); + } this.stateMachine.step(this.currentContext.currentLine(), this.currentContext.nextLine()); } /** * Define a transition in the parser statemachine. - * + * * @param from * state to be matched against the parsers current state * @param to 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 5aa0322e..2a4a6c8e 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 @@ -2,9 +2,12 @@ import static org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LineParserState.*; +import java.util.regex.Pattern; + import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.importer.lightweightmarkup.AbstractLightWeightMarkupImporter; +import org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader.LineContext; import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*; /** @@ -21,6 +24,8 @@ class MarkdownImporter extends AbstractLightWeightMarkupImporter { private static final LinePattern SECTION_TITLE = new MdSectionTitlePattern(); + private static final Pattern COVERAGE_TAG_COMMENT = Pattern.compile("\\s*\\s*", + Pattern.UNICODE_CHARACTER_CLASS); /** * Creates a {@link MarkdownImporter} object with the given parameters. @@ -35,7 +40,14 @@ class MarkdownImporter extends AbstractLightWeightMarkupImporter super(fileName, listener); } - @SuppressWarnings("squid:S138") // Transition table is OK be larger than 75 lines. + @Override + protected boolean isCoverageTagCommentCandidate(final LineContext context) + { + return COVERAGE_TAG_COMMENT.matcher(context.currentLine()).matches(); + } + + // Transition table is OK be larger than 75 lines. + @SuppressWarnings("squid:S138") protected Transition[] configureTransitions() { // @formatter:off 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 544ab519..8edf8fbb 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 @@ -6,7 +6,7 @@ import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; -import org.itsallcode.openfasttrace.api.importer.ImporterFactory; +import org.itsallcode.openfasttrace.api.importer.*; import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -15,7 +15,8 @@ class TestMarkdownMarkupImporter extends AbstractLightWeightMarkupImporterTest { - private static final ImporterFactory importerFactory = new MarkdownImporterFactory(); + private static final String FULL_COVERAGE_TAG = "[doc" + "->req~guide~1]"; + private static final ImporterFactory importerFactory = createImporterFactory(); TestMarkdownMarkupImporter() { @@ -28,6 +29,41 @@ protected ImporterFactory getImporterFactory() return importerFactory; } + private static ImporterFactory createImporterFactory() + { + final MarkdownImporterFactory factory = new MarkdownImporterFactory(); + factory.init(new ImporterContext(ImportSettings.createDefault())); + return factory; + } + + // [utest->dsn~markdown.comment-coverage-tags~1] + @Test + void testImportsCoverageTagFromStandaloneHtmlComment() + { + assertImport("guide.md", """ + # Guide + + req~ordinary~1 + """.formatted(FULL_COVERAGE_TAG), contains( + item().id("doc", "guide-1257333065", 0) + .addCoveredId("req", "guide", 1) + .location("guide.md", 2).build(), + item().id("req", "ordinary", 1).location("guide.md", 3).build())); + } + + // [utest->dsn~markdown.comment-coverage-tags~1] + @ParameterizedTest + @ValueSource(strings = + { FULL_COVERAGE_TAG, + "before ", + " after", + "" }) + void testIgnoresCoverageTagsOutsideStandaloneHtmlComments(final String line) + { + assertImport("guide.md", line, emptyIterable()); + } + protected String formatTitle(final String title, final int level) { return "#".repeat(level) + " " + title; @@ -194,7 +230,9 @@ void testWhenNotInsideMarkdownCodeBlockThenSpecificationItemMustBeDetected(final contains(item() .id(SpecificationItemId.parseId("req~example~1")) .location("file_without_code_block.md", 4) - .description(endMarker) // End marker looks like part of the description in this case. + // End marker looks like part of the description in this + // case. + .description(endMarker) .build())); } @@ -211,29 +249,29 @@ void testWhenCodeBlockIsInsideCommentSectionThenItIsImportedAsPartOfComment() """, contains(item() .id(SpecificationItemId.createId("req", "comment_with_code_block", 1)) - .comment("```" + System.lineSeparator()+ "This is a code block inside a comment." - + System.lineSeparator() + "```") + .comment("```" + System.lineSeparator() + "This is a code block inside a comment." + + System.lineSeparator() + "```") .location("file_with_code_block_in_comment.md", 1) .build())); } - // [utest -> dsn~disabling-oft-parsing-for-parts-of-a-markup-file~1] @Test - void testDisablingMarkdownParsingForATextBlock() { + void testDisablingMarkdownParsingForATextBlock() + { assertImport("disable_parsing.md", """ `req~stop-parsing~1` - + The next part must not be parsed: - + `req~do-not-parse-me~2` - + Invisible. - + Needs: utest - + Needs: impl """, contains(item() @@ -243,4 +281,4 @@ void testDisablingMarkdownParsingForATextBlock() { .location("disable_parsing.md", 1) .build())); } -} \ No newline at end of file +} 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 caeee4aa..8835f901 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 @@ -2,9 +2,12 @@ import static org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LineParserState.*; +import java.util.regex.Pattern; + import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.importer.lightweightmarkup.AbstractLightWeightMarkupImporter; +import org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader.LineContext; import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*; /** @@ -21,6 +24,11 @@ public class RestructuredTextImporter extends AbstractLightWeightMarkupImporter { private static final LinePattern SECTION_TITLE = new RstSectionTitlePattern(); + private static final Pattern COMMENT_PATTERN = Pattern.compile("\\s*+\\.\\.\\s++.*", + Pattern.UNICODE_CHARACTER_CLASS); + private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("\\s*+\\.\\.\\s++[^\\s:]++::.*", + Pattern.UNICODE_CHARACTER_CLASS); + private static final Pattern INDENTED_CONTINUATION = Pattern.compile("\\s+\\S.*", Pattern.UNICODE_CHARACTER_CLASS); /** * Creates a {@link RestructuredTextImporter} object with the given @@ -37,7 +45,21 @@ public class RestructuredTextImporter extends AbstractLightWeightMarkupImporter } @Override - @SuppressWarnings("java:S138") // Transition table is OK be larger than 75 lines. + protected boolean isCoverageTagCommentCandidate(final LineContext context) + { + final String line = context.currentLine(); + return COMMENT_PATTERN.matcher(line).matches() && !DIRECTIVE_PATTERN.matcher(line).matches() + && !hasIndentedContinuation(context.nextLine()); + } + + private static boolean hasIndentedContinuation(final String nextLine) + { + return nextLine != null && INDENTED_CONTINUATION.matcher(nextLine).matches(); + } + + @Override + // Transition table is OK be larger than 75 lines. + @SuppressWarnings("java:S138") protected Transition[] configureTransitions() { // @formatter:off diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java index 83604687..5ec2f7b4 100644 --- a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java +++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java @@ -15,7 +15,8 @@ public RestructuredTextImporterFactory() } @Override - public int getPriority() { + public int getPriority() + { return 2000; } diff --git a/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java index 40b3cdf2..f6a91f56 100644 --- a/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java +++ b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java @@ -1,11 +1,12 @@ package org.itsallcode.openfasttrace.importer.restructuredtext; +import static org.hamcrest.Matchers.emptyIterable; import static org.itsallcode.matcher.auto.AutoMatcher.contains; import static org.itsallcode.openfasttrace.api.core.SpecificationItemId.createId; import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; -import org.itsallcode.openfasttrace.api.importer.ImporterFactory; +import org.itsallcode.openfasttrace.api.importer.*; import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -13,7 +14,8 @@ class TestRestructuredTextImporter extends AbstractLightWeightMarkupImporterTest { - private static final ImporterFactory importerFactory = new RestructuredTextImporterFactory(); + private static final String FULL_COVERAGE_TAG = "[doc" + "->req~guide~1]"; + private static final ImporterFactory importerFactory = createImporterFactory(); TestRestructuredTextImporter() { @@ -26,6 +28,52 @@ protected ImporterFactory getImporterFactory() return importerFactory; } + private static ImporterFactory createImporterFactory() + { + final RestructuredTextImporterFactory factory = new RestructuredTextImporterFactory(); + factory.init(new ImporterContext(ImportSettings.createDefault())); + return factory; + } + + // [utest->dsn~rst.comment-coverage-tags~1] + @Test + void testImportsCoverageTagFromStandaloneComment() + { + assertImport("guide.rst", """ + Guide + ===== + .. %s + req~ordinary~1 + """.formatted(FULL_COVERAGE_TAG), contains( + item().id("doc", "guide-1442787702", 0) + .addCoveredId("req", "guide", 1) + .location("guide.rst", 3).build(), + item().id("req", "ordinary", 1).location("guide.rst", 4).build())); + } + + // [utest->dsn~rst.comment-coverage-tags~1] + @ParameterizedTest + @ValueSource(strings = + { + FULL_COVERAGE_TAG, + ".. directive:: " + FULL_COVERAGE_TAG, + "..\n " + FULL_COVERAGE_TAG, + " " + FULL_COVERAGE_TAG }) + void testIgnoresCoverageTagsOutsideComments(final String line) + { + assertImport("guide.rst", line, emptyIterable()); + } + + // [utest->dsn~rst.comment-coverage-tags~1] + @Test + void testIgnoresCoverageTagsInMultilineComments() + { + assertImport("guide.rst", """ + .. %s + continuation + """.formatted(FULL_COVERAGE_TAG), emptyIterable()); + } + protected String formatTitle(final String title, final int level) { return title + "\n" + "=".repeat(title.length()); @@ -147,20 +195,21 @@ void testLessThenThreeUnderliningCharactersAreNotDetectedAsTitleUnderlines() // [utest -> dsn~disabling-oft-parsing-for-parts-of-a-markup-file~1] @Test - void testDisablingRstParsingForATextBlock() { + void testDisablingRstParsingForATextBlock() + { assertImport("disable_parsing.rst", """ `req~stop-parsing~1` - + The next part must not be parsed: - + .. oft:off `req~do-not-parse-me~2` - + Invisible. - + Needs: utest .. oft:on - + Needs: impl """, contains(item() diff --git a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java index 068bb600..ddfbfe90 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java @@ -14,6 +14,8 @@ import org.itsallcode.openfasttrace.core.Oft; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class ImporterFactoryLoaderIT { @@ -64,4 +66,37 @@ void testGherkinImporterAlsoImportsBasicCoverageTags(@TempDir final Path tempDir hasProperty("id", hasToString("scn~login~1")), hasProperty("id", hasToString("impl~login~1")))); } + + // [itest->dsn~markdown.comment-coverage-tags~1] + // [itest->dsn~rst.comment-coverage-tags~1] + @ParameterizedTest + @ValueSource(strings = + { "markdown.md", "markdown.markdown", "restructuredtext.rst" }) + void testSelectsLightWeightMarkupImportersBeforeTagImporter(final String fileName, @TempDir final Path tempDir) + throws IOException + { + final Oft oft = Oft.create(); + final String nonCommentTag = "[impl~must-not-be-imported~1" + + "->req~covered~1]"; + Files.writeString(tempDir.resolve(fileName), """ + %s + %s + """.formatted(coverageComment(fileName), nonCommentTag)); + final ImportSettings settings = ImportSettings.builder() + .addInputs(tempDir) + .build(); + + final List items = oft.importItems(settings); + + assertThat(items, contains( + allOf( + hasProperty("id", hasToString("impl~markdown-comment~1")), + hasProperty("coveredIds", contains(hasToString("req~covered~1")))))); + } + + private static String coverageComment(final String fileName) + { + final String coverageTag = "[impl~markdown-comment~1" + "->req~covered~1]"; + return fileName.endsWith(".rst") ? ".. " + coverageTag : ""; + } }