Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agents/skills/openfasttrace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ Multiple coverage:
// [dsn -> req~local-stability~1,arch~dimensional-input~1]
```

Multi-line coverage (source files and Gherkin comments; a trailing comma continues the tag, leading comment markers and whitespace are ignored):

```Java
// [dsn~stability~1 -> req~local-stability~1,
// arch~dimensional-input~1]
```

## Tracing

## Syntax: Gherkin
Expand Down
1 change: 1 addition & 0 deletions doc/changes/changes_4.11.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The user guide documentation now has automatic lateral navigation.

* #610: Allow adding plugins via configuration using `Oft.builder().addPlugin(name, jars...)`
* #612: Add support for Visual Basic in the source tag importer
* #632: Allow full coverage tags in source files and Gherkin comments to span multiple lines

## Bugfixes

Expand Down
49 changes: 49 additions & 0 deletions doc/spec/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,20 @@ Covers:

Needs: impl, utest

#### Support Multi-line Coverage Tags in Comments
`dsn~gherkin.multi-line-comment-coverage-tags~1`

The Gherkin importer imports full coverage tags that span consecutive comment lines, using the shared multi-line coverage-tag parser (see `dsn~import.full-coverage-tag-multi-line~1`).

An executable or blank line ends the comment block. If it occurs while a multi-line coverage tag is still open, the import fails with an error naming the file and line, because the tag would otherwise continue across executable Gherkin text.

Covers:

* `req~import.full-coverage-tag-multi-line~1`
* `req~gherkin-comment-coverage-tags~1`

Needs: impl, utest

### ReqM2 File Detection
`dsn~import.reqm2-file-detection~1`

Expand Down Expand Up @@ -1204,6 +1218,41 @@ Covers:

Needs: impl, utest

#### Full Coverage Tag Format Spanning Multiple Lines
`dsn~import.full-coverage-tag-multi-line~1`

The Tag importer and the Gherkin importer import full coverage tags whose list of covered IDs continues on following lines:

multi-line-full-tag = opening-line *continuation-line closing-line

opening-line = "[" *WSP reference *WSP "->" *WSP requirement-id
*(*WSP "," *WSP requirement-id) *WSP "," *WSP EOL

continuation-line = *WSP [comment-marker *WSP] requirement-id
*(*WSP "," *WSP requirement-id) *WSP "," *WSP EOL

closing-line = *WSP [comment-marker *WSP] requirement-id
*(*WSP "," *WSP requirement-id) *WSP
[">>" *WSP artifact-type *WSP *("," *WSP artifact-type) *WSP] "]"

comment-marker = 1*(any character except letters, digits, whitespace, "[" and "]")

A comma at the end of the opening line starts the multi-line tag. The importer removes the comment marker and whitespace at the start of each following line, joins the lines and imports the result like a single-line full coverage tag located at the opening line.

If a following line is neither a continuation line nor a closing line, or if the file ends before the closing `]`, the import fails with an error naming the file and line.

Markdown and RST comments only support single-line coverage tags.

Rationale:

Aligning covered IDs on separate lines keeps long coverage lists readable and reviewable. Requiring the trailing comma avoids changing how existing files with unclosed tag-like text are imported. Failing instead of ignoring a broken multi-line tag prevents silently losing coverage.

Covers:

* `req~import.full-coverage-tag-multi-line~1`

Needs: impl, utest

#### Full Coverage Tag Format Allows Specifying a Revision
`dsn~import.full-coverage-tag-with-revision~1`

Expand Down
15 changes: 15 additions & 0 deletions doc/spec/system_requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,21 @@ Covers:

Needs: scn, dsn

##### Import Multi-line Full Coverage Tags
`req~import.full-coverage-tag-multi-line~1`

OFT imports full coverage tags whose list of covered IDs spans multiple comment lines in source files and Gherkin feature files.

Rationale:

Source and test artifacts often cover many specification items. Listing them on a single line makes the tag hard to read and review. Spreading the list over several aligned lines keeps the coverage tag maintainable.

Covers:

* [feat~coverage-tag-import~2](#coverage-tag-import)

Needs: dsn

##### Import Short Coverage Tag Format
`req~import.short-coverage-tag-format~2`

Expand Down
35 changes: 35 additions & 0 deletions doc/user_guide/reference/input_format_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,32 @@ Examples:
// [impl~validate-password~2->dsn~validate-authentication-request~1]
```

##### Covering Multiple Items

A single tag can cover several specification items. Separate the covered IDs with commas:

```
[ <covered-artifact-type> -> <specification-object-id> , <specification-object-id> ... ]
```

##### Multi-line Tags

When a tag covers many items, you can spread the list of covered IDs over multiple comment lines:

```ada
-- [SRC~ICM45686-CORE~1->CPR~ICM45686-0001~1,
-- CPR~ICM45686-0002~1,
-- CPR~ICM45686-0010~1]
```

This is equivalent to writing all covered IDs on the first line. The item is located at the line where the tag starts. Needed coverage (`>> ...`) goes on the last line, before the closing bracket.

> Note: Comment markers made of punctuation are supported (e.g. `--`, `//`, `#`, `*`, `'`, `;`, `%`, `!`). Comment markers made of letters, like Visual Basic's `REM`, are not.

If a tag ends with a comma but the following line does not continue the list or close it with `]`, OFT reports an error with the file and line instead of silently ignoring the tag.

Multi-line tags are supported by the Tag Importer and in Gherkin comments. Coverage tags in Markdown and RST comments must stay on a single line.

##### Needed Coverage

When using UML models as design document files like UML models it is useful to add needed coverage as well. To do this, you can use the following format:
Expand Down Expand Up @@ -151,6 +177,15 @@ The ID tag becomes the item location and the scenario header becomes its title;

