From 10b1f398311715ca278b74aba25f99045629609b Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 6 Aug 2026 16:15:59 +0200 Subject: [PATCH 01/16] #570: Fill location in importers --- .../api/core/SpecificationItem.java | 30 ++++++++++ .../importer/SpecificationListBuilder.java | 31 +++++++++- .../api/core/TestSpecificationItem.java | 23 ++++++++ .../TestSpecificationListBuilder.java | 34 +++++++++++ doc/spec/design.md | 40 +++++++++++++ doc/spec/system_requirements.md | 12 ++++ .../importer/gherkin/GherkinLineConsumer.java | 53 ++++++++++++++---- .../AbstractLightWeightMarkupImporter.java | 27 ++++++++- .../handler/DependenciesHandlerBuilder.java | 5 +- .../ProvidesCoverageHandlerBuilder.java | 5 +- .../handler/SpecObjectsHandlerBuilder.java | 4 +- .../common/LongTagImportingLineConsumer.java | 56 ++++++++++++++++--- .../common/ShortTagImportingLineConsumer.java | 22 ++++++-- 13 files changed, 312 insertions(+), 30 deletions(-) diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java index 6e7b9727e..dba11fbc1 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java @@ -48,6 +48,16 @@ public SpecificationItemId getId() return this.id.getId(); } + /** + * Get the declared ID together with its source occurrence. + * + * @return declared located ID + */ + public LocatedSpecificationItemId getLocatedId() + { + return this.id; + } + /** * Get the artifact type of the specification item * @@ -134,6 +144,16 @@ public List getCoveredIds() return this.coveredIds.stream().map(LocatedSpecificationItemId::getId).toList(); } + /** + * Get covered IDs together with their source occurrences. + * + * @return located covered IDs + */ + public List getLocatedCoveredIds() + { + return Collections.unmodifiableList(this.coveredIds); + } + /** * Add a covered {@link SpecificationItemId} to the list of covered IDs. *

@@ -167,6 +187,16 @@ public List getDependOnIds() return this.dependOnIds.stream().map(LocatedSpecificationItemId::getId).toList(); } + /** + * Get dependency IDs together with their source occurrences. + * + * @return located dependency IDs + */ + public List getLocatedDependOnIds() + { + return this.dependOnIds; + } + /** * Get the list of artifact types this specification item need to be covered * in diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java index f3bc5d5db..608e0afcd 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java @@ -15,7 +15,7 @@ public final class SpecificationListBuilder implements ImportEventListener private final FilterSettings filterSettings; private final List items = new LinkedList<>(); private SpecificationItem.Builder itemBuilder; - private SpecificationItemId id; + private LocatedSpecificationItemId id; private StringBuilder description = new StringBuilder(); private StringBuilder rationale = new StringBuilder(); private StringBuilder comment = new StringBuilder(); @@ -68,6 +68,13 @@ private void resetState() @Override public void setId(final SpecificationItemId id) { + this.setId(LocatedSpecificationItemId.builder().id(id).build()); + } + + @Override + public void setId(final LocatedSpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-storage~1] this.id = id; } @@ -80,6 +87,7 @@ public void setStatus(final ItemStatus status) @Override public void addCoveredId(final SpecificationItemId id) { + // TODO: call new method // [impl->dsn~filtering-by-artifact-types-during-import~1] if (isAcceptedArtifactType(id.getArtifactType())) { @@ -87,6 +95,16 @@ public void addCoveredId(final SpecificationItemId id) } } + @Override + public void addCoveredId(final LocatedSpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-storage~1] + if (isAcceptedArtifactType(id.getId().getArtifactType())) + { + this.itemBuilder.addCoveredId(id); + } + } + @Override public void appendDescription(final String fragment) { @@ -108,6 +126,7 @@ public void appendComment(final String fragment) @Override public void addDependsOnId(final SpecificationItemId id) { + // TODO: call new method // [impl->dsn~filtering-by-artifact-types-during-import~1] if (isAcceptedArtifactType(id.getArtifactType())) { @@ -115,6 +134,16 @@ public void addDependsOnId(final SpecificationItemId id) } } + @Override + public void addDependsOnId(final LocatedSpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-storage~1] + if (isAcceptedArtifactType(id.getId().getArtifactType())) + { + this.itemBuilder.addDependOnId(id); + } + } + @Override public void addNeededArtifactType(final String artifactType) { diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java index fd0e0e8ea..e50a7beca 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -41,6 +42,28 @@ void testPreservesCompatibilityForUnlocatedIds() () -> assertThat(item.getDependOnIds(), contains(DEPEND_ON_ID))); } + // [utest->dsn~located-specification-item-id-storage~1] + @Test + void testLocatedCoveredIdsAreImmutable() + { + final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)) + .addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build(); + + assertThrows(UnsupportedOperationException.class, + () -> item.getLocatedCoveredIds().add(locatedId(ID))); + } + + // [utest->dsn~located-specification-item-id-storage~1] + @Test + void testLocatedDependOnIdsAreImmutable() + { + final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)) + .addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build(); + + assertThrows(UnsupportedOperationException.class, + () -> item.getLocatedDependOnIds().add(locatedId(ID))); + } + private static LocatedSpecificationItemId locatedId(final SpecificationItemId id) { return LocatedSpecificationItemId.builder().id(id).range( diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java index 30d8231fd..db3a02b26 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java @@ -16,6 +16,40 @@ class TestSpecificationListBuilder private static final String TITLE = "title"; private static final SpecificationItemId ID = SpecificationItemId.parseId("feat~id~1"); + // [utest->dsn~located-specification-item-id-storage~1] + @Test + void testPreservesLocatedIdOccurrences() + { + final SpecificationItemId coveredId = SpecificationItemId.parseId("req~covered~1"); + final SpecificationItemId dependencyId = SpecificationItemId.parseId("req~dependency~1"); + final LocatedSpecificationItemId locatedId = locatedId(ID, 0); + final LocatedSpecificationItemId firstCoveredId = locatedId(coveredId, 5); + final LocatedSpecificationItemId secondCoveredId = locatedId(coveredId, 20); + final LocatedSpecificationItemId locatedDependencyId = locatedId(dependencyId, 3); + final SpecificationListBuilder builder = SpecificationListBuilder.create(); + builder.beginSpecificationItem(); + builder.setId(locatedId); + builder.addCoveredId(firstCoveredId); + builder.addCoveredId(secondCoveredId); + builder.addDependsOnId(locatedDependencyId); + + final SpecificationItem item = builder.build().get(0); + + assertAll( + () -> assertThat(item.getLocatedId(), equalTo(locatedId)), + () -> assertThat(item.getLocatedCoveredIds(), contains(firstCoveredId, secondCoveredId)), + () -> assertThat(item.getLocatedDependOnIds(), contains(locatedDependencyId)), + () -> assertThat(item.getCoveredIds(), contains(coveredId, coveredId)), + () -> assertThat(item.getDependOnIds(), contains(dependencyId))); + } + + private static LocatedSpecificationItemId locatedId(final SpecificationItemId id, final int column) + { + final SourceRange range = new SourceRange(new SourcePosition(0, column), + new SourcePosition(0, column + id.toString().length())); + return LocatedSpecificationItemId.builder().id(id).range(range).build(); + } + @Test void testBuildBasicItem() { diff --git a/doc/spec/design.md b/doc/spec/design.md index b32803e29..6f223a82c 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -110,6 +110,46 @@ Importers emit events if they find parts of a [specification item](#specificatio ### Specification List Builder The specification list builder is an import event listener that creates a list of specification items from import events. +#### Located Specification Item ID Storage +`dsn~located-specification-item-id-storage~1` + +`SpecificationItem` and `SpecificationListBuilder` preserve the individual +declared, Covers, and Depends occurrences as `LocatedSpecificationItemId` +values while retaining compatible semantic-ID accessors. Located-ID lists are +immutable when observed through the public API. + +Covers: + +* req~located-specification-item-ids~1 + +Needs: impl, utest + +#### Text Source Ranges +`dsn~located-specification-item-id-text-ranges~1` + +Text importers create zero-based UTF-16, start-inclusive and end-exclusive +ranges for source ID occurrences and their represented components. Equal +semantic IDs at distinct source occurrences remain separate values. + +Needs: impl + +#### Coverage-tag Source Ranges +`dsn~located-specification-item-id-tag-ranges~1` + +Coverage-tag importers locate source-backed declared and covered ID components. +They leave component ranges absent when a tag generates the corresponding ID +component. + +Needs: impl + +#### SpecObject ID Occurrences +`dsn~located-specification-item-id-specobject~1` + +The SpecObject importer emits located declared, covered, and dependency IDs +without ranges because its XML event model does not expose character offsets. + +Needs: impl + ## Command Line Interpreter The command line interpreter (CLI) takes parameters given to OFT and parses them. It is responsible for making sense of the parameter contents and issuing help and error messages about the command line syntax. diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index 37662d7e7..d35591171 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -143,6 +143,18 @@ Covers: Needs: dsn +### Located Specification Item IDs +`req~located-specification-item-ids~1` + +For every imported declared, covered, and dependency specification item ID, +OFT shall retain the individual source occurrence. For source text, positions +shall be zero-based UTF-16 offsets with start-inclusive, end-exclusive ranges. +Where an ID component is generated or its source is unavailable, its component +range shall be absent. Equal semantic IDs at distinct source occurrences shall +remain distinct occurrences. + +Needs: dsn + #### Validate Gherkin Covers Metadata `req~gherkin-covers-validation~1` diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java index 4afb707c0..f26982279 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java @@ -6,7 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -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.input.InputFile; import org.itsallcode.openfasttrace.importer.tag.common.CoverageTagParser; @@ -33,8 +33,8 @@ final class GherkinLineConsumer implements LineConsumer private final InputFile file; private final ImportEventListener listener; private final LineConsumer coverageTagParser; - private SpecificationItemId pendingId; - private Set coveredIds = new LinkedHashSet<>(); + private LocatedSpecificationItemId pendingId; + private Set coveredIds = new LinkedHashSet<>(); private Set neededArtifactTypes = new LinkedHashSet<>(); private boolean hasNeedsDirective; private boolean metadataRegion; @@ -91,13 +91,13 @@ private void readMetadata(final int lineNumber, final String line) { if (line.trim().startsWith("@")) { - readTagRegion(lineNumber, line.trim()); + readTagRegion(lineNumber, line); return; } final Matcher directive = OFT_DIRECTIVE.matcher(line); if (this.metadataRegion && directive.matches()) { - readDirective(lineNumber, directive.group(1), directive.group(2)); + readDirective(lineNumber, line, directive.group(1), directive.group(2)); return; } if (!line.trim().startsWith("#")) @@ -106,8 +106,9 @@ private void readMetadata(final int lineNumber, final String line) } } - private void readTagRegion(final int lineNumber, final String tags) + private void readTagRegion(final int lineNumber, final String line) { + final String tags = line.trim(); if (!this.tagRegion) { clearMetadata(); @@ -126,7 +127,7 @@ private void readTagRegion(final int lineNumber, final String tags) invalidateMetadata(lineNumber, "multiple @id tags before a scenario"); return; } - this.pendingId = parseId(lineNumber, matcher.group(1)); + this.pendingId = locatedId(lineNumber, line, line.indexOf(tags) + matcher.start(1), matcher.group(1)); if (this.pendingId == null) { return; @@ -135,7 +136,7 @@ private void readTagRegion(final int lineNumber, final String tags) } } - private void readDirective(final int lineNumber, final String name, final String values) + private void readDirective(final int lineNumber, final String line, final String name, final String values) { this.tagRegion = false; if (this.invalidMetadata) @@ -156,7 +157,7 @@ private void readDirective(final int lineNumber, final String name, final String final String[] entries = splitValues(lineNumber, name, values); if (covers) { - readCoveredIds(lineNumber, entries); + readCoveredIds(lineNumber, line, entries); return; } readNeededArtifactTypes(lineNumber, entries); @@ -173,8 +174,9 @@ private String[] splitValues(final int lineNumber, final String name, final Stri return values.trim().split(",", -1); } - private void readCoveredIds(final int lineNumber, final String[] entries) + private void readCoveredIds(final int lineNumber, final String line, final String[] entries) { + int searchStart = 0; for (final String entry : entries) { final String value = requireValue(lineNumber, "Covers", entry); @@ -182,16 +184,19 @@ private void readCoveredIds(final int lineNumber, final String[] entries) { return; } + final int column = line.indexOf(value, searchStart); + searchStart = column + value.length(); final SpecificationItemId id = parseId(lineNumber, value); if (id == null) { return; } - if (!this.coveredIds.add(id)) + if (this.coveredIds.stream().anyMatch(locatedId -> locatedId.getId().equals(id))) { invalidateMetadata(lineNumber, "Covers directive contains duplicate value '" + id + "'"); return; } + this.coveredIds.add(locatedId(lineNumber, column, value, id)); } } @@ -285,4 +290,30 @@ private void invalidateMetadata(final int lineNumber, final String reason) this.neededArtifactTypes.clear(); this.invalidMetadata = true; } + + private LocatedSpecificationItemId locatedId(final int lineNumber, final String line, final int column, + final String value) + { + return locatedId(lineNumber, column, value, SpecificationItemId.parseId(value)); + } + + private static LocatedSpecificationItemId locatedId(final int lineNumber, final int column, final String value, + final SpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-text-ranges~1] + final int line = lineNumber - 1; + final SourceRange range = new SourceRange(new SourcePosition(line, column), + new SourcePosition(line, column + value.length())); + final int artifactEnd = value.indexOf('~'); + final int revisionStart = value.lastIndexOf('~') + 1; + return LocatedSpecificationItemId.builder().id(id).range(range) + .artifactTypeRange(componentRange(line, column, column + artifactEnd)) + .nameRange(componentRange(line, column + artifactEnd + 1, column + revisionStart - 1)) + .revisionRange(componentRange(line, column + revisionStart, column + value.length())).build(); + } + + private static SourceRange componentRange(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } } 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 415f5b026..68bcbcbb3 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 @@ -138,7 +138,7 @@ protected void informListenerAboutNewItem() final String idText = this.stateMachine.getLastToken(); final SpecificationItemId id = new SpecificationItemId.Builder(idText).build(); this.listener.beginSpecificationItem(); - this.listener.setId(id); + this.listener.setId(locatedId(idText, id)); this.listener.setLocation(this.file.getPath(), this.currentContext.lineNumber()); if (this.lastTitle != null) { @@ -226,7 +226,7 @@ protected void addDependency() { final SpecificationItemId.Builder builder = new SpecificationItemId.Builder( this.stateMachine.getLastToken()); - this.listener.addDependsOnId(builder.build()); + this.listener.addDependsOnId(locatedId(this.stateMachine.getLastToken(), builder.build())); } /** @@ -264,7 +264,28 @@ protected void resetTitle() */ protected void addCoverage() { - this.listener.addCoveredId(SpecificationItemId.parseId(this.stateMachine.getLastToken())); + final String idText = this.stateMachine.getLastToken(); + this.listener.addCoveredId(locatedId(idText, SpecificationItemId.parseId(idText))); + } + + private LocatedSpecificationItemId locatedId(final String idText, final SpecificationItemId id) + { + // [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); + final int revisionStart = idText.lastIndexOf(SpecificationItemId.REVISION_SEPARATOR) + 1; + return LocatedSpecificationItemId.builder().id(id).range(range) + .artifactTypeRange(range(column, column + artifactTypeEnd)) + .nameRange(range(column + artifactTypeEnd + 1, column + revisionStart - 1)) + .revisionRange(range(column + revisionStart, column + idText.length())).build(); + } + + private SourceRange range(final int start, final int end) + { + final int line = this.currentContext.lineNumber() - 1; + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); } /** diff --git a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/DependenciesHandlerBuilder.java b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/DependenciesHandlerBuilder.java index c7e6fe923..d703a517c 100644 --- a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/DependenciesHandlerBuilder.java +++ b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/DependenciesHandlerBuilder.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.specobject.handler; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.api.core.LocatedSpecificationItemId; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.importer.xmlparser.tree.CallbackContentHandler; import org.itsallcode.openfasttrace.importer.xmlparser.tree.TreeContentHandler; @@ -18,8 +19,10 @@ class DependenciesHandlerBuilder TreeContentHandler build() { + // [impl->dsn~located-specification-item-id-specobject~1] this.handler.addCharacterDataListener("dependson", - data -> this.listener.addDependsOnId(SpecificationItemId.parseId(data))); + data -> this.listener.addDependsOnId(LocatedSpecificationItemId.builder() + .id(SpecificationItemId.parseId(data)).build())); return this.handler; } } diff --git a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/ProvidesCoverageHandlerBuilder.java b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/ProvidesCoverageHandlerBuilder.java index 6e4d79f45..bd650a234 100644 --- a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/ProvidesCoverageHandlerBuilder.java +++ b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/ProvidesCoverageHandlerBuilder.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.specobject.handler; import org.itsallcode.openfasttrace.api.core.SpecificationItemId.Builder; +import org.itsallcode.openfasttrace.api.core.LocatedSpecificationItemId; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.importer.xmlparser.tree.CallbackContentHandler; import org.itsallcode.openfasttrace.importer.xmlparser.tree.TreeContentHandler; @@ -22,7 +23,9 @@ TreeContentHandler build() this.handler.addElementListener("provcov", elem -> this.providesCoverageIdBuilder = new Builder(), // endElem -> { - this.listener.addCoveredId(this.providesCoverageIdBuilder.build()); + // [impl->dsn~located-specification-item-id-specobject~1] + this.listener.addCoveredId(LocatedSpecificationItemId.builder() + .id(this.providesCoverageIdBuilder.build()).build()); this.providesCoverageIdBuilder = null; }); diff --git a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecObjectsHandlerBuilder.java b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecObjectsHandlerBuilder.java index e96506ded..0a16deda7 100644 --- a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecObjectsHandlerBuilder.java +++ b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecObjectsHandlerBuilder.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.specobject.handler; import org.itsallcode.openfasttrace.api.core.Location; +import org.itsallcode.openfasttrace.api.core.LocatedSpecificationItemId; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.core.SpecificationItemId.Builder; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; @@ -48,7 +49,8 @@ private void handleStartElement(final TreeElement elem) private void handleEndElement() { - this.listener.setId(this.idBuilder.build()); + // [impl->dsn~located-specification-item-id-specobject~1] + this.listener.setId(LocatedSpecificationItemId.builder().id(this.idBuilder.build()).build()); this.listener.setLocation(this.locationBuilder.build()); this.listener.endSpecificationItem(); this.idBuilder = null; diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java index 6290593a9..d2c355ba9 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java @@ -7,8 +7,7 @@ import java.util.logging.Logger; import java.util.regex.Matcher; -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.input.InputFile; @@ -66,27 +65,68 @@ public void processMatch(final Matcher matcher, final int lineNumber, final int assert generatedIds.size() == coveredIds.size(); for (int i = 0; i < generatedIds.size(); i++) { - addSpecificationItem(lineNumber, generatedIds.get(i), List.of(coveredIds.get(i)), neededArtifactTypes); + addSpecificationItem(lineNumber, matcher, generatedIds.get(i), List.of(coveredIds.get(i)), + neededArtifactTypes); } } else { - addSpecificationItem(lineNumber, generatedIds.get(0), coveredIds, neededArtifactTypes); + addSpecificationItem(lineNumber, matcher, generatedIds.get(0), coveredIds, neededArtifactTypes); } } - private void addSpecificationItem(final int lineNumber, final SpecificationItemId generatedId, - final List coveredIds, final List neededArtifactTypes) + private void addSpecificationItem(final int lineNumber, final Matcher matcher, + final SpecificationItemId generatedId, final List coveredIds, + final List neededArtifactTypes) { final SpecificationItem.Builder item = SpecificationItem.builder() - .id(generatedId) + .id(locatedGeneratedId(lineNumber, matcher, generatedId)) .location(this.file.getPath(), lineNumber); - coveredIds.forEach(item::addCoveredId); + int searchStart = 0; + for (final SpecificationItemId coveredId : coveredIds) + { + final int start = matcher.group("coveredIds").indexOf(coveredId.toString(), searchStart); + searchStart = start + coveredId.toString().length(); + item.addCoveredId(locatedId(lineNumber, matcher.start("coveredIds") + start, coveredId)); + } neededArtifactTypes.forEach(item::addNeedsArtifactType); this.listener.addSpecificationItem(item.build()); logItem(lineNumber, coveredIds, neededArtifactTypes, generatedId); } + private static LocatedSpecificationItemId locatedGeneratedId(final int lineNumber, final Matcher matcher, + final SpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-tag-ranges~1] + if (matcher.group("customName") == null) + { + return LocatedSpecificationItemId.builder().id(id).build(); + } + final int start = matcher.start("artifactType"); + final int end = matcher.end("revision"); + return LocatedSpecificationItemId.builder().id(id).range(sourceRange(lineNumber, start, end)) + .artifactTypeRange(sourceRange(lineNumber, start, matcher.end("artifactType"))) + .nameRange(sourceRange(lineNumber, matcher.start("customName"), matcher.end("customName"))) + .revisionRange(sourceRange(lineNumber, matcher.start("revision"), matcher.end("revision"))).build(); + } + + private static LocatedSpecificationItemId locatedId(final int lineNumber, final int start, + final SpecificationItemId id) + { + final String text = id.toString(); + final int typeEnd = text.indexOf('~'); + final int revisionStart = text.lastIndexOf('~') + 1; + return LocatedSpecificationItemId.builder().id(id).range(sourceRange(lineNumber, start, start + text.length())) + .artifactTypeRange(sourceRange(lineNumber, start, start + typeEnd)) + .nameRange(sourceRange(lineNumber, start + typeEnd + 1, start + revisionStart - 1)) + .revisionRange(sourceRange(lineNumber, start + revisionStart, start + text.length())).build(); + } + + private static SourceRange sourceRange(final int lineNumber, final int start, final int end) + { + return new SourceRange(new SourcePosition(lineNumber - 1, start), new SourcePosition(lineNumber - 1, end)); + } + private static List parseCoveredIds(final String input) { if (input == null) diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java index d246dbe09..c89fd5a62 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java @@ -3,8 +3,7 @@ import java.util.logging.Logger; import java.util.regex.Matcher; -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.ImporterException; import org.itsallcode.openfasttrace.api.importer.input.InputFile; @@ -46,12 +45,27 @@ void processMatch(final Matcher matcher, final int lineNumber, final int lineMat LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + tagItemId + "' covering id '" + coveredId + "'"); this.listener.addSpecificationItem(SpecificationItem.builder() - .id(tagItemId) + .id(LocatedSpecificationItemId.builder().id(tagItemId).build()) .location(this.file.toString(), lineNumber) - .addCoveredId(coveredId) + .addCoveredId(locatedCoveredId(matcher, lineNumber, coveredId)) .build()); } + private static LocatedSpecificationItemId locatedCoveredId(final Matcher matcher, final int lineNumber, + final SpecificationItemId id) + { + // [impl->dsn~located-specification-item-id-tag-ranges~1] + final int start = matcher.start(1); + final int end = matcher.end(2); + final SourceRange range = new SourceRange(new SourcePosition(lineNumber - 1, start), + new SourcePosition(lineNumber - 1, end)); + return LocatedSpecificationItemId.builder().id(id).range(range) + .nameRange(new SourceRange(new SourcePosition(lineNumber - 1, start), + new SourcePosition(lineNumber - 1, matcher.end(1)))) + .revisionRange(new SourceRange(new SourcePosition(lineNumber - 1, matcher.start(2)), + new SourcePosition(lineNumber - 1, end))).build(); + } + private SpecificationItemId createCoveredItem(final String name, final String revision) { final int parsedRevision = parseRevision(name, revision); final String nameWithPrefix = getCoveredItemNamePrefix() + name; From 71b5c22f8d06b5a07379445ee3aa438757456f7a Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 6 Aug 2026 16:24:52 +0200 Subject: [PATCH 02/16] Add unit tests for location --- AGENTS.md | 1 + doc/spec/design.md | 6 +-- .../importer/gherkin/GherkinImporterTest.java | 24 ++++++++- .../markdown/TestMarkdownMarkupImporter.java | 35 ++++++++++++- .../TestRestructuredTextImporter.java | 34 ++++++++++++- .../TestLongTagImportingLineConsumer.java | 23 ++++++++- .../TestShortTagImportingLineConsumer.java | 49 +++++++++++++++---- .../TestSpecobjectImportExport.java | 23 +++++++++ 8 files changed, 177 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 94d7c2c3d..8ce506485 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,7 @@ Needs: impl, utest ### Code Style & Conventions - **Clean Code**: Meaningful names, small functiKons, single responsibility. +- **Type References**: Use plain class names with imports instead of fully qualified names such as `java.util.List`. - **Formatting**: Use the project's Eclipse formatter (`doc/itsallcode_formatter.xml`). - **Logging**: Use `java.util.logging`. Test config: `core/src/test/resources/logging.properties`. diff --git a/doc/spec/design.md b/doc/spec/design.md index 6f223a82c..c8da7d925 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -131,7 +131,7 @@ Text importers create zero-based UTF-16, start-inclusive and end-exclusive ranges for source ID occurrences and their represented components. Equal semantic IDs at distinct source occurrences remain separate values. -Needs: impl +Needs: impl, utest #### Coverage-tag Source Ranges `dsn~located-specification-item-id-tag-ranges~1` @@ -140,7 +140,7 @@ Coverage-tag importers locate source-backed declared and covered ID components. They leave component ranges absent when a tag generates the corresponding ID component. -Needs: impl +Needs: impl, utest #### SpecObject ID Occurrences `dsn~located-specification-item-id-specobject~1` @@ -148,7 +148,7 @@ Needs: impl The SpecObject importer emits located declared, covered, and dependency IDs without ranges because its XML event model does not expose character offsets. -Needs: impl +Needs: impl, utest ## Command Line Interpreter The command line interpreter (CLI) takes parameters given to OFT and parses them. It is responsible for making sense of the parameter contents and issuing help and error messages about the command line syntax. diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java index 3ee4d60a3..897d2cf73 100644 --- a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java @@ -13,8 +13,7 @@ import java.util.List; import java.util.stream.Stream; -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.input.InputFile; import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; @@ -94,6 +93,27 @@ void testImportsMultipleCoversDirectives() hasToString("req~login~1"), hasToString("req~security~1"))); } + // [utest->dsn~located-specification-item-id-text-ranges~1] + @Test + void testImportsLocatedScenarioAndCoversIds() + { + final SpecificationItem item = importText(""" + @tag @id:scn~login~1 + # Covers: req~login~1, req~security~2 + Scenario: Login + """).get(0); + + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is(range(0, 9, 20))), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(1, 10, 21))), + () -> assertThat(item.getLocatedCoveredIds().get(1).getRange(), is(range(1, 23, 37)))); + } + + private static SourceRange range(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + // [utest->dsn~gherkin.streaming-import~1] // [utest->dsn~gherkin.id-detection~1] // [utest->dsn~gherkin.needs-metadata-validation~1] 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 8edf8fbbc..d4bb05034 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 @@ -1,12 +1,18 @@ package org.itsallcode.openfasttrace.importer.markdown; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.is; 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 java.nio.file.Path; +import java.util.List; + +import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.*; +import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -64,6 +70,33 @@ void testIgnoresCoverageTagsOutsideStandaloneHtmlComments(final String line) assertImport("guide.md", line, emptyIterable()); } + // [utest->dsn~located-specification-item-id-text-ranges~1] + @Test + void testImportsLocatedDeclarationCoverageAndDependencyIds() + { + final SpecificationItem item = importText(""" + req~subject~1 + Covers: + * req~covered~2 + Depends: + * req~dependency~3 + """).get(0); + + assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))); + assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 2, 15))); + assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18))); + } + + private static List importText(final String source) + { + return ImportAssertions.runImporterOnText(Path.of("located.md"), source, importerFactory); + } + + private static SourceRange range(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + protected String formatTitle(final String title, final int level) { return "#".repeat(level) + " " + title; 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 f6a91f56a..7d352a555 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,12 +1,17 @@ package org.itsallcode.openfasttrace.importer.restructuredtext; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.is; 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 java.nio.file.Path; + +import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.*; +import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -74,6 +79,33 @@ void testIgnoresCoverageTagsInMultilineComments() """.formatted(FULL_COVERAGE_TAG), emptyIterable()); } + // [utest->dsn~located-specification-item-id-text-ranges~1] + @Test + void testImportsLocatedDeclarationCoverageAndDependencyIds() + { + final SpecificationItem item = importText(""" + req~subject~1 + Covers: + * req~covered~2 + Depends: + * req~dependency~3 + """).get(0); + + assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))); + assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 2, 15))); + assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18))); + } + + private static java.util.List importText(final String source) + { + return ImportAssertions.runImporterOnText(Path.of("located.rst"), source, importerFactory); + } + + private static SourceRange range(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + protected String formatTitle(final String title, final int level) { return title + "\n" + "=".repeat(title.length()); diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java index a5236ffca..b93bfe29a 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import java.io.BufferedReader; import java.io.StringReader; @@ -9,8 +10,7 @@ import java.util.List; import java.util.stream.Stream; -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.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; @@ -51,6 +51,25 @@ void importsLongTag(final int lineNumber, final String tag, final Listdsn~located-specification-item-id-tag-ranges~1] + @Test + void importsLocatedLongTagIds() + { + final SpecificationListBuilder listener = SpecificationListBuilder.create(); + new LongTagImportingLineConsumer(inputFile(), listener).readLine(3, + "😀 [impl~tag~1 -> dsn~covered~2" + "]"); + + final SpecificationItem item = listener.build().get(0); + + assertThat(item.getLocatedId().getRange(), is(range(2, 4, 14))); + assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 18, 31))); + } + + private static SourceRange range(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + private static SpecificationItem item(final String id, final int lineNumber, final List coveredIds, final List neededArtifactTypes) { final SpecificationItem.Builder builder = SpecificationItem.builder() diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java index b1f155466..b95edc9d2 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java @@ -2,6 +2,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertAll; import java.io.BufferedReader; import java.io.StringReader; @@ -9,21 +11,23 @@ import java.util.List; import java.util.stream.Stream; -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.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.api.importer.tag.config.PathConfig; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; // [utest->dsn~import.short-coverage-tag~1] -class TestShortTagImportingLineConsumer { +class TestShortTagImportingLineConsumer +{ private static final String FILE = "source.file"; - static Stream shortTagImportingTests() { + static Stream shortTagImportingTests() + { return Stream.of( testCase(3, "[[covered:2" + "]]", null, item("covered-3798966306", 3, "req~covered~2")), @@ -35,14 +39,16 @@ static Stream shortTagImportingTests() { } private static Arguments testCase(final int lineNumber, final String tag, final String coveredItemNamePrefix, - final SpecificationItem... expectedItems) { + final SpecificationItem... expectedItems) + { return Arguments.of(lineNumber, tag, coveredItemNamePrefix, List.of(expectedItems)); } @ParameterizedTest @MethodSource("shortTagImportingTests") void importsShortTag(final int lineNumber, final String tag, final String coveredItemNamePrefix, - final List expectedItems) { + final List expectedItems) + { final SpecificationListBuilder listener = SpecificationListBuilder.create(); final ShortTagImportingLineConsumer consumer = new ShortTagImportingLineConsumer( pathConfig(coveredItemNamePrefix), inputFile(), listener); @@ -52,7 +58,30 @@ void importsShortTag(final int lineNumber, final String tag, final String covere assertThat(listener.build(), equalTo(expectedItems)); } - private static PathConfig pathConfig(final String coveredItemNamePrefix) { + // [utest->dsn~located-specification-item-id-tag-ranges~1] + @Test + void importsLocatedShortTagCoveredId() + { + final SpecificationListBuilder listener = SpecificationListBuilder.create(); + new ShortTagImportingLineConsumer(pathConfig(null), inputFile(), listener).readLine(3, "😀 [[covered:2]]"); + + final LocatedSpecificationItemId coveredId = listener.build().get(0).getLocatedCoveredIds().get(0); + + assertAll( + () -> assertThat(coveredId.getId(), is(SpecificationItemId.parseId("req~covered~2"))), + () -> assertThat(coveredId.getRange(), is(range(2, 5, 14))), + () -> assertThat(coveredId.getArtifactTypeRange().isEmpty(), is(true)), + () -> assertThat(coveredId.getNameRange().orElseThrow(), is(range(2, 5, 12))), + () -> assertThat(coveredId.getRevisionRange().orElseThrow(), is(range(2, 13, 14)))); + } + + private static SourceRange range(final int line, final int start, final int end) + { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + + private static PathConfig pathConfig(final String coveredItemNamePrefix) + { return PathConfig.builder() .patternPathMatcher("glob:**") .coveredItemArtifactType("req") @@ -61,7 +90,8 @@ private static PathConfig pathConfig(final String coveredItemNamePrefix) { .build(); } - private static SpecificationItem item(final String tagItemName, final int lineNumber, final String coveredId) { + private static SpecificationItem item(final String tagItemName, final int lineNumber, final String coveredId) + { return SpecificationItem.builder() .id(SpecificationItemId.createId("utest", tagItemName)) .location(FILE, lineNumber) @@ -69,7 +99,8 @@ private static SpecificationItem item(final String tagItemName, final int lineNu .build(); } - private static InputFile inputFile() { + private static InputFile inputFile() + { return StreamInput.forReader(Paths.get(FILE), new BufferedReader(new StringReader(""))); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImportExport.java b/product/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImportExport.java index f22f411f0..982322fdf 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImportExport.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImportExport.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.specobject; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.itsallcode.openfasttrace.testutil.core.TraceAssertions.assertTraceContainsDefectIds; import static org.itsallcode.openfasttrace.testutil.core.TraceAssertions.assertTraceSize; import static org.itsallcode.openfasttrace.testutil.core.TraceAssertions.getItemFromTraceForId; @@ -21,6 +22,28 @@ class TestSpecobjectImportExport { + // [utest->dsn~located-specification-item-id-specobject~1] + @Test + void testImportsLocatedIdsWithoutSourceRanges() + { + final SpecificationItem item = parse(""" + + + example1 + feat:covered2 + req:dependency, v3 + + """).get(0); + + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is((SourceRange) null)), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is((SourceRange) null)), + () -> assertThat(item.getLocatedDependOnIds().get(0).getRange(), is((SourceRange) null)), + () -> assertThat(item.getLocatedId().getArtifactTypeRange().isEmpty(), is(true)), + () -> assertThat(item.getLocatedCoveredIds().get(0).getNameRange().isEmpty(), is(true)), + () -> assertThat(item.getLocatedDependOnIds().get(0).getRevisionRange().isEmpty(), is(true))); + } + @Test void testTraceContent() { From a409ef91f92e41470a89aeab7d0cfabad58354d2 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 6 Aug 2026 16:37:44 +0200 Subject: [PATCH 03/16] Adapt unit tests --- .../specobject/TestSpecobjectImporter.java | 39 +++++----- .../tag/common/TestCoverageTagParser.java | 48 ++++++++++-- .../TestLongTagImportingLineConsumer.java | 73 ++++++++++++++----- 3 files changed, 120 insertions(+), 40 deletions(-) diff --git a/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporter.java b/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporter.java index 5ce21cee0..877f3d973 100644 --- a/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporter.java +++ b/importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporter.java @@ -85,8 +85,8 @@ void testImportSeesTheCorrectId(final String input, final String expectedId) { final ImportEventListener listenerMock = importFromString(input); verify(listenerMock).beginSpecificationItem(); + verify(listenerMock).setId(locatedId(expectedId)); verify(listenerMock).setLocation(TestSpecobjectImporter.STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId(expectedId)); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -103,6 +103,11 @@ private ImportEventListener importFromString(final String text) return listenerMock; } + private static LocatedSpecificationItemId locatedId(final String id) + { + return LocatedSpecificationItemId.builder().id(SpecificationItemId.parseId(id)).build(); + } + @Test void testImportOfComplexSpecObject() { @@ -122,13 +127,13 @@ void testImportOfComplexSpecObject() """); verify(listenerMock).beginSpecificationItem(); - verify(listenerMock).setLocation(STANDARD_LOCATION); verify(listenerMock).setStatus(ItemStatus.DRAFT); - verify(listenerMock).setId(SpecificationItemId.parseId("req~complex~2")); + verify(listenerMock).setId(locatedId("req~complex~2")); verify(listenerMock).setTitle("my short description"); verify(listenerMock).appendDescription("multiline description\none more line"); verify(listenerMock).appendRationale("multiline rationale\nand another line"); verify(listenerMock).appendComment("multiline comment\nyet another line"); + verify(listenerMock).setLocation(STANDARD_LOCATION); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -145,9 +150,9 @@ void testImportOnlyShortDescription() """); verify(listenerMock).beginSpecificationItem(); - verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("req~complex~2")); + verify(listenerMock).setId(locatedId("req~complex~2")); verify(listenerMock).setTitle("My item title"); + verify(listenerMock).setLocation(STANDARD_LOCATION); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -163,8 +168,8 @@ void testStripSuperfluousArtifactPrefixFromName() """); verify(listenerMock).beginSpecificationItem(); + verify(listenerMock).setId(locatedId("impl~strip_duplicate_prefix~0")); verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("impl~strip_duplicate_prefix~0")); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -183,8 +188,8 @@ void testTakeOverLocationFromImportedFile() + " \n" // + ""); verify(listenerMock).beginSpecificationItem(); + verify(listenerMock).setId(locatedId("utest~takeOverLocation~99999999")); verify(listenerMock).setLocation(Location.create(expectedFileName, expectedLine)); - verify(listenerMock).setId(SpecificationItemId.parseId("utest~takeOverLocation~99999999")); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -204,10 +209,10 @@ void testImportWithTags() """); verify(listenerMock).beginSpecificationItem(); - verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("itest~with-tags~1")); + verify(listenerMock).setId(locatedId("itest~with-tags~1")); verify(listenerMock).addTag("tag 1"); verify(listenerMock).addTag("tag 2"); + verify(listenerMock).setLocation(STANDARD_LOCATION); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -227,10 +232,10 @@ void testImportWithNeedsCoverage() """); verify(listenerMock).beginSpecificationItem(); - verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("req~with-needs-coverage~1")); + verify(listenerMock).setId(locatedId("req~with-needs-coverage~1")); verify(listenerMock).addNeededArtifactType("impl"); verify(listenerMock).addNeededArtifactType("utest"); + verify(listenerMock).setLocation(STANDARD_LOCATION); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -250,10 +255,10 @@ void testImportWithDependencies() """); verify(listenerMock).beginSpecificationItem(); + verify(listenerMock).addDependsOnId(locatedId("req~dep-a~1")); + verify(listenerMock).addDependsOnId(locatedId("req~dep-b~2")); + verify(listenerMock).setId(locatedId("req~with-dependencies~1")); verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("req~with-dependencies~1")); - verify(listenerMock).addDependsOnId(SpecificationItemId.parseId("req~dep-a~1")); - verify(listenerMock).addDependsOnId(SpecificationItemId.parseId("req~dep-b~2")); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } @@ -279,10 +284,10 @@ void testImportProvidesCoverage() """); verify(listenerMock).beginSpecificationItem(); + verify(listenerMock).addCoveredId(locatedId("feat~provides-a~1")); + verify(listenerMock).addCoveredId(locatedId("feat~provides-b~2")); + verify(listenerMock).setId(locatedId("req~with-fulfilled-by~1")); verify(listenerMock).setLocation(STANDARD_LOCATION); - verify(listenerMock).setId(SpecificationItemId.parseId("req~with-fulfilled-by~1")); - verify(listenerMock).addCoveredId(SpecificationItemId.parseId("feat~provides-a~1")); - verify(listenerMock).addCoveredId(SpecificationItemId.parseId("feat~provides-b~2")); verify(listenerMock).endSpecificationItem(); verifyNoMoreInteractions(listenerMock); } diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java index e0086e9b9..54f6a2e10 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java @@ -8,6 +8,9 @@ import java.nio.file.Paths; import java.util.List; +import org.itsallcode.openfasttrace.api.core.LocatedSpecificationItemId; +import org.itsallcode.openfasttrace.api.core.SourcePosition; +import org.itsallcode.openfasttrace.api.core.SourceRange; import org.itsallcode.openfasttrace.api.core.SpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.importer.SpecificationListBuilder; @@ -29,7 +32,7 @@ void importsFullTag() { parser.readLine(3, coverageTag); assertThat(listener.build(), equalTo(List.of(item(SpecificationItemId.parseId("impl~name~1"), 3, - List.of("dsn~covered~2"), List.of("utest"))))); + coverageTag, List.of("dsn~covered~2"), List.of("utest"))))); } @Test @@ -42,7 +45,7 @@ void importsConfiguredShortTag() { assertThat(listener.build(), equalTo(List.of(item(SpecificationItemId.createId("utest", "prefix.covered-1743877134"), - 2, List.of("req~prefix.covered~3"), List.of())))); + 2, "[[covered:3]]", List.of("req~prefix.covered~3"), List.of())))); } private static PathConfig pathConfig() { @@ -55,15 +58,50 @@ private static PathConfig pathConfig() { } private static SpecificationItem item(final SpecificationItemId id, final int lineNumber, - final List coveredIds, final List neededArtifactTypes) { + final String tag, final List coveredIds, final List neededArtifactTypes) { final SpecificationItem.Builder builder = SpecificationItem.builder() - .id(id) .location(FILE, lineNumber); - coveredIds.stream().map(SpecificationItemId::parseId).forEach(builder::addCoveredId); + if (tag.contains(id.toString())) { + builder.id(locatedId(lineNumber, tag.indexOf(id.toString()), id)); + } else { + builder.id(id); + } + coveredIds.stream().map(SpecificationItemId::parseId) + .map(coveredId -> locatedCoveredId(lineNumber, tag, coveredId)) + .forEach(builder::addCoveredId); neededArtifactTypes.forEach(builder::addNeedsArtifactType); return builder.build(); } + private static LocatedSpecificationItemId locatedCoveredId(final int lineNumber, final String tag, + final SpecificationItemId id) { + if (!tag.startsWith("[[")) { + return locatedId(lineNumber, tag.indexOf(id.toString()), id); + } + final int nameStart = 2; + final int revisionStart = tag.indexOf(':') + 1; + final int end = tag.indexOf("]]", revisionStart); + return LocatedSpecificationItemId.builder().id(id) + .range(range(lineNumber - 1, nameStart, end)) + .nameRange(range(lineNumber - 1, nameStart, revisionStart - 1)) + .revisionRange(range(lineNumber - 1, revisionStart, end)).build(); + } + + private static LocatedSpecificationItemId locatedId(final int lineNumber, final int start, + final SpecificationItemId id) { + final String text = id.toString(); + final int typeEnd = text.indexOf('~'); + final int revisionStart = text.lastIndexOf('~') + 1; + return LocatedSpecificationItemId.builder().id(id).range(range(lineNumber - 1, start, start + text.length())) + .artifactTypeRange(range(lineNumber - 1, start, start + typeEnd)) + .nameRange(range(lineNumber - 1, start + typeEnd + 1, start + revisionStart - 1)) + .revisionRange(range(lineNumber - 1, start + revisionStart, start + text.length())).build(); + } + + private static SourceRange range(final int line, final int start, final int end) { + return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); + } + private static InputFile inputFile() { return StreamInput.forReader(Paths.get(FILE), new BufferedReader(new StringReader(""))); } diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java index b93bfe29a..0e8dba563 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java @@ -14,37 +14,46 @@ import org.itsallcode.openfasttrace.api.importer.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class TestLongTagImportingLineConsumer { +class TestLongTagImportingLineConsumer +{ private static final String FILE = "source.file"; - static Stream tagImportingTests() { + static Stream tagImportingTests() + { + final String namedTag = "[impl~tag~1 -> dsn~first~2, dsn~second~3 >> utest, itest" + "]"; + final String generatedNameTag = "[impl -> dsn~covered~2" + "]"; + final String readableNamesTag = "[impl -> dsn~first~2, dsn~second~3 >> utest" + "]"; return Stream.of( // [utest->dsn~import.full-coverage-tag-with-name-and-revision~1] // [utest->dsn~import.full-coverage-tag-with-needed-coverage~1] // [utest->dsn~import.full-coverage-tag-multiple-needed-coverage~1] - testCase(3, "[impl~tag~1 -> dsn~first~2, dsn~second~3 >> utest, itest" + "]", - item("impl~tag~1", 3, List.of("dsn~first~2", "dsn~second~3"), + testCase(3, namedTag, + item("impl~tag~1", 3, namedTag, List.of("dsn~first~2", "dsn~second~3"), List.of("utest", "itest"))), // [utest->dsn~import.full-coverage-tag~1] - testCase(4, "[impl -> dsn~covered~2" + "]", - item("impl~covered-3014110766~0", 4, List.of("dsn~covered~2"), List.of())), + testCase(4, generatedNameTag, + item("impl~covered-3014110766~0", 4, generatedNameTag, List.of("dsn~covered~2"), + List.of())), // [utest->dsn~import.full-coverage-tag-with-needed-coverage-readable-names~1] - testCase(5, "[impl -> dsn~first~2, dsn~second~3 >> utest" + "]", - item("impl~first~0", 5, List.of("dsn~first~2"), List.of("utest")), - item("impl~second~0", 5, List.of("dsn~second~3"), List.of("utest")))); + testCase(5, readableNamesTag, + item("impl~first~0", 5, readableNamesTag, List.of("dsn~first~2"), List.of("utest")), + item("impl~second~0", 5, readableNamesTag, List.of("dsn~second~3"), List.of("utest")))); } - static Arguments testCase(final int lineNumber, final String tag, final SpecificationItem... expectedItems) { + static Arguments testCase(final int lineNumber, final String tag, final SpecificationItem... expectedItems) + { return Arguments.of(lineNumber, tag, List.of(expectedItems)); } @ParameterizedTest @MethodSource("tagImportingTests") - void importsLongTag(final int lineNumber, final String tag, final List expectedItems) { + void importsLongTag(final int lineNumber, final String tag, final List expectedItems) + { final SpecificationListBuilder listener = SpecificationListBuilder.create(); final LongTagImportingLineConsumer consumer = new LongTagImportingLineConsumer(inputFile(), listener); consumer.readLine(lineNumber, tag); @@ -70,17 +79,45 @@ private static SourceRange range(final int line, final int start, final int end) return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); } - private static SpecificationItem item(final String id, final int lineNumber, final List coveredIds, - final List neededArtifactTypes) { - final SpecificationItem.Builder builder = SpecificationItem.builder() - .id(SpecificationItemId.parseId(id)) - .location(FILE, lineNumber); - coveredIds.stream().map(SpecificationItemId::parseId).forEach(builder::addCoveredId); + private static SpecificationItem item(final String id, final int lineNumber, final String tag, + final List coveredIds, + final List neededArtifactTypes) + { + final SpecificationItemId specificationItemId = SpecificationItemId.parseId(id); + final SpecificationItem.Builder builder = SpecificationItem.builder().location(FILE, lineNumber); + if (tag.contains(id)) + { + builder.id(locatedId(lineNumber, tag.indexOf(id), specificationItemId)); + } + else + { + builder.id(specificationItemId); + } + int searchStart = 0; + for (final String coveredId : coveredIds) + { + final int start = tag.indexOf(coveredId, searchStart); + searchStart = start + coveredId.length(); + builder.addCoveredId(locatedId(lineNumber, start, SpecificationItemId.parseId(coveredId))); + } neededArtifactTypes.forEach(builder::addNeedsArtifactType); return builder.build(); } - private static InputFile inputFile() { + private static LocatedSpecificationItemId locatedId(final int lineNumber, final int start, + final SpecificationItemId id) + { + final String text = id.toString(); + final int typeEnd = text.indexOf('~'); + final int revisionStart = text.lastIndexOf('~') + 1; + return LocatedSpecificationItemId.builder().id(id).range(range(lineNumber - 1, start, start + text.length())) + .artifactTypeRange(range(lineNumber - 1, start, start + typeEnd)) + .nameRange(range(lineNumber - 1, start + typeEnd + 1, start + revisionStart - 1)) + .revisionRange(range(lineNumber - 1, start + revisionStart, start + text.length())).build(); + } + + private static InputFile inputFile() + { return StreamInput.forReader(Paths.get(FILE), new BufferedReader(new StringReader(""))); } } From 15e308f6e55524b58f5cef3bf7c71cbee5cf97f0 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 06:37:52 +0200 Subject: [PATCH 04/16] Revert location for short tags --- doc/spec/design.md | 7 +- doc/spec/system_requirements.md | 3 +- .../common/ShortTagImportingLineConsumer.java | 48 +++++++------- .../tag/common/TestCoverageTagParser.java | 64 +++++++++++-------- .../TestShortTagImportingLineConsumer.java | 28 +------- 5 files changed, 67 insertions(+), 83 deletions(-) diff --git a/doc/spec/design.md b/doc/spec/design.md index c8da7d925..3010458b2 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -136,9 +136,10 @@ Needs: impl, utest #### Coverage-tag Source Ranges `dsn~located-specification-item-id-tag-ranges~1` -Coverage-tag importers locate source-backed declared and covered ID components. -They leave component ranges absent when a tag generates the corresponding ID -component. +The full coverage-tag importer locates source-backed declared and covered ID +components. It leaves component ranges absent when a tag generates the +corresponding ID component. The short coverage-tag importer creates item +IDs without source ranges. Needs: impl, utest diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index d35591171..c47f729f2 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -151,7 +151,8 @@ OFT shall retain the individual source occurrence. For source text, positions shall be zero-based UTF-16 offsets with start-inclusive, end-exclusive ranges. Where an ID component is generated or its source is unavailable, its component range shall be absent. Equal semantic IDs at distinct source occurrences shall -remain distinct occurrences. +remain distinct occurrences. Short coverage tags shall not generate source +ranges for their generated or covered IDs. Needs: dsn diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java index c89fd5a62..d3580f070 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/ShortTagImportingLineConsumer.java @@ -3,14 +3,16 @@ import java.util.logging.Logger; import java.util.regex.Matcher; -import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; 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.api.importer.tag.config.PathConfig; // [impl->dsn~import.short-coverage-tag~1] -class ShortTagImportingLineConsumer extends AbstractRegexLineConsumer { +class ShortTagImportingLineConsumer extends AbstractRegexLineConsumer +{ private static final Logger LOG = Logger.getLogger(ShortTagImportingLineConsumer.class.getName()); private static final String TAG_PREFIX = "\\[\\["; @@ -26,7 +28,8 @@ class ShortTagImportingLineConsumer extends AbstractRegexLineConsumer { private final InputFile file; ShortTagImportingLineConsumer(final PathConfig pathConfig, final InputFile file, - final ImportEventListener listener) { + final ImportEventListener listener) + { super(TAG_REGEX); this.pathConfig = pathConfig; this.file = file; @@ -34,7 +37,8 @@ class ShortTagImportingLineConsumer extends AbstractRegexLineConsumer { } @Override - void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount) { + void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount) + { final String coveredItemName = matcher.group(1); final String coveredItemRevision = matcher.group(2); final SpecificationItemId coveredId = createCoveredItem(coveredItemName, coveredItemRevision); @@ -45,48 +49,40 @@ void processMatch(final Matcher matcher, final int lineNumber, final int lineMat LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + tagItemId + "' covering id '" + coveredId + "'"); this.listener.addSpecificationItem(SpecificationItem.builder() - .id(LocatedSpecificationItemId.builder().id(tagItemId).build()) + .id(tagItemId) .location(this.file.toString(), lineNumber) - .addCoveredId(locatedCoveredId(matcher, lineNumber, coveredId)) + .addCoveredId(coveredId) .build()); } - private static LocatedSpecificationItemId locatedCoveredId(final Matcher matcher, final int lineNumber, - final SpecificationItemId id) + private SpecificationItemId createCoveredItem(final String name, final String revision) { - // [impl->dsn~located-specification-item-id-tag-ranges~1] - final int start = matcher.start(1); - final int end = matcher.end(2); - final SourceRange range = new SourceRange(new SourcePosition(lineNumber - 1, start), - new SourcePosition(lineNumber - 1, end)); - return LocatedSpecificationItemId.builder().id(id).range(range) - .nameRange(new SourceRange(new SourcePosition(lineNumber - 1, start), - new SourcePosition(lineNumber - 1, matcher.end(1)))) - .revisionRange(new SourceRange(new SourcePosition(lineNumber - 1, matcher.start(2)), - new SourcePosition(lineNumber - 1, end))).build(); - } - - private SpecificationItemId createCoveredItem(final String name, final String revision) { final int parsedRevision = parseRevision(name, revision); final String nameWithPrefix = getCoveredItemNamePrefix() + name; return SpecificationItemId.createId(this.pathConfig.getCoveredItemArtifactType(), nameWithPrefix, parsedRevision); } - private static int parseRevision(final String name, final String revision) { - try { + private static int parseRevision(final String name, final String revision) + { + try + { return Integer.parseInt(revision); - } catch (final NumberFormatException exception) { + } + catch (final NumberFormatException exception) + { throw new ImporterException("Error parsing revision '" + revision + "' for item '" + name + "'.", exception); } } - private String getCoveredItemNamePrefix() { + private String getCoveredItemNamePrefix() + { return this.pathConfig.getCoveredItemNamePrefix() != null ? this.pathConfig.getCoveredItemNamePrefix() : ""; } - private String generateName(final SpecificationItemId coveredId, final int lineNumber, final int counter) { + private String generateName(final SpecificationItemId coveredId, final int lineNumber, final int counter) + { final String uniqueName = this.file.toString() + lineNumber + counter + coveredId; final String checksum = Long.toString(ChecksumCalculator.calculateCrc32(uniqueName)); return coveredId.getName() + "-" + checksum; diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java index 54f6a2e10..ab7a90337 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestCoverageTagParser.java @@ -8,11 +8,7 @@ import java.nio.file.Paths; import java.util.List; -import org.itsallcode.openfasttrace.api.core.LocatedSpecificationItemId; -import org.itsallcode.openfasttrace.api.core.SourcePosition; -import org.itsallcode.openfasttrace.api.core.SourceRange; -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.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.api.importer.tag.config.PathConfig; @@ -20,11 +16,13 @@ import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; import org.junit.jupiter.api.Test; -class TestCoverageTagParser { +class TestCoverageTagParser +{ private static final String FILE = "source.file"; @Test - void importsFullTag() { + void importsFullTag() + { final SpecificationListBuilder listener = SpecificationListBuilder.create(); final LineConsumer parser = CoverageTagParser.create(null, inputFile(), listener); @@ -36,7 +34,8 @@ void importsFullTag() { } @Test - void importsConfiguredShortTag() { + void importsConfiguredShortTag() + { final PathConfig config = pathConfig(); final SpecificationListBuilder listener = SpecificationListBuilder.create(); final LineConsumer parser = CoverageTagParser.create(config, inputFile(), listener); @@ -45,10 +44,11 @@ void importsConfiguredShortTag() { assertThat(listener.build(), equalTo(List.of(item(SpecificationItemId.createId("utest", "prefix.covered-1743877134"), - 2, "[[covered:3]]", List.of("req~prefix.covered~3"), List.of())))); + 2, List.of("req~prefix.covered~3"), List.of())))); } - private static PathConfig pathConfig() { + private static PathConfig pathConfig() + { return PathConfig.builder() .patternPathMatcher("glob:**") .coveredItemArtifactType("req") @@ -58,12 +58,27 @@ private static PathConfig pathConfig() { } private static SpecificationItem item(final SpecificationItemId id, final int lineNumber, - final String tag, final List coveredIds, final List neededArtifactTypes) { + final List coveredIds, final List neededArtifactTypes) + { final SpecificationItem.Builder builder = SpecificationItem.builder() + .id(id) .location(FILE, lineNumber); - if (tag.contains(id.toString())) { + coveredIds.stream().map(SpecificationItemId::parseId).forEach(builder::addCoveredId); + neededArtifactTypes.forEach(builder::addNeedsArtifactType); + return builder.build(); + } + + private static SpecificationItem item(final SpecificationItemId id, final int lineNumber, + final String tag, final List coveredIds, final List neededArtifactTypes) + { + final SpecificationItem.Builder builder = SpecificationItem.builder() + .location(FILE, lineNumber); + if (tag.contains(id.toString())) + { builder.id(locatedId(lineNumber, tag.indexOf(id.toString()), id)); - } else { + } + else + { builder.id(id); } coveredIds.stream().map(SpecificationItemId::parseId) @@ -74,21 +89,14 @@ private static SpecificationItem item(final SpecificationItemId id, final int li } private static LocatedSpecificationItemId locatedCoveredId(final int lineNumber, final String tag, - final SpecificationItemId id) { - if (!tag.startsWith("[[")) { - return locatedId(lineNumber, tag.indexOf(id.toString()), id); - } - final int nameStart = 2; - final int revisionStart = tag.indexOf(':') + 1; - final int end = tag.indexOf("]]", revisionStart); - return LocatedSpecificationItemId.builder().id(id) - .range(range(lineNumber - 1, nameStart, end)) - .nameRange(range(lineNumber - 1, nameStart, revisionStart - 1)) - .revisionRange(range(lineNumber - 1, revisionStart, end)).build(); + final SpecificationItemId id) + { + return locatedId(lineNumber, tag.indexOf(id.toString()), id); } private static LocatedSpecificationItemId locatedId(final int lineNumber, final int start, - final SpecificationItemId id) { + final SpecificationItemId id) + { final String text = id.toString(); final int typeEnd = text.indexOf('~'); final int revisionStart = text.lastIndexOf('~') + 1; @@ -98,11 +106,13 @@ private static LocatedSpecificationItemId locatedId(final int lineNumber, final .revisionRange(range(lineNumber - 1, start + revisionStart, start + text.length())).build(); } - private static SourceRange range(final int line, final int start, final int end) { + private static SourceRange range(final int line, final int start, final int end) + { return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); } - private static InputFile inputFile() { + private static InputFile inputFile() + { return StreamInput.forReader(Paths.get(FILE), new BufferedReader(new StringReader(""))); } } diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java index b95edc9d2..0de90fdfa 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestShortTagImportingLineConsumer.java @@ -2,8 +2,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertAll; import java.io.BufferedReader; import java.io.StringReader; @@ -11,12 +9,12 @@ import java.util.List; import java.util.stream.Stream; -import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.importer.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.api.importer.tag.config.PathConfig; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -58,28 +56,6 @@ void importsShortTag(final int lineNumber, final String tag, final String covere assertThat(listener.build(), equalTo(expectedItems)); } - // [utest->dsn~located-specification-item-id-tag-ranges~1] - @Test - void importsLocatedShortTagCoveredId() - { - final SpecificationListBuilder listener = SpecificationListBuilder.create(); - new ShortTagImportingLineConsumer(pathConfig(null), inputFile(), listener).readLine(3, "😀 [[covered:2]]"); - - final LocatedSpecificationItemId coveredId = listener.build().get(0).getLocatedCoveredIds().get(0); - - assertAll( - () -> assertThat(coveredId.getId(), is(SpecificationItemId.parseId("req~covered~2"))), - () -> assertThat(coveredId.getRange(), is(range(2, 5, 14))), - () -> assertThat(coveredId.getArtifactTypeRange().isEmpty(), is(true)), - () -> assertThat(coveredId.getNameRange().orElseThrow(), is(range(2, 5, 12))), - () -> assertThat(coveredId.getRevisionRange().orElseThrow(), is(range(2, 13, 14)))); - } - - private static SourceRange range(final int line, final int start, final int end) - { - return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); - } - private static PathConfig pathConfig(final String coveredItemNamePrefix) { return PathConfig.builder() From 92a45b74bf54f818c91f3c78cade92c060655b67 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 07:04:25 +0200 Subject: [PATCH 05/16] Fix unit tests --- .../importer/gherkin/GherkinImporterTest.java | 2 +- .../importer/tag/TestTagImporter.java | 21 ++++++++++++++- .../tag/TestTagImporterWithConfig.java | 26 ++++++++++++++----- ...AbstractLightWeightMarkupImporterTest.java | 23 +++++++++++++++- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java index 897d2cf73..5c46b47d6 100644 --- a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java @@ -335,7 +335,7 @@ void testImportsCommentCoverageTagsWhileScenarioIsOpen() final InOrder events = inOrder(listener); events.verify(listener).beginSpecificationItem(); - events.verify(listener).setId(SpecificationItemId.parseId("scn~ordinary~1")); + events.verify(listener).setId(any(LocatedSpecificationItemId.class)); events.verify(listener).addSpecificationItem(any(SpecificationItem.class)); events.verify(listener).endSpecificationItem(); } diff --git a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java index c41c3413e..7eef5a0a7 100644 --- a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java +++ b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java @@ -249,10 +249,29 @@ void testTagImporter(final String content, final List expecte assertThat(result, hasSize(expectedItems.size())); if (!expectedItems.isEmpty()) { - assertThat(result, AutoMatcher.contains(expectedItems.toArray(new SpecificationItem[0]))); + assertThat(result.stream().map(TestTagImporter::withoutIdLocations).toList(), + AutoMatcher.contains(expectedItems.toArray(new SpecificationItem[0]))); } } + private static SpecificationItem withoutIdLocations(final SpecificationItem item) + { + final SpecificationItem.Builder builder = SpecificationItem.builder() + .id(item.getId()) + .title(item.getTitle()) + .description(item.getDescription()) + .rationale(item.getRationale()) + .comment(item.getComment()) + .status(item.getStatus()) + .location(item.getLocation()) + .forwards(item.isForwarding()); + item.getCoveredIds().forEach(builder::addCoveredId); + item.getDependOnIds().forEach(builder::addDependOnId); + item.getNeedsArtifactTypes().forEach(builder::addNeedsArtifactType); + item.getTags().forEach(builder::addTag); + return builder.build(); + } + private List runImporter(final String content) { final SpecificationListBuilder builder = SpecificationListBuilder.create(); diff --git a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterWithConfig.java b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterWithConfig.java index 103a9a394..e3da9043f 100644 --- a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterWithConfig.java +++ b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterWithConfig.java @@ -8,8 +8,7 @@ import java.nio.file.Path; import java.nio.file.Paths; -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.ImporterException; import org.itsallcode.openfasttrace.api.importer.input.InputFile; @@ -67,14 +66,12 @@ void testFileWithoutMatchingTag() @Test void testFileWithNewTagFormatAlsoSupported() { - final String itemName = "coveredtype~coveredname~1"; // do not inline to - // avoid error in - // self-trace + final String itemName = "coveredtype~coveredname~1"; // do not inline to avoid error in self-trace runImport("[type->" + itemName + "]"); verify(this.listenerMock).addSpecificationItem(SpecificationItem.builder() .id(SpecificationItemId.createId("type", "coveredname" + "-3264583751", 0)) .location(FILE.toString(), 1) - .addCoveredId(SpecificationItemId.parseId(itemName)) + .addCoveredId(locatedId(SpecificationItemId.parseId(itemName), 7, 18, 19, 30, 31, 32)) .build()); } @@ -150,6 +147,23 @@ private void verifyTag(final int lineNumber, final SpecificationItemId coveredId .build()); } + private static LocatedSpecificationItemId locatedId(final SpecificationItemId id, final int start, + final int artifactTypeEnd, final int nameStart, final int nameEnd, final int revisionStart, + final int end) + { + return LocatedSpecificationItemId.builder().id(id) + .range(range(start, end)) + .artifactTypeRange(range(start, artifactTypeEnd)) + .nameRange(range(nameStart, nameEnd)) + .revisionRange(range(revisionStart, end)) + .build(); + } + + private static SourceRange range(final int start, final int end) + { + return new SourceRange(new SourcePosition(0, start), new SourcePosition(0, end)); + } + private void runImport(final String content) { final InputFile file = StreamInput.forReader(FILE, 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 cca11c292..cd473f8d4 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 @@ -98,7 +98,28 @@ protected void assertImport(final String path, final String input, protected void assertImport(final Path path, final String input, final Matcher> matcher) { - ImportAssertions.assertImportWithFactory(path, processTextInput(input), matcher, getImporterFactory()); + final List importedItems = ImportAssertions.runImporterOnText(path, + processTextInput(input), getImporterFactory()); + assertThat(importedItems.stream().map(AbstractLightWeightMarkupImporterTest::withoutIdLocations) + .toList(), matcher); + } + + private static SpecificationItem withoutIdLocations(final SpecificationItem item) + { + final SpecificationItem.Builder builder = SpecificationItem.builder() + .id(item.getId()) + .title(item.getTitle()) + .description(item.getDescription()) + .rationale(item.getRationale()) + .comment(item.getComment()) + .status(item.getStatus()) + .location(item.getLocation()) + .forwards(item.isForwarding()); + item.getCoveredIds().forEach(builder::addCoveredId); + item.getDependOnIds().forEach(builder::addDependOnId); + item.getNeedsArtifactTypes().forEach(builder::addNeedsArtifactType); + item.getTags().forEach(builder::addTag); + return builder.build(); } private String processTextInput(final String input) From 4ec577e1a3601fa5f645028fd00d6b954135dd05 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 07:30:29 +0200 Subject: [PATCH 06/16] Add toBuilder() method for spec items --- .../api/core/SpecificationItem.java | 50 +++++++++++++++++++ .../api/core/TestSpecificationItem.java | 50 +++++++++++++++++++ .../importer/tag/TestTagImporter.java | 21 +------- .../testutil/core/ItemBuilderFactory.java | 14 ++++++ ...AbstractLightWeightMarkupImporterTest.java | 21 +------- 5 files changed, 118 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java index dba11fbc1..755a6a654 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItem.java @@ -304,6 +304,28 @@ public static Builder builder() return new Builder(); } + /** + * Create a builder pre-populated with this item's values. + * + * @return builder initialized from this item + */ + public Builder toBuilder() + { + final Builder builder = builder().id(this.id) + .title(this.title) + .description(this.description) + .rationale(this.rationale) + .comment(this.comment) + .status(this.status) + .location(this.location) + .forwards(this.forwards); + this.coveredIds.forEach(builder::addCoveredId); + this.dependOnIds.forEach(builder::addDependOnId); + this.needsArtifactTypes.forEach(builder::addNeedsArtifactType); + this.tags.forEach(builder::addTag); + return builder; + } + /** * Builder for objects of type {@link SpecificationItem} */ @@ -473,6 +495,20 @@ public Builder addCoveredId(final SpecificationItemId coveredId) return this.addCoveredId(locatedId(coveredId)); } + /** + * Replace the IDs of specification items covered by the item to build. + * + * @param coveredIds + * the covered IDs + * @return this builder instance + */ + public Builder coveredIds(final Collection coveredIds) + { + this.coveredIds.clear(); + coveredIds.forEach(this::addCoveredId); + return this; + } + /** * Add the ID of a specification item covered by the item to build * @@ -516,6 +552,20 @@ public Builder addDependOnId(final LocatedSpecificationItemId dependOnId) return this; } + /** + * Replace the IDs of specification items the item to build depends on. + * + * @param dependOnIds + * the dependency IDs + * @return this builder instance + */ + public Builder dependOnIds(final Collection dependOnIds) + { + this.dependOnIds.clear(); + dependOnIds.forEach(this::addDependOnId); + return this; + } + /** * Add the ID of a specification item the item to be build depends on * diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java index e50a7beca..9d3bc9889 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java @@ -3,9 +3,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; + import org.junit.jupiter.api.Test; import nl.jqno.equalsverifier.EqualsVerifier; @@ -42,6 +45,53 @@ void testPreservesCompatibilityForUnlocatedIds() () -> assertThat(item.getDependOnIds(), contains(DEPEND_ON_ID))); } + // [utest->dsn~specification-item~3] + @Test + void testToBuilderPreservesAllValues() + { + final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)).title("Title") + .description("Description").rationale("Rationale").comment("Comment").status(ItemStatus.DRAFT) + .location(Location.create("file.md", 7)).addCoveredId(locatedId(COVERED_ID)) + .addDependOnId(locatedId(DEPEND_ON_ID)).addNeedsArtifactType("impl").addTag("important") + .forwards(true).build(); + + assertThat(item.toBuilder().build(), equalTo(item)); + } + + // [utest->dsn~specification-item~3] + @Test + void testToBuilderDoesNotModifyOriginalItem() + { + final SpecificationItem item = SpecificationItem.builder().id(ID).addCoveredId(COVERED_ID).build(); + + final SpecificationItem copy = item.toBuilder().addCoveredId(DEPEND_ON_ID).build(); + + assertAll( + () -> assertThat(item.getCoveredIds(), contains(COVERED_ID)), + () -> assertThat(copy, not(equalTo(item))), + () -> assertThat(copy.getCoveredIds(), contains(COVERED_ID, DEPEND_ON_ID))); + } + + // [utest->dsn~specification-item~3] + @Test + void testBuilderReplacesCoveredIds() + { + final SpecificationItem item = SpecificationItem.builder().id(ID).addCoveredId(DEPEND_ON_ID) + .coveredIds(List.of(COVERED_ID)).build(); + + assertThat(item.getCoveredIds(), contains(COVERED_ID)); + } + + // [utest->dsn~specification-item~3] + @Test + void testBuilderReplacesDependencyIds() + { + final SpecificationItem item = SpecificationItem.builder().id(ID).addDependOnId(COVERED_ID) + .dependOnIds(List.of(DEPEND_ON_ID)).build(); + + assertThat(item.getDependOnIds(), contains(DEPEND_ON_ID)); + } + // [utest->dsn~located-specification-item-id-storage~1] @Test void testLocatedCoveredIdsAreImmutable() diff --git a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java index 7eef5a0a7..3cff01811 100644 --- a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java +++ b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java @@ -18,6 +18,7 @@ import org.itsallcode.openfasttrace.api.importer.ImporterContext; import org.itsallcode.openfasttrace.api.importer.SpecificationListBuilder; import org.itsallcode.openfasttrace.api.importer.input.InputFile; +import org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -249,29 +250,11 @@ void testTagImporter(final String content, final List expecte assertThat(result, hasSize(expectedItems.size())); if (!expectedItems.isEmpty()) { - assertThat(result.stream().map(TestTagImporter::withoutIdLocations).toList(), + assertThat(result.stream().map(ItemBuilderFactory::withoutIdLocations).toList(), AutoMatcher.contains(expectedItems.toArray(new SpecificationItem[0]))); } } - private static SpecificationItem withoutIdLocations(final SpecificationItem item) - { - final SpecificationItem.Builder builder = SpecificationItem.builder() - .id(item.getId()) - .title(item.getTitle()) - .description(item.getDescription()) - .rationale(item.getRationale()) - .comment(item.getComment()) - .status(item.getStatus()) - .location(item.getLocation()) - .forwards(item.isForwarding()); - item.getCoveredIds().forEach(builder::addCoveredId); - item.getDependOnIds().forEach(builder::addDependOnId); - item.getNeedsArtifactTypes().forEach(builder::addNeedsArtifactType); - item.getTags().forEach(builder::addTag); - return builder.build(); - } - private List runImporter(final String content) { final SpecificationListBuilder builder = SpecificationListBuilder.create(); diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java index 800638910..84117f590 100644 --- a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java +++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java @@ -41,4 +41,18 @@ public static SpecificationItem.Builder itemWithId(SpecificationItemId id) { public static SpecificationItem.Builder itemWithDefaultFilenameInLine(final int line) { return SpecificationItem.builder().location("file", line); } + + /** + * Creates a copy of an item without source locations on its IDs. + * + * @param item item to copy + * @return copy without source locations on its IDs + */ + public static SpecificationItem withoutIdLocations(final SpecificationItem item) { + return item.toBuilder() + .id(item.getId()) + .coveredIds(item.getCoveredIds()) + .dependOnIds(item.getDependOnIds()) + .build(); + } } 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 cd473f8d4..6bcafea07 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 @@ -13,6 +13,7 @@ import org.hamcrest.Matcher; import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.ImporterFactory; +import org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory; import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -100,28 +101,10 @@ protected void assertImport(final Path path, final String input, { final List importedItems = ImportAssertions.runImporterOnText(path, processTextInput(input), getImporterFactory()); - assertThat(importedItems.stream().map(AbstractLightWeightMarkupImporterTest::withoutIdLocations) + assertThat(importedItems.stream().map(ItemBuilderFactory::withoutIdLocations) .toList(), matcher); } - private static SpecificationItem withoutIdLocations(final SpecificationItem item) - { - final SpecificationItem.Builder builder = SpecificationItem.builder() - .id(item.getId()) - .title(item.getTitle()) - .description(item.getDescription()) - .rationale(item.getRationale()) - .comment(item.getComment()) - .status(item.getStatus()) - .location(item.getLocation()) - .forwards(item.isForwarding()); - item.getCoveredIds().forEach(builder::addCoveredId); - item.getDependOnIds().forEach(builder::addDependOnId); - item.getNeedsArtifactTypes().forEach(builder::addNeedsArtifactType); - item.getTags().forEach(builder::addTag); - return builder.build(); - } - private String processTextInput(final String input) { return TITLE_PLACEHOLDER.matcher(input) From 2f77aa3a451bed2e923e54af7bd9071591abc3c9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 08:02:47 +0200 Subject: [PATCH 07/16] Code review --- doc/spec/design.md | 4 ++-- doc/spec/system_requirements.md | 8 ++++++- .../markdown/TestMarkdownMarkupImporter.java | 8 ++++--- .../TestRestructuredTextImporter.java | 8 ++++--- .../TestLongTagImportingLineConsumer.java | 24 +++++++++++++++---- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/doc/spec/design.md b/doc/spec/design.md index 3010458b2..b18a4e7f1 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -115,7 +115,7 @@ The specification list builder is an import event listener that creates a list o `SpecificationItem` and `SpecificationListBuilder` preserve the individual declared, Covers, and Depends occurrences as `LocatedSpecificationItemId` -values while retaining compatible semantic-ID accessors. Located-ID lists are +values while retaining compatible item ID accessors. Located-ID lists are immutable when observed through the public API. Covers: @@ -129,7 +129,7 @@ Needs: impl, utest Text importers create zero-based UTF-16, start-inclusive and end-exclusive ranges for source ID occurrences and their represented components. Equal -semantic IDs at distinct source occurrences remain separate values. +item IDs at distinct source occurrences remain separate values. Needs: impl, utest diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index c47f729f2..35974a246 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -150,10 +150,16 @@ For every imported declared, covered, and dependency specification item ID, OFT shall retain the individual source occurrence. For source text, positions shall be zero-based UTF-16 offsets with start-inclusive, end-exclusive ranges. Where an ID component is generated or its source is unavailable, its component -range shall be absent. Equal semantic IDs at distinct source occurrences shall +range shall be absent. Equal item IDs at distinct source occurrences shall remain distinct occurrences. Short coverage tags shall not generate source ranges for their generated or covered IDs. +Rationale: + +The exact location of an item ID is required for IDE plugins in order to support +features like syntax highlighting, find occurrences, jump to definition and auto-complete. +Adding this feature to OFT helps avoid code duplications and improves reliability. + Needs: dsn #### Validate Gherkin Covers Metadata 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 d4bb05034..b28c4eced 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,6 +6,7 @@ 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 static org.junit.jupiter.api.Assertions.assertAll; import java.nio.file.Path; import java.util.List; @@ -82,9 +83,10 @@ void testImportsLocatedDeclarationCoverageAndDependencyIds() * req~dependency~3 """).get(0); - assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))); - assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 2, 15))); - assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18))); + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 2, 15))), + () -> assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18)))); } private static List importText(final String source) 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 7d352a555..11e18e06e 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 @@ -6,6 +6,7 @@ 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 static org.junit.jupiter.api.Assertions.assertAll; import java.nio.file.Path; @@ -91,9 +92,10 @@ void testImportsLocatedDeclarationCoverageAndDependencyIds() * req~dependency~3 """).get(0); - assertThat(item.getLocatedId().getRange(), is(range(0, 0, 13))); - assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 2, 15))); - assertThat(item.getLocatedDependOnIds().get(0).getRange(), is(range(4, 2, 18))); + assertAll( + () -> assertThat(item.getId(), is(createId("req", "subject", 1))), + () -> assertThat(item.getCoveredIds(), contains(createId("req", "covered", 2))), + () -> assertThat(item.getDependOnIds(), contains(createId("req", "dependency", 3)))); } private static java.util.List importText(final String source) diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java index 0e8dba563..43f259330 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLongTagImportingLineConsumer.java @@ -1,8 +1,8 @@ package org.itsallcode.openfasttrace.importer.tag.common; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertAll; import java.io.BufferedReader; import java.io.StringReader; @@ -70,8 +70,24 @@ void importsLocatedLongTagIds() final SpecificationItem item = listener.build().get(0); - assertThat(item.getLocatedId().getRange(), is(range(2, 4, 14))); - assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 18, 31))); + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is(range(2, 4, 14))), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 18, 31)))); + } + + // [utest->dsn~located-specification-item-id-tag-ranges~1] + @Test + void importsLocatedLongTagIdWithGeneratedName() + { + final SpecificationListBuilder listener = SpecificationListBuilder.create(); + new LongTagImportingLineConsumer(inputFile(), listener).readLine(3, + "😀 [impl -> dsn~covered~2" + "]"); + + final SpecificationItem item = listener.build().get(0); + + assertAll( + () -> assertThat(item.getLocatedId().getRange(), is(nullValue())), + () -> assertThat(item.getLocatedCoveredIds().get(0).getRange(), is(range(2, 12, 25)))); } private static SourceRange range(final int line, final int start, final int end) From 5253f1c36fcc8965835d5b0a0e42dec425aba396 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 08:10:55 +0200 Subject: [PATCH 08/16] Fix sonar warnings --- .../api/core/TestSpecificationItem.java | 14 ++++---- .../importer/gherkin/GherkinLineConsumer.java | 7 ++-- .../common/LongTagImportingLineConsumer.java | 33 ++++++++++--------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java index 9d3bc9889..2ef7b7fe3 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/core/TestSpecificationItem.java @@ -1,9 +1,7 @@ package org.itsallcode.openfasttrace.api.core; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -99,8 +97,9 @@ void testLocatedCoveredIdsAreImmutable() final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)) .addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build(); - assertThrows(UnsupportedOperationException.class, - () -> item.getLocatedCoveredIds().add(locatedId(ID))); + final List immutableList = item.getLocatedCoveredIds(); + final LocatedSpecificationItemId locatedId = locatedId(ID); + assertThrows(UnsupportedOperationException.class, () -> immutableList.add(locatedId)); } // [utest->dsn~located-specification-item-id-storage~1] @@ -110,8 +109,9 @@ void testLocatedDependOnIdsAreImmutable() final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)) .addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build(); - assertThrows(UnsupportedOperationException.class, - () -> item.getLocatedDependOnIds().add(locatedId(ID))); + final List immutableList = item.getLocatedDependOnIds(); + final LocatedSpecificationItemId locatedId = locatedId(ID); + assertThrows(UnsupportedOperationException.class, () -> immutableList.add(locatedId)); } private static LocatedSpecificationItemId locatedId(final SpecificationItemId id) diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java index f26982279..1c0fb7dec 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java @@ -108,7 +108,6 @@ private void readMetadata(final int lineNumber, final String line) private void readTagRegion(final int lineNumber, final String line) { - final String tags = line.trim(); if (!this.tagRegion) { clearMetadata(); @@ -119,6 +118,7 @@ private void readTagRegion(final int lineNumber, final String line) { return; } + final String tags = line.trim(); final Matcher matcher = ID_TAG.matcher(tags); while (matcher.find()) { @@ -127,7 +127,7 @@ private void readTagRegion(final int lineNumber, final String line) invalidateMetadata(lineNumber, "multiple @id tags before a scenario"); return; } - this.pendingId = locatedId(lineNumber, line, line.indexOf(tags) + matcher.start(1), matcher.group(1)); + this.pendingId = locatedId(lineNumber, line.indexOf(tags) + matcher.start(1), matcher.group(1)); if (this.pendingId == null) { return; @@ -291,8 +291,7 @@ private void invalidateMetadata(final int lineNumber, final String reason) this.invalidMetadata = true; } - private LocatedSpecificationItemId locatedId(final int lineNumber, final String line, final int column, - final String value) + private static LocatedSpecificationItemId locatedId(final int lineNumber, final int column, final String value) { return locatedId(lineNumber, column, value, SpecificationItemId.parseId(value)); } diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java index d2c355ba9..4733e5583 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java @@ -16,9 +16,11 @@ // [impl->dsn~import.full-coverage-tag-multiple-needed-coverage~1] class LongTagImportingLineConsumer extends AbstractRegexLineConsumer { - private static final Logger LOG = Logger - .getLogger(LongTagImportingLineConsumer.class.getName()); - + private static final Logger LOG = Logger.getLogger(LongTagImportingLineConsumer.class.getName()); + private static final String ARTIFACT_TYPE_GROUP = "artifactType"; + private static final String CUSTOM_NAME_GROUP = "customName"; + private static final String REVISION_GROUP = "revision"; + private static final String COVERED_IDS_GROUP = "coveredIds"; private static final String COVERING_ARTIFACT_TYPE_PATTERN = "\\p{Alpha}+"; // [impl->dsn~import.full-coverage-tag-with-revision~1] private static final String OPTIONAL_WHITESPACE = "\\s*"; @@ -55,7 +57,7 @@ class LongTagImportingLineConsumer extends AbstractRegexLineConsumer @Override public void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount) { - final List coveredIds = parseCoveredIds(matcher.group("coveredIds")); + final List coveredIds = parseCoveredIds(matcher.group(COVERED_IDS_GROUP)); final List neededArtifactTypes = parseNeededArtifactTypes(matcher.group("neededArtifactTypes")); final List generatedIds = createItemIds(matcher, lineNumber, lineMatchCount, coveredIds, neededArtifactTypes); @@ -85,9 +87,9 @@ private void addSpecificationItem(final int lineNumber, final Matcher matcher, int searchStart = 0; for (final SpecificationItemId coveredId : coveredIds) { - final int start = matcher.group("coveredIds").indexOf(coveredId.toString(), searchStart); + final int start = matcher.group(COVERED_IDS_GROUP).indexOf(coveredId.toString(), searchStart); searchStart = start + coveredId.toString().length(); - item.addCoveredId(locatedId(lineNumber, matcher.start("coveredIds") + start, coveredId)); + item.addCoveredId(locatedId(lineNumber, matcher.start(COVERED_IDS_GROUP) + start, coveredId)); } neededArtifactTypes.forEach(item::addNeedsArtifactType); this.listener.addSpecificationItem(item.build()); @@ -98,16 +100,17 @@ private static LocatedSpecificationItemId locatedGeneratedId(final int lineNumbe final SpecificationItemId id) { // [impl->dsn~located-specification-item-id-tag-ranges~1] - if (matcher.group("customName") == null) + if (matcher.group(CUSTOM_NAME_GROUP) == null) { return LocatedSpecificationItemId.builder().id(id).build(); } - final int start = matcher.start("artifactType"); - final int end = matcher.end("revision"); + final int start = matcher.start(ARTIFACT_TYPE_GROUP); + final int end = matcher.end(REVISION_GROUP); return LocatedSpecificationItemId.builder().id(id).range(sourceRange(lineNumber, start, end)) - .artifactTypeRange(sourceRange(lineNumber, start, matcher.end("artifactType"))) - .nameRange(sourceRange(lineNumber, matcher.start("customName"), matcher.end("customName"))) - .revisionRange(sourceRange(lineNumber, matcher.start("revision"), matcher.end("revision"))).build(); + .artifactTypeRange(sourceRange(lineNumber, start, matcher.end(ARTIFACT_TYPE_GROUP))) + .nameRange(sourceRange(lineNumber, matcher.start(CUSTOM_NAME_GROUP), matcher.end(CUSTOM_NAME_GROUP))) + .revisionRange(sourceRange(lineNumber, matcher.start(REVISION_GROUP), matcher.end(REVISION_GROUP))) + .build(); } private static LocatedSpecificationItemId locatedId(final int lineNumber, final int start, @@ -156,9 +159,9 @@ private List createItemIds(final Matcher matcher, final int final int lineMatchCount, final List coveredIds, final List neededArtifactTypes) { - final String artifactType = matcher.group("artifactType"); - final String customName = matcher.group("customName"); - final String revision = matcher.group("revision"); + final String artifactType = matcher.group(ARTIFACT_TYPE_GROUP); + final String customName = matcher.group(CUSTOM_NAME_GROUP); + final String revision = matcher.group(REVISION_GROUP); if (customName != null) { return List.of(SpecificationItemId.createId(artifactType, customName, parseRevision(revision))); From fd884b00d0504aa489dc50aa270f711dd2150c5e Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:02:35 +0200 Subject: [PATCH 09/16] Mark old methods as deprecated --- .../api/core/SpecificationItemId.java | 16 ++++---- .../api/importer/ImportEventListener.java | 8 +++- .../importer/SpecificationListBuilder.java | 16 ++------ .../TestSpecificationListBuilder.java | 41 ++++++++++++------- .../TestRestructuredTextImporter.java | 8 +--- 5 files changed, 48 insertions(+), 41 deletions(-) diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItemId.java b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItemId.java index 1b45ff2d7..1a83a889e 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItemId.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/core/SpecificationItemId.java @@ -92,8 +92,10 @@ public String getArtifactType() } @Override - public boolean equals(final Object o) { - if (!(o instanceof final SpecificationItemId that)) { + public boolean equals(final Object o) + { + if (!(o instanceof final SpecificationItemId that)) + { return false; } return revision == that.revision && Objects.equals(name, that.name) @@ -101,19 +103,20 @@ public boolean equals(final Object o) { } @Override - public int hashCode() { + public int hashCode() + { return Objects.hash(name, revision, artifactType); } @Override public String toString() { - return this.artifactType + ARTIFACT_TYPE_SEPARATOR + this.name + REVISION_SEPARATOR + this.revision; + return this.artifactType + ARTIFACT_TYPE_SEPARATOR + this.name + REVISION_SEPARATOR + this.revision; } /** * Get this item with a wildcard as revision. - * + * * @return a copy of this ID with a wildcard as revision. */ public SpecificationItemId toRevisionWildcard() @@ -145,8 +148,7 @@ public static SpecificationItemId parseId(final String idText) * the revision * @return the specification item ID */ - public static SpecificationItemId createId(final String artifactType, final String name, - final int revision) + public static SpecificationItemId createId(final String artifactType, final String name, final int revision) { return new SpecificationItemId.Builder().artifactType(artifactType).name(name) .revision(revision).build(); diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/ImportEventListener.java b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/ImportEventListener.java index 8589d2f3d..03a3c14f6 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/ImportEventListener.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/ImportEventListener.java @@ -13,7 +13,7 @@ public interface ImportEventListener /** * The importer found a new specification item. The * {@link SpecificationItemId} must be defined using - * {@link #setId(SpecificationItemId)}. + * {@link #setId(LocatedSpecificationItemId)}. */ void beginSpecificationItem(); @@ -22,7 +22,9 @@ public interface ImportEventListener * * @param id * the ID of the new item + * @deprecated Use {@link #setId(LocatedSpecificationItemId)} instead. */ + @Deprecated(since = "4.9.0", forRemoval = true) void setId(final SpecificationItemId id); /** @@ -82,7 +84,9 @@ default void setId(final LocatedSpecificationItemId id) * * @param id * the ID of the item that is covered + * @deprecated Use {@link #addCoveredId(LocatedSpecificationItemId)} instead. */ + @Deprecated(since = "4.9.0", forRemoval = true) void addCoveredId(final SpecificationItemId id); /** @@ -101,7 +105,9 @@ default void addCoveredId(final LocatedSpecificationItemId id) * * @param id * the ID of the item depends on + * @deprecated Use {@link #addDependsOnId(LocatedSpecificationItemId)} instead. */ + @Deprecated(since = "4.9.0", forRemoval = true) void addDependsOnId(final SpecificationItemId id); /** diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java index 608e0afcd..3d2afc279 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java @@ -87,17 +87,13 @@ public void setStatus(final ItemStatus status) @Override public void addCoveredId(final SpecificationItemId id) { - // TODO: call new method - // [impl->dsn~filtering-by-artifact-types-during-import~1] - if (isAcceptedArtifactType(id.getArtifactType())) - { - this.itemBuilder.addCoveredId(id); - } + this.addCoveredId(LocatedSpecificationItemId.builder().id(id).build()); } @Override public void addCoveredId(final LocatedSpecificationItemId id) { + // [impl->dsn~filtering-by-artifact-types-during-import~1] // [impl->dsn~located-specification-item-id-storage~1] if (isAcceptedArtifactType(id.getId().getArtifactType())) { @@ -126,17 +122,13 @@ public void appendComment(final String fragment) @Override public void addDependsOnId(final SpecificationItemId id) { - // TODO: call new method - // [impl->dsn~filtering-by-artifact-types-during-import~1] - if (isAcceptedArtifactType(id.getArtifactType())) - { - this.itemBuilder.addDependOnId(id); - } + this.addDependsOnId(LocatedSpecificationItemId.builder().id(id).build()); } @Override public void addDependsOnId(final LocatedSpecificationItemId id) { + // [impl->dsn~filtering-by-artifact-types-during-import~1] // [impl->dsn~located-specification-item-id-storage~1] if (isAcceptedArtifactType(id.getId().getArtifactType())) { diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java index db3a02b26..c61565862 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/TestSpecificationListBuilder.java @@ -43,6 +43,17 @@ void testPreservesLocatedIdOccurrences() () -> assertThat(item.getDependOnIds(), contains(dependencyId))); } + private static LocatedSpecificationItemId locatedId(final String artifactType, final String name, + final int revision) + { + return locatedId(SpecificationItemId.createId(artifactType, name, revision)); + } + + private static LocatedSpecificationItemId locatedId(final SpecificationItemId id) + { + return LocatedSpecificationItemId.builder().id(id).build(); + } + private static LocatedSpecificationItemId locatedId(final SpecificationItemId id, final int column) { final SourceRange range = new SourceRange(new SourcePosition(0, column), @@ -67,7 +78,7 @@ private SpecificationListBuilder createBasicListBuilder() { final SpecificationListBuilder builder = SpecificationListBuilder.create(); builder.beginSpecificationItem(); - builder.setId(ID); + builder.setId(locatedId(ID)); return builder; } @@ -105,11 +116,11 @@ void testFilterArtifactOfType() { final SpecificationListBuilder builder = createListBuilderFilteringByArtifactTypes("dsn"); builder.beginSpecificationItem(); - builder.setId(SpecificationItemId.createId("impl", "ignore", 1)); + builder.setId(locatedId("impl", "ignore", 1)); builder.endSpecificationItem(); builder.beginSpecificationItem(); final SpecificationItemId importedId = SpecificationItemId.createId("dsn", "import", 1); - builder.setId(importedId); + builder.setId(locatedId(importedId)); builder.endSpecificationItem(); final List items = builder.build(); assertThat(items.size(), equalTo(1)); @@ -133,7 +144,7 @@ void testFilterNeededArtifactType() "utest", "itest"); builder.beginSpecificationItem(); final SpecificationItemId id = SpecificationItemId.createId("dsn", "import", 1); - builder.setId(id); + builder.setId(locatedId(id)); builder.addNeededArtifactType("impl"); builder.addNeededArtifactType("utest"); builder.addNeededArtifactType("itest"); @@ -152,9 +163,9 @@ void testFilterCoversLinkWithArtifactType() "dsn"); builder.beginSpecificationItem(); final SpecificationItemId importedId = SpecificationItemId.createId("dsn", "import", 1); - builder.setId(importedId); - builder.addCoveredId(acceptedId); - builder.addCoveredId(rejectedId); + builder.setId(locatedId(importedId)); + builder.addCoveredId(locatedId(acceptedId)); + builder.addCoveredId(locatedId(rejectedId)); builder.endSpecificationItem(); final List items = builder.build(); assertThat(items.get(0).getCoveredIds(), containsInAnyOrder(acceptedId)); @@ -170,9 +181,9 @@ void testFilterDependsLinkWithArtifactType() "dsn"); builder.beginSpecificationItem(); final SpecificationItemId importedId = SpecificationItemId.createId("dsn", "import", 1); - builder.setId(importedId); - builder.addDependsOnId(acceptedId); - builder.addDependsOnId(rejectedId); + builder.setId(locatedId(importedId)); + builder.addDependsOnId(locatedId(acceptedId)); + builder.addDependsOnId(locatedId(rejectedId)); builder.endSpecificationItem(); final List items = builder.build(); assertThat(items.get(0).getDependOnIds(), containsInAnyOrder(acceptedId)); @@ -183,10 +194,10 @@ void testDuplicateIdNotIgnored() { final SpecificationListBuilder builder = SpecificationListBuilder.create(); builder.beginSpecificationItem(); - builder.setId(ID); + builder.setId(locatedId(ID)); builder.endSpecificationItem(); builder.beginSpecificationItem(); - builder.setId(ID); + builder.setId(locatedId(ID)); builder.endSpecificationItem(); assertThat(builder.getItemCount(), equalTo(2)); } @@ -216,7 +227,7 @@ private void addItemWithStatus(final SpecificationListBuilder builder, final Str { builder.beginSpecificationItem(); final SpecificationItemId id = SpecificationItemId.createId("dsn", name, 1); - builder.setId(id); + builder.setId(locatedId(id)); if (status != null) { builder.setStatus(status); @@ -251,7 +262,7 @@ private void addItemWithTags(final SpecificationListBuilder builder, final Strin { builder.beginSpecificationItem(); final SpecificationItemId idA = SpecificationItemId.createId("dsn", name, 1); - builder.setId(idA); + builder.setId(locatedId(idA)); for (final String tag : tags) { builder.addTag(tag); @@ -286,7 +297,7 @@ void testMultilineTextFieldsGetTrimmed() { final SpecificationListBuilder builder = SpecificationListBuilder.create(); builder.beginSpecificationItem(); - builder.setId(SpecificationItemId.createId("foo", "bar", 1)); + builder.setId(locatedId("foo", "bar", 1)); builder.appendComment(" a comment "); builder.appendDescription(" a description\t \t"); builder.appendRationale("\n\na rationale\n \n"); 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 11e18e06e..e989e3934 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 @@ -10,7 +10,8 @@ import java.nio.file.Path; -import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.importer.*; import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest; @@ -103,11 +104,6 @@ private static java.util.List importText(final String source) return ImportAssertions.runImporterOnText(Path.of("located.rst"), source, importerFactory); } - private static SourceRange range(final int line, final int start, final int end) - { - return new SourceRange(new SourcePosition(line, start), new SourcePosition(line, end)); - } - protected String formatTitle(final String title, final int level) { return title + "\n" + "=".repeat(title.length()); From 63bf6bcd58cbe382889de29335cf3b83a7960892 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:02:54 +0200 Subject: [PATCH 10/16] Increment version, add release note --- doc/changes/changes.md | 1 + doc/changes/changes_4.9.0.md | 19 +++++++++++++++++++ parent/pom.xml | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 doc/changes/changes_4.9.0.md diff --git a/doc/changes/changes.md b/doc/changes/changes.md index 97bef72df..a549f6b2b 100644 --- a/doc/changes/changes.md +++ b/doc/changes/changes.md @@ -1,5 +1,6 @@ # Changes +* [4.9.0](changes_4.9.0.md) * [4.8.0](changes_4.8.0.md) * [4.7.0](changes_4.7.0.md) * [4.6.0](changes_4.6.0.md) diff --git a/doc/changes/changes_4.9.0.md b/doc/changes/changes_4.9.0.md new file mode 100644 index 000000000..f35731a25 --- /dev/null +++ b/doc/changes/changes_4.9.0.md @@ -0,0 +1,19 @@ +# OpenFastTrace 4.9.0, released 2026-08-07 + +Code name: Item ID Location + +## Summary + +Importers for long coverage tags, Markdown and reStructuredText now collect the location of coverage IDs in documents and source code. + +The exact location of an item ID is required for IDE plugins in order to support features like syntax highlighting, find occurrences, jump to definition and auto-complete. Adding this feature to OFT helps avoid code duplications and improves reliability. + +**Deprecation Notes:** Users of the OFT API and OFT plugin authors please note the deprecation of the following methods: +* Interface `org.itsallcode.openfasttrace.api.importer.ImportEventListener`: methods + * `void setId(SpecificationItemId id)` + * `void addCoveredId(SpecificationItemId id)` + * `void addDependsOnId(SpecificationItemId id)` + +## New Features + +* #570: Collect source code location of specification item IDs diff --git a/parent/pom.xml b/parent/pom.xml index 648ce017f..0acaabebf 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -10,7 +10,7 @@ Free requirement tracking suite https://github.com/itsallcode/openfasttrace - 4.8.0 + 4.9.0 17 6.1.0-M1 6.1.1 From 1bf6c36691263935cc0594c43f1f75ec16f280d5 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:12:29 +0200 Subject: [PATCH 11/16] Fix compiler warnings --- .../api/importer/SpecificationListBuilder.java | 3 +++ .../restructuredtext/TestRestructuredTextImporter.java | 3 ++- .../importer/tag/common/LongTagImportingLineConsumer.java | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java index 3d2afc279..de4a11051 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/importer/SpecificationListBuilder.java @@ -66,6 +66,7 @@ private void resetState() } @Override + @SuppressWarnings("removal") // Need to implement method from interface for backward compatibility public void setId(final SpecificationItemId id) { this.setId(LocatedSpecificationItemId.builder().id(id).build()); @@ -85,6 +86,7 @@ public void setStatus(final ItemStatus status) } @Override + @SuppressWarnings("removal") // Need to implement method from interface for backward compatibility public void addCoveredId(final SpecificationItemId id) { this.addCoveredId(LocatedSpecificationItemId.builder().id(id).build()); @@ -120,6 +122,7 @@ public void appendComment(final String fragment) } @Override + @SuppressWarnings("removal") // Need to implement method from interface for backward compatibility public void addDependsOnId(final SpecificationItemId id) { this.addDependsOnId(LocatedSpecificationItemId.builder().id(id).build()); 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 e989e3934..f861a534b 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 @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import java.nio.file.Path; +import java.util.List; import org.itsallcode.openfasttrace.api.core.SpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; @@ -99,7 +100,7 @@ void testImportsLocatedDeclarationCoverageAndDependencyIds() () -> assertThat(item.getDependOnIds(), contains(createId("req", "dependency", 3)))); } - private static java.util.List importText(final String source) + private static List importText(final String source) { return ImportAssertions.runImporterOnText(Path.of("located.rst"), source, importerFactory); } diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java index 4733e5583..0aeb24bc8 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LongTagImportingLineConsumer.java @@ -33,14 +33,14 @@ class LongTagImportingLineConsumer extends AbstractRegexLineConsumer + "(?\\p{Alpha}+(?:" + OPTIONAL_WHITESPACE + "," + OPTIONAL_WHITESPACE + "\\p{Alpha}+)*)"; private static final String TAG_REGEX = TAG_PREFIX + OPTIONAL_WHITESPACE - + "(?" + COVERING_ARTIFACT_TYPE_PATTERN + ")" + + "(?<" + ARTIFACT_TYPE_GROUP + ">" + COVERING_ARTIFACT_TYPE_PATTERN + ")" + "(?:" + SpecificationItemId.ARTIFACT_TYPE_SEPARATOR // [impl->dsn~import.full-coverage-tag-with-name-and-revision~1] - + "(?" + SpecificationItemId.ITEM_NAME_PATTERN + ")?" + + "(?<" + CUSTOM_NAME_GROUP + ">" + SpecificationItemId.ITEM_NAME_PATTERN + ")?" + SpecificationItemId.REVISION_SEPARATOR - + "(?" + SpecificationItemId.ITEM_REVISION_PATTERN + "))?" + + "(?<" + REVISION_GROUP + ">" + SpecificationItemId.ITEM_REVISION_PATTERN + "))?" + OPTIONAL_WHITESPACE + "->" + OPTIONAL_WHITESPACE - + "(?" + COVERED_IDS + ")" + + "(?<" + COVERED_IDS_GROUP + ">" + COVERED_IDS + ")" + OPTIONAL_WHITESPACE + "(?:" + NEEDS_COVERAGE + OPTIONAL_WHITESPACE + ")?" + TAG_SUFFIX; From a8c8589838ace647f7e400ca09b909c2b2e0f477 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:34:57 +0200 Subject: [PATCH 12/16] Fix sonar warning about unrelated coverage info [INFO] Sensor JaCoCo XML Report Importer [jacoco] [INFO] Importing 17 report(s). Turn your logs in debug mode in order to see the exhaustive list. [WARNING] File 'TreeContentHandler.java' not found in project source --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index 14e96d6b3..2823fa8bc 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,6 @@ itsallcode https://sonarcloud.io - ${maven.multiModuleProjectDirectory}/**/target/site/jacoco/jacoco.xml testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java UTF-8 From f0587a95e7f40e6f0e38eaa207e29c728f5c13a6 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:41:52 +0200 Subject: [PATCH 13/16] Upgrade dev dependencies --- parent/pom.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index 0acaabebf..e238cb49a 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -13,7 +13,7 @@ 4.9.0 17 6.1.0-M1 - 6.1.1 + 6.1.2 3.5.6 UTF-8 UTF-8 @@ -289,7 +289,7 @@ org.apache.maven.plugins maven-toolchains-plugin - 3.2.0 + 3.3.0 @@ -449,7 +449,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.3 + 1.8.0 oss @@ -584,7 +584,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.5.0 + 3.5.1 org.apache.maven.plugins @@ -599,7 +599,6 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.3 true resolveCiFriendliesOnly From c94e4facf87b7be6c954a5a91b5c9acb7457ec42 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:50:26 +0200 Subject: [PATCH 14/16] Fix sonar warning about unspecified plugin version --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 2823fa8bc..e0449b846 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,11 @@ true + + org.sonarsource.scanner.maven + sonar-maven-plugin + 5.7.0.6970 + From e65878f2fd328feecf3a0bb9a17f99d854fd3a76 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 10:53:41 +0200 Subject: [PATCH 15/16] Fix sonar warnings about invalid characters --- .../openfasttrace/api/importer/input/TestRealFileInput.java | 2 +- .../openfasttrace/importer/zip/ITZipFileImporter.java | 2 +- .../openfasttrace/importer/zip/input/TestZipEntryInput.java | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/input/TestRealFileInput.java b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/input/TestRealFileInput.java index 555570f31..a353c160f 100644 --- a/api/src/test/java/org/itsallcode/openfasttrace/api/importer/input/TestRealFileInput.java +++ b/api/src/test/java/org/itsallcode/openfasttrace/api/importer/input/TestRealFileInput.java @@ -104,7 +104,7 @@ void testReadFileWithInvalidEncoding2() throws IOException final Path path = tempDir.resolve("file"); Files.write(path, new byte[] { (byte) 0x9F, (byte) 0x88 }); final InputFile inputFile = RealFileInput.forPath(path, StandardCharsets.UTF_8); - assertThat(readContent(inputFile), equalTo("��")); + assertThat(readContent(inputFile), equalTo("\uFFFD\uFFFD")); } private Path writeTempFile(final String content, final Charset charset) throws IOException diff --git a/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/ITZipFileImporter.java b/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/ITZipFileImporter.java index d2744adee..98ad9bf37 100644 --- a/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/ITZipFileImporter.java +++ b/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/ITZipFileImporter.java @@ -139,7 +139,7 @@ void testImportZipWithInvalidEncoding() throws IOException addEntryToZip("file1.c", new byte[] { (byte) 0x9F, (byte) 0x88 }); final List importedFiles = runImporter(1); assertThat(importedFiles.get(0).getPath(), equalTo(this.zipFile.getPath() + "!file1.c")); - assertThat(this.actualFileContent.get(0), equalTo("��")); + assertThat(this.actualFileContent.get(0), equalTo("\uFFFD\uFFFD")); } private void addZipEntryDirectory(final String name) throws IOException diff --git a/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/input/TestZipEntryInput.java b/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/input/TestZipEntryInput.java index ffff9e4e0..601e67232 100644 --- a/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/input/TestZipEntryInput.java +++ b/importer/zip/src/test/java/org/itsallcode/openfasttrace/importer/zip/input/TestZipEntryInput.java @@ -121,13 +121,14 @@ void testReadInvalidEncoding() throws IOException { final InputFile inputFile = ZipEntryInput.forZipEntry(zip, new ZipEntry("file"), StandardCharsets.UTF_8); - assertThat(readContent(inputFile), equalTo("��")); + assertThat(readContent(inputFile), equalTo("\uFFFD\uFFFD")); } } private String readContent(final InputFile inputFile) throws IOException { - try(final BufferedReader reader = inputFile.createReader()) { + try (final BufferedReader reader = inputFile.createReader()) + { return reader.lines().collect(joining("\n")); } } From e63f5500cc4457ab85bb8b7120d96e381dac6801 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 7 Aug 2026 11:15:18 +0200 Subject: [PATCH 16/16] Specifiy verison for plugin in pluginManagement --- parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parent/pom.xml b/parent/pom.xml index e238cb49a..bb1d55ce9 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -449,7 +449,6 @@ org.codehaus.mojo flatten-maven-plugin - 1.8.0 oss @@ -599,6 +598,7 @@ org.codehaus.mojo flatten-maven-plugin + 1.8.0 true resolveCiFriendliesOnly