Existing full coverage tags remain supported in Gherkin comments, for example `# [impl~login~1 -> dsn~authentication~1]`. OFT deliberately ignores coverage-tag-shaped text in executable Gherkin lines.

A comment coverage tag can span several consecutive comment lines (see [Multi-line Tags](#multi-line-tags)):

```gherkin
# [tst~login-test~1 -> req~authentication~1,
# req~session-timeout~2]
```

An executable or blank line inside such a tag is reported as an error.

#### Markdown

The main importer of OFT accepts Markdown files with the extensions `.md` and `.markdown`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,13 @@ final class GherkinLineConsumer implements LineConsumer
{
this.file = file;
this.listener = listener;
this.coverageTagParser = CoverageTagParser.create(null, file, listener);
this.coverageTagParser = CoverageTagParser.createWithMultiLineTags(null, file, listener);
}

@Override
public void readLine(final int lineNumber, final String line)
{
if (line.trim().startsWith("#"))
{
this.coverageTagParser.readLine(lineNumber, line);
}
readCoverageTags(lineNumber, line);
final Matcher scenario = SCENARIO.matcher(line);
if (scenario.matches())
{
Expand Down Expand Up @@ -92,9 +89,25 @@ public void readLine(final int lineNumber, final String line)
readMetadata(lineNumber, line);
}

// [impl->dsn~gherkin.multi-line-comment-coverage-tags~1]
private void readCoverageTags(final int lineNumber, final String line)
{
if (line.trim().startsWith("#"))
{
this.coverageTagParser.readLine(lineNumber, line);
}
else
{
// Executable lines are never scanned for coverage tags. Passing an empty line instead lets a
// multi-line coverage tag that is still open fail instead of continuing across executable lines.
this.coverageTagParser.readLine(lineNumber, "");
}
}

@Override
public void finish()
{
this.coverageTagParser.finish();
endScenario();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
Expand All @@ -15,6 +16,7 @@

import org.itsallcode.openfasttrace.api.core.*;
import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
import org.itsallcode.openfasttrace.api.importer.ImporterException;
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions;
import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput;
Expand Down Expand Up @@ -365,6 +367,41 @@ void testImportsCommentCoverageTagsWhileScenarioIsOpen()
events.verify(listener).endSpecificationItem();
}

// [utest->dsn~gherkin.multi-line-comment-coverage-tags~1]
@Test
void testImportsMultiLineCommentCoverageTag()
{
final String source = """
# [%s,
# req~second~2,
# req~third~3]
@id:scn~ordinary~1
Scenario: ordinary
Given a step
""".formatted("tst~login-test~1 -> req~first~1");
final List<SpecificationItem> items = importText(source);

assertThat(items, containsInAnyOrder(
hasProperty("id", hasToString("scn~ordinary~1")),
allOf(hasProperty("id", hasToString("tst~login-test~1")),
hasProperty("coveredIds", contains(hasToString("req~first~1"), hasToString("req~second~2"),
hasToString("req~third~3"))))));
}

// [utest->dsn~gherkin.multi-line-comment-coverage-tags~1]
@Test
void testRejectsMultiLineCommentCoverageTagInterruptedByExecutableLine()
{
final String source = """
# [%s,
Scenario: ordinary
# req~second~2]
""".formatted("tst~login-test~1 -> req~first~1");

final ImporterException exception = assertThrows(ImporterException.class, () -> importText(source));
assertThat(exception.getMessage(), containsString("specification.feature:2"));
}

private static List<SpecificationItem> importText(final String source)
{
return ImportAssertions.runImporterOnText(Path.of("specification.feature"), source, FACTORY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,29 @@ private AbstractRegexLineConsumer(final Pattern pattern) {

@Override
public void readLine(final int lineNumber, final String line) {
processMatches(lineNumber, line, 0);
}

/**
* Process all matches in a line, starting at the given column.
*
* @param lineNumber
* line number, starting at {@code 1}
* @param line
* line content
* @param startColumn
* column where the search starts
* @return number of processed matches
*/
int processMatches(final int lineNumber, final String line, final int startColumn) {
final Matcher matcher = this.pattern.matcher(line);
matcher.region(startColumn, line.length());
int counter = 0;
while (matcher.find()) {
processMatch(matcher, lineNumber, counter);
counter++;
}
return counter;
}

abstract void processMatch(Matcher matcher, int lineNumber, int lineMatchCount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,32 @@ private CoverageTagParser() {
*/
public static LineConsumer create(final PathConfig config, final InputFile file,
final ImportEventListener listener) {
return create(config, file, listener, false);
}

/**
* Create a line consumer for coverage tags in an input file that also
* imports full coverage tags spanning multiple lines.
*
* @param config
* optional configuration for short coverage tags
* @param file
* input file that contains the tags
* @param listener
* listener receiving imported specification items
* @return line consumer that imports single- and multi-line full coverage
* tags and configured short coverage tags
*/
// [impl->dsn~import.full-coverage-tag-multi-line~1]
public static LineConsumer createWithMultiLineTags(final PathConfig config, final InputFile file,
final ImportEventListener listener) {
return create(config, file, listener, true);
}

private static LineConsumer create(final PathConfig config, final InputFile file,
final ImportEventListener listener, final boolean multiLineTags) {
final List<LineConsumer> parsers = new ArrayList<>();
parsers.add(new LongTagImportingLineConsumer(file, listener));
parsers.add(new LongTagImportingLineConsumer(file, listener, multiLineTags));
if (config != null) {
parsers.add(new ShortTagImportingLineConsumer(config, file, listener));
}
Expand Down
Loading
Loading