From cae6f1c0b5a739b883871346964968910e672079 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 14:00:02 +0200 Subject: [PATCH 01/17] #563: Added Gherkin `.feature` specification import --- .agents/skills/openfasttrace/SKILL.md | 22 ++ doc/changes/changes.md | 1 + doc/changes/changes_4.7.0.md | 9 + doc/spec/design.md | 75 +++-- doc/spec/design/quality_requirements.md | 11 +- doc/spec/system_requirements.md | 41 +++ doc/user_guide.md | 19 ++ importer/gherkin/pom.xml | 31 ++ .../gherkin/src/main/java/module-info.java | 10 + .../importer/gherkin/GherkinImporter.java | 276 ++++++++++++++++++ .../gherkin/GherkinImporterFactory.java | 23 ++ ...openfasttrace.api.importer.ImporterFactory | 1 + .../importer/gherkin/GherkinImporterTest.java | 116 ++++++++ .../importer/tag/TagImporterFactory.java | 1 - oft-self-trace.sh | 1 + parent/pom.xml | 10 +- pom.xml | 1 + product/pom.xml | 4 + .../TestAllServicesAvailable.java | 2 +- .../TestInitializingServiceLoader.java | 4 +- .../importer/ImporterFactoryLoaderIT.java | 22 ++ 21 files changed, 653 insertions(+), 27 deletions(-) create mode 100644 doc/changes/changes_4.7.0.md create mode 100644 importer/gherkin/pom.xml create mode 100644 importer/gherkin/src/main/java/module-info.java create mode 100644 importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java create mode 100644 importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java create mode 100644 importer/gherkin/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory create mode 100644 importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java diff --git a/.agents/skills/openfasttrace/SKILL.md b/.agents/skills/openfasttrace/SKILL.md index 68debdbce..2e25ae04b 100644 --- a/.agents/skills/openfasttrace/SKILL.md +++ b/.agents/skills/openfasttrace/SKILL.md @@ -71,6 +71,28 @@ Multiple coverage: ## Tracing +## Syntax: Gherkin + +Gherkin `.feature` files can define OFT scenario items. Put exactly one OFT ID +in the contiguous tag region immediately before a `Scenario` or `Scenario +Outline`. Optional `# Covers:` and `# Needs:` comments belong between the tags +and the scenario header. Multiple `Covers` comments accumulate IDs; `Needs` +may appear once. + +```gherkin +@id:scn~user-login~1 +# Covers: req~authentication~1 +# Needs: dsn, itest +Scenario: User logs in + Given a registered user + When valid credentials are entered + Then access is granted +``` + +Legacy coverage tags are recognized only in Gherkin comments, for example +`# [impl~login~1 -> dsn~authentication~1]`. Executable Gherkin lines are not +evaluated for coverage tags. + Tracing can be performed via CLI, Maven, or Gradle. ### CLI Usage diff --git a/doc/changes/changes.md b/doc/changes/changes.md index e12760c6d..a3891fc6e 100644 --- a/doc/changes/changes.md +++ b/doc/changes/changes.md @@ -1,5 +1,6 @@ # Changes +* [4.7.0](changes_4.7.0.md) * [4.6.0](changes_4.6.0.md) * [4.5.0](changes_4.5.0.md) * [4.4.0](changes_4.4.0.md) diff --git a/doc/changes/changes_4.7.0.md b/doc/changes/changes_4.7.0.md new file mode 100644 index 000000000..0358a2274 --- /dev/null +++ b/doc/changes/changes_4.7.0.md @@ -0,0 +1,9 @@ +# OpenFastTrace 4.7.0, released 2026-07-?? + +Code name: Gherkin Importer + +## Summary + +## New Features + +* #563: Added Gherkin `.feature` specification import for annotated scenarios and scenario outlines. diff --git a/doc/spec/design.md b/doc/spec/design.md index 48353ade3..7156b075f 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -235,8 +235,49 @@ The default priorities for standard importers are: 1. Markdown Importer: 1000 2. reStructuredText Importer: 2000 3. Specobject (ReqM2) Importer: 3000 -4. Tag Importer: 10000 -5. Zip Importer: 20000 +4. Gherkin Importer: 9000 +5. Tag Importer: 10000 +6. Zip Importer: 20000 + +### Gherkin Import + +#### Importer Selection +`dsn~gherkin.importer-selection~1` + +The Gherkin importer selects `.feature` files with priority 9000, ahead of the Tag Importer. This gives Gherkin syntax ownership to the dedicated importer while retaining the Tag Importer as the fallback for other source files. + +Covers: + +* `req~gherkin-scenario-import~1` + +Needs: impl, utest, itest + +#### Streaming Import +`dsn~gherkin.streaming-import~1` + +The Gherkin importer scans each input file once. It imports only scenarios and scenario outlines with exactly one immediately preceding `@id:` tag, maps header location and title to import events, and streams non-comment scenario steps into the description until a Gherkin block boundary. + +Scoped `# Covers:` and `# Needs:` comments are validated between the tag region and header. Multiple `Covers` directives accumulate coverage IDs; `Needs` occurs at most once. The importer retains only active metadata and previously imported IDs. + +Covers: + +* `req~gherkin-scenario-import~1` +* `req~gherkin-metadata-validation~1` + +Needs: impl, utest + +#### Support Legacy Coverage Tags +`dsn~gherkin.comment-coverage-tags~1` + +The Gherkin importer delegates only comment lines to the shared coverage-tag parser. This preserves legacy comment coverage tags without applying their regular expressions to executable Gherkin text. + +When a comment tag occurs inside an imported scenario, its listener events are buffered until the scenario ends. The shared parser emits complete specification-item event sequences, so emitting them immediately would interleave them with the open scenario item and corrupt the listener state. + +Covers: + +* `req~gherkin-comment-coverage-tags~1` + +Needs: impl, utest ### ReqM2 File Detection `dsn~import.reqm2-file-detection~1` @@ -558,7 +599,7 @@ Needs: impl, itest #### HTML Reports Allows Configuring Details Display Status `dsn~reporting.html.details-display~1` -OFT allows configuring the specification item detail section display status (expanded or collapsed). Default is collapsed. +OFT allows configuring the specification item detail section display status (expanded or collapsed). Default is collapsed. Covers: @@ -699,13 +740,13 @@ Needs: impl, utest A requirement ID has the following format requirement-id = type "~" id "~" revision - + type = 1*ALPHA - + id = id-fragment *("." id-fragment) - + id-fragment = UNICODE_ALPHA *(UNICODE_ALPHA / DIGIT / "_" / "-") - + revision = 1*DIGIT Rationale: @@ -750,9 +791,9 @@ Needs: impl, utest In Markdown specification item references have the following format: reference = (plain-reference / url-style-link) - + plain-reference = requirement-id - + url-style-link = "[" link-text "]" "(" "#" requirement-id ")" Covers: @@ -767,9 +808,9 @@ Needs: impl, utest The Markdown Importer supports the following format for links that cover a different specification item. covers-list = covers-header 1*(LINEBREAK covers-line) - + covers-header = "Covers:" *WSP - + covers-line = *WSP "*" *WSP reference Only one traced reference per line is supported. Any optional text after the reference is ignored if it is separated by at least one whitespace character @@ -790,9 +831,9 @@ Needs: impl, utest The Markdown Importer supports the following format for links to a different specification item which the current depends on. depends-list = depends-header 1*(LINEBREAK depends-line) - + depends-header = "Depends:" *WSP - + depends-line = *WSP "*" *WSP reference Only one traced reference per line is supported. Any optional text after the reference is ignored if it is separated by at least one whitespace character @@ -852,11 +893,11 @@ The Markdown Importer supports forwarding required coverage from one artifact ty artifact-need-redirection = skipped-artifact-type *WSP "-->" *WSP target-artifact-list *WSP ":" *WSP original-requirement-id - + skipped-artifact-type = artifact-type - + target-artifact-list = artifact-type *("," *WSP artifact-type) - + original-requirement-id = requirement-id The following example shows an architectural specification item that forwards the needed coverage directly to the detailed design and an integration test: @@ -1234,7 +1275,7 @@ Needs: impl, utest ### Why is This Architecture Relevant? -Authors of importers need to be able to rely on these cleanups being done centrally, so that they don't have to implement them themselves. +Authors of importers need to be able to rely on these cleanups being done centrally, so that they don't have to implement them themselves. ### Alternatives Considered diff --git a/doc/spec/design/quality_requirements.md b/doc/spec/design/quality_requirements.md index 04101bacf..3d94811a3 100644 --- a/doc/spec/design/quality_requirements.md +++ b/doc/spec/design/quality_requirements.md @@ -115,11 +115,12 @@ The following rules are written for both human contributors and coding agents. T ### Java Test Rules -1. Use Hamcrest matchers for assertions. -2. Extract repeated complex assertions into dedicated matcher classes when that improves readability. -3. Declare only the specific checked exceptions that are directly thrown by the code under test. Do not use generic exceptions (e.g., Exception, Throwable) in test method signatures. -4. When asserting exceptions, the assertion should invoke exactly one method call—the method under test. Avoid nesting additional method calls inside the assertion. Prepare all inputs outside the assertion so the failure is attributable to a single call. -5. Prefer @ParameterizedTest for testing multiple input variations instead of generating or mutating test data within a single test method (e.g., loops or inline variations). +1. Test method names must start with `test` and describe the behavior under test. +2. Use Hamcrest matchers for assertions. +3. Extract repeated complex assertions into dedicated matcher classes when that improves readability. +4. Declare only the specific checked exceptions that are directly thrown by the code under test. Do not use generic exceptions (e.g., Exception, Throwable) in test method signatures. +5. When asserting exceptions, the assertion should invoke exactly one method call—the method under test. Avoid nesting additional method calls inside the assertion. Prepare all inputs outside the assertion so the failure is attributable to a single call. +6. Prefer @ParameterizedTest for testing multiple input variations instead of generating or mutating test data within a single test method (e.g., loops or inline variations). ## Dependency Policy diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index c4efb2b41..8cc5f2b75 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -119,6 +119,47 @@ Needs: req OFT imports coverage tags from source code files. +### Gherkin Import +`feat~gherkin-import~1` + +OFT imports specification items from annotated Gherkin scenarios and scenario outlines in `.feature` files. + +Needs: req + +#### Import Gherkin Scenarios +`req~gherkin-scenario-import~1` + +OFT imports a Gherkin `Scenario` or `Scenario Outline` as a specification item when its immediately preceding contiguous tag region contains exactly one `@id:` tag. The scenario header supplies the title and location; scenario steps form the description. + +Covers: + +* [feat~gherkin-import~1](#gherkin-import) + +Needs: dsn + +#### Validate Gherkin Metadata +`req~gherkin-metadata-validation~1` + +OFT accepts optional, scoped `# Covers:` and `# Needs:` comments between an ID tag region and its scenario header. `Covers` may occur multiple times; `Needs` may occur once. Each directive must contain a non-empty, valid, duplicate-free list. Invalid IDs, artifact types, repeated IDs, duplicate scenario IDs, and orphan directives cause an import error that identifies the file and line. + +Covers: + +* [feat~gherkin-import~1](#gherkin-import) + +Needs: dsn + +#### Preserve Gherkin Comment Coverage Tags +`req~gherkin-comment-coverage-tags~1` + +OFT imports legacy coverage tags from comments in `.feature` files, but does not evaluate coverage tags in executable Gherkin lines. + +Covers: + +* [feat~gherkin-import~1](#gherkin-import) +* [feat~coverage-tag-import~1](#coverage-tag-import) + +Needs: dsn + Rationale: Coverage tags indicate parts of the source code that implements a certain requirement. diff --git a/doc/user_guide.md b/doc/user_guide.md index 15daae3d9..70b29d02c 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -822,6 +822,25 @@ Note that XML is at the moment not yet supported by the Tag Importer, because it **Test Specification languages** * [Gherkin](https://cucumber.io/docs/gherkin/) (`.feature`) + +#### Gherkin + +OFT imports Gherkin `Scenario` and `Scenario Outline` blocks in `.feature` files when the immediately preceding tag region contains one OFT ID tag. Place optional `Covers` and `Needs` comments after the tags and before the scenario header: + +```gherkin +@smoke +@id:scn~user-can-log-in~1 +# Covers: req~authentication~1 +# Needs: dsn, itest +Scenario: A registered user logs in + Given a registered user + When they enter valid credentials + Then access is granted +``` + +The scenario header becomes the item title and location; its executable steps become the description. `Covers` and `Needs` are case-sensitive and optional. Multiple `Covers` comments accumulate coverage IDs, while `Needs` may appear once; all lists must be non-empty and comma-separated. Invalid or duplicate IDs, types, or directives cause an import error. + +Existing full coverage tags remain supported in Gherkin comments, for example `# [impl~login~1 -> dsn~authentication~1]`. OFT deliberately ignores coverage-tag-shaped text in executable Gherkin lines. #### Markdown diff --git a/importer/gherkin/pom.xml b/importer/gherkin/pom.xml new file mode 100644 index 000000000..4038d4640 --- /dev/null +++ b/importer/gherkin/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + openfasttrace-importer-gherkin + OpenFastTrace Gherkin Importer + + ../../openfasttrace-mc-deployable-parent/pom.xml + org.itsallcode.openfasttrace + openfasttrace-mc-deployable-parent + ${revision} + + + ${reproducible.build.timestamp} + + + + org.itsallcode.openfasttrace + openfasttrace-api + + + org.itsallcode.openfasttrace + openfasttrace-importer-tag-importer-common + + + org.itsallcode.openfasttrace + openfasttrace-testutil + test + + + diff --git a/importer/gherkin/src/main/java/module-info.java b/importer/gherkin/src/main/java/module-info.java new file mode 100644 index 000000000..040409eca --- /dev/null +++ b/importer/gherkin/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import org.itsallcode.openfasttrace.importer.gherkin.GherkinImporterFactory; + +/** Provides an importer for Gherkin {@code .feature} files. */ +module org.itsallcode.openfasttrace.importer.gherkin { + requires transitive org.itsallcode.openfasttrace.api; + requires org.itsallcode.openfasttrace.importer.tag.common; + + provides org.itsallcode.openfasttrace.api.importer.ImporterFactory + with GherkinImporterFactory; +} diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java new file mode 100644 index 000000000..501039d01 --- /dev/null +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java @@ -0,0 +1,276 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import java.util.*; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.importer.ImportEventListener; +import org.itsallcode.openfasttrace.api.importer.Importer; +import org.itsallcode.openfasttrace.api.importer.input.InputFile; +import org.itsallcode.openfasttrace.importer.tag.common.CoverageTagParser; +import org.itsallcode.openfasttrace.importer.tag.common.LineReader; +import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer; + +/** Imports annotated Gherkin scenarios while streaming the input once. */ +// [impl->dsn~gherkin.streaming-import~1] +// [impl->dsn~gherkin.comment-coverage-tags~1] +final class GherkinImporter implements Importer, LineConsumer { + private static final Pattern ID_TAG = Pattern.compile("@id:(\\S+)"); + private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:\\s*(.*)$"); + private static final Pattern BOUNDARY = Pattern + .compile("^\\s*(?:Scenario(?: Outline)?|Feature|Rule|Background|Examples):"); + private static final Pattern DIRECTIVE = Pattern.compile("^\\s*#\\s*(Covers|Needs):(.*)$"); + + private final InputFile file; + private final ImportEventListener listener; + private final LineConsumer coverageTagParser; + // Legacy tags emit complete items and therefore must not interleave with an open scenario item. + private final LineConsumer delayedCoverageTagParser; + private final EventBuffer delayedCoverageEvents = new EventBuffer(); + private final Set importedIds = new LinkedHashSet<>(); + private SpecificationItemId pendingId; + private Set coveredIds = new LinkedHashSet<>(); + private Set neededArtifactTypes = new LinkedHashSet<>(); + private boolean hasNeedsDirective; + private boolean metadataRegion; + private boolean tagRegion; + private boolean importingScenario; + + GherkinImporter(final InputFile file, final ImportEventListener listener) { + this.file = file; + this.listener = listener; + this.coverageTagParser = CoverageTagParser.create(null, file, listener); + this.delayedCoverageTagParser = CoverageTagParser.create(null, file, this.delayedCoverageEvents); + } + + @Override + public void runImport() { + LineReader.create(this.file).readLines(this); + endScenario(); + } + + @Override + public void readLine(final int lineNumber, final String line) { + if (line.trim().startsWith("#")) { + getCoverageTagParser().readLine(lineNumber, line); + } + final Matcher scenario = SCENARIO.matcher(line); + if (scenario.matches()) { + endScenario(); + beginScenario(lineNumber, scenario.group(1)); + return; + } + if (BOUNDARY.matcher(line).find()) { + endScenario(); + clearMetadata(); + return; + } + if (this.importingScenario) { + if (!line.trim().startsWith("#")) { + this.listener.appendDescription(line + System.lineSeparator()); + } + return; + } + readMetadata(lineNumber, line); + } + + private LineConsumer getCoverageTagParser() { + return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; + } + + private void readMetadata(final int lineNumber, final String line) { + if (line.trim().startsWith("@")) { + readTagRegion(lineNumber, line.trim()); + return; + } + final Matcher directive = DIRECTIVE.matcher(line); + if (this.metadataRegion && directive.matches()) { + readDirective(lineNumber, directive.group(1), directive.group(2)); + return; + } + if (!line.trim().startsWith("#")) { + clearMetadata(); + } + } + + private void readTagRegion(final int lineNumber, final String tags) { + if (!this.tagRegion) { + clearMetadata(); + } + this.metadataRegion = true; + this.tagRegion = true; + final Matcher matcher = ID_TAG.matcher(tags); + while (matcher.find()) { + if (this.pendingId != null) { + fail(lineNumber, "multiple @id tags before a scenario"); + } + this.pendingId = parseId(lineNumber, matcher.group(1)); + } + } + + private void readDirective(final int lineNumber, final String name, final String values) { + this.tagRegion = false; + if (this.pendingId == null) { + fail(lineNumber, name + " directive requires exactly one preceding @id tag"); + } + final boolean covers = "Covers".equals(name); + if (!covers && this.hasNeedsDirective) { + fail(lineNumber, "repeated " + name + " directive"); + } + final String[] entries = values.trim().split(",", -1); + if (entries.length == 0 || values.trim().isEmpty()) { + fail(lineNumber, name + " directive requires a non-empty list"); + } + for (final String entry : entries) { + if (entry.trim().isEmpty()) { + fail(lineNumber, name + " directive contains an empty value"); + } + if (covers) { + final SpecificationItemId id = parseId(lineNumber, entry.trim()); + if (!this.coveredIds.add(id)) { + fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); + } + } else if (!entry.trim().matches("\\p{Alpha}+")) { + fail(lineNumber, "Needs directive contains invalid artifact type '" + entry.trim() + "'"); + } else if (!this.neededArtifactTypes.add(entry.trim())) { + fail(lineNumber, "Needs directive contains duplicate value '" + entry.trim() + "'"); + } + } + if (!covers) { + this.hasNeedsDirective = true; + } + } + + private SpecificationItemId parseId(final int lineNumber, final String value) { + if (!SpecificationItemId.ID_PATTERN.matcher(value).matches()) { + fail(lineNumber, "invalid specification item ID '" + value + "'"); + } + return SpecificationItemId.parseId(value); + } + + private void beginScenario(final int lineNumber, final String title) { + if (this.pendingId == null) { + clearMetadata(); + return; + } + if (!this.importedIds.add(this.pendingId)) { + fail(lineNumber, "duplicate Gherkin ID '" + this.pendingId + "'"); + } + this.listener.beginSpecificationItem(); + this.listener.setLocation(this.file.getPath(), lineNumber); + this.listener.setId(this.pendingId); + this.listener.setTitle(title); + this.coveredIds.forEach(this.listener::addCoveredId); + this.neededArtifactTypes.forEach(this.listener::addNeededArtifactType); + this.importingScenario = true; + clearMetadata(); + } + + private void endScenario() { + if (this.importingScenario) { + this.listener.endSpecificationItem(); + this.importingScenario = false; + this.delayedCoverageEvents.replay(this.listener); + } + } + + private void clearMetadata() { + this.pendingId = null; + this.coveredIds = new LinkedHashSet<>(); + this.neededArtifactTypes = new LinkedHashSet<>(); + this.hasNeedsDirective = false; + this.metadataRegion = false; + this.tagRegion = false; + } + + private void fail(final int lineNumber, final String reason) { + throw new IllegalArgumentException(this.file.getPath() + ":" + lineNumber + ": " + reason); + } + + /** Buffers legacy coverage-tag events until the current scenario has ended. */ + private static final class EventBuffer implements ImportEventListener { + private final List> events = new ArrayList<>(); + + @Override + public void beginSpecificationItem() { + this.events.add(ImportEventListener::beginSpecificationItem); + } + + @Override + public void setId(final SpecificationItemId id) { + this.events.add(listener -> listener.setId(id)); + } + + @Override + public void setTitle(final String title) { + this.events.add(listener -> listener.setTitle(title)); + } + + @Override + public void setStatus(final ItemStatus status) { + this.events.add(listener -> listener.setStatus(status)); + } + + @Override + public void appendDescription(final String fragment) { + this.events.add(listener -> listener.appendDescription(fragment)); + } + + @Override + public void appendRationale(final String fragment) { + this.events.add(listener -> listener.appendRationale(fragment)); + } + + @Override + public void appendComment(final String fragment) { + this.events.add(listener -> listener.appendComment(fragment)); + } + + @Override + public void addCoveredId(final SpecificationItemId id) { + this.events.add(listener -> listener.addCoveredId(id)); + } + + @Override + public void addDependsOnId(final SpecificationItemId id) { + this.events.add(listener -> listener.addDependsOnId(id)); + } + + @Override + public void addNeededArtifactType(final String artifactType) { + this.events.add(listener -> listener.addNeededArtifactType(artifactType)); + } + + @Override + public void addTag(final String tag) { + this.events.add(listener -> listener.addTag(tag)); + } + + @Override + public void setLocation(final String path, final int line) { + this.events.add(listener -> listener.setLocation(path, line)); + } + + @Override + public void endSpecificationItem() { + this.events.add(ImportEventListener::endSpecificationItem); + } + + @Override + public void setLocation(final Location location) { + this.events.add(listener -> listener.setLocation(location)); + } + + @Override + public void setForwards(final boolean forwards) { + this.events.add(listener -> listener.setForwards(forwards)); + } + + void replay(final ImportEventListener listener) { + this.events.forEach(event -> event.accept(listener)); + this.events.clear(); + } + } +} diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java new file mode 100644 index 000000000..f67805f4b --- /dev/null +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java @@ -0,0 +1,23 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import org.itsallcode.openfasttrace.api.importer.*; +import org.itsallcode.openfasttrace.api.importer.input.InputFile; + +/** Factory for importing OpenFastTrace specifications from Gherkin files. */ +// [impl->dsn~gherkin.importer-selection~1] +public class GherkinImporterFactory extends AbstractRegexMatchingImporterFactory { + /** Create a factory that accepts Gherkin {@code .feature} files. */ + public GherkinImporterFactory() { + super("(?i).*\\.feature"); + } + + @Override + public int getPriority() { + return 9000; + } + + @Override + public Importer createImporter(final InputFile file, final ImportEventListener listener) { + return new GherkinImporter(file, listener); + } +} diff --git a/importer/gherkin/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory b/importer/gherkin/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory new file mode 100644 index 000000000..77df55442 --- /dev/null +++ b/importer/gherkin/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory @@ -0,0 +1 @@ +org.itsallcode.openfasttrace.importer.gherkin.GherkinImporterFactory 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 new file mode 100644 index 000000000..c786aae73 --- /dev/null +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterTest.java @@ -0,0 +1,116 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Path; +import java.util.List; + +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.importer.ImporterException; +import org.itsallcode.openfasttrace.api.importer.input.InputFile; +import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; +import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; +import org.junit.jupiter.api.Test; + +class GherkinImporterTest { + private static final GherkinImporterFactory FACTORY = new GherkinImporterFactory(); + + // [utest->dsn~gherkin.streaming-import~1] + @Test + void testImportsScenarioOutlineWithScopedMetadataAndSteps() { + final String source = """ + @smoke + @id:scn~account-login~1 + # Covers: req~login~1 + # Needs: dsn, itest + Scenario Outline: Login works + Given a registered user + When they log in + Then access is granted + Examples: + | user | + | Ada | + """; + + final List items = importText(source); + + assertThat(items, contains( + hasProperty("id", hasToString("scn~account-login~1")))); + final SpecificationItem item = items.get(0); + assertThat(item.getTitle(), is("Login works")); + assertThat(item.getLocation().getLine(), is(5)); + assertThat(item.getDescription(), is("Given a registered user\n When they log in\n Then access is granted")); + assertThat(item.getCoveredIds(), contains(hasToString("req~login~1"))); + assertThat(item.getNeedsArtifactTypes(), containsInAnyOrder("dsn", "itest")); + } + + // [utest->dsn~gherkin.importer-selection~1] + @Test + void testFactorySupportsFeatureFilesWithHigherPrecedenceThanTagImporter() { + final InputFile file = StreamInput.forReader(Path.of("specification.feature"), + new java.io.BufferedReader(new java.io.StringReader(""))); + + assertThat(FACTORY.supportsFile(file), is(true)); + assertThat(FACTORY.getPriority(), is(9000)); + } + + // [utest->dsn~gherkin.streaming-import~1] + @Test + void testIgnoresScenarioWithoutOftMetadata() { + final List items = importText(""" + Feature: login + Scenario: ordinary scenario + Given nothing + """); + + assertThat(items, is(empty())); + } + + // [utest->dsn~gherkin.metadata-validation~1] + @Test + void testImportsMultipleCoversDirectives() { + final String source = """ + @id:scn~account-login~1 + # Covers: req~login~1 + # Covers: req~security~1 + Scenario: Login + """; + + final List items = importText(source); + + assertThat(items.get(0).getCoveredIds(), contains( + hasToString("req~login~1"), hasToString("req~security~1"))); + } + + // [utest->dsn~gherkin.metadata-validation~1] + @Test + void testRejectsDirectiveWithoutId() { + final String source = """ + @ordinary + # Needs: dsn + Scenario: Login + """; + + final ImporterException exception = assertThrows(ImporterException.class, () -> importText(source)); + + assertThat(exception.getMessage(), hasToString(org.hamcrest.Matchers.containsString("requires exactly one"))); + } + + // [utest->dsn~gherkin.comment-coverage-tags~1] + @Test + void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { + final List items = importText(""" + # [impl~gherkin-comment~1 -> dsn~gherkin~1] + Scenario: ordinary + Given [impl~gherkin-executable~1 -> dsn~gherkin~1] + """); + + assertThat(items, contains(hasProperty("id", hasToString("impl~gherkin-comment~1")))); + } + + private static List importText(final String source) { + return ImportAssertions.runImporterOnText(Path.of("specification.feature"), source, FACTORY); + } +} diff --git a/importer/tag/src/main/java/org/itsallcode/openfasttrace/importer/tag/TagImporterFactory.java b/importer/tag/src/main/java/org/itsallcode/openfasttrace/importer/tag/TagImporterFactory.java index 775694084..7d2585665 100644 --- a/importer/tag/src/main/java/org/itsallcode/openfasttrace/importer/tag/TagImporterFactory.java +++ b/importer/tag/src/main/java/org/itsallcode/openfasttrace/importer/tag/TagImporterFactory.java @@ -21,7 +21,6 @@ public class TagImporterFactory extends AbstractImporterFactory "dox", // Doxygen "c#", "cs", // C# "cfg", "conf", "ini", // configuration files - "feature", // Gherkin feature files "go", // Go "groovy", // Groovy "json", "htm", "html", "xhtml", "xml", "yaml", "yml", // markup languages diff --git a/oft-self-trace.sh b/oft-self-trace.sh index d6b5b5471..a35633efb 100755 --- a/oft-self-trace.sh +++ b/oft-self-trace.sh @@ -22,6 +22,7 @@ if $oft_script trace \ "$base_dir/importer/specobject/src" \ "$base_dir/importer/zip/src" \ "$base_dir/importer/tag-importer-common/src" \ + "$base_dir/importer/gherkin/src" \ "$base_dir/importer/tag/src" \ "$base_dir/core/src/main" \ "$base_dir/core/src/test/java" \ diff --git a/parent/pom.xml b/parent/pom.xml index 2648a3eff..a4863bfe5 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -10,7 +10,7 @@ Free requirement tracking suite https://github.com/itsallcode/openfasttrace - 4.6.0 + 4.7.0 17 6.1.0-M1 6.1.1 @@ -147,6 +147,12 @@ ${revision} compile + + org.itsallcode.openfasttrace + openfasttrace-importer-gherkin + ${revision} + compile + org.itsallcode.openfasttrace openfasttrace-importer-tag-importer-common @@ -615,7 +621,7 @@ - org.eclipse.m2e diff --git a/pom.xml b/pom.xml index 3b7d3fca3..14e96d6b3 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ importer/restructuredtext importer/specobject importer/tag-importer-common + importer/gherkin importer/tag importer/zip reporter/plaintext diff --git a/product/pom.xml b/product/pom.xml index 58214bd1f..23d7f68fc 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -37,6 +37,10 @@ org.itsallcode.openfasttrace openfasttrace-importer-specobject + + org.itsallcode.openfasttrace + openfasttrace-importer-gherkin + org.itsallcode.openfasttrace openfasttrace-importer-tag diff --git a/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java b/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java index 33368f051..381bf5d63 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java @@ -69,7 +69,7 @@ private static ReporterFactoryLoader createReporterLoader() @ParameterizedTest @CsvSource( - { "md", "oreqm", "java", "zip" }) + { "md", "oreqm", "java", "feature", "zip" }) void importerAvailable(final String suffix) { final InputFile file = RealFileInput.forPath(Paths.get("file." + suffix)); diff --git a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java index 3144600ae..98ad3c447 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java @@ -14,6 +14,7 @@ import org.itsallcode.openfasttrace.api.report.ReporterFactory; import org.itsallcode.openfasttrace.exporter.specobject.SpecobjectExporterFactory; import org.itsallcode.openfasttrace.importer.markdown.MarkdownImporterFactory; +import org.itsallcode.openfasttrace.importer.gherkin.GherkinImporterFactory; import org.itsallcode.openfasttrace.importer.restructuredtext.RestructuredTextImporterFactory; import org.itsallcode.openfasttrace.importer.specobject.SpecobjectImporterFactory; import org.itsallcode.openfasttrace.importer.tag.TagImporterFactory; @@ -48,11 +49,12 @@ void testImporterFactoriesRegistered() final ImporterContext context = new ImporterContext(null); final List services = getRegisteredServices(ImporterFactory.class, context); - assertThat(services, hasSize(5)); + assertThat(services, hasSize(6)); assertThat(services, containsInAnyOrder( instanceOf(MarkdownImporterFactory.class), instanceOf(RestructuredTextImporterFactory.class), instanceOf(SpecobjectImporterFactory.class), + instanceOf(GherkinImporterFactory.class), instanceOf(TagImporterFactory.class), instanceOf(ZipFileImporterFactory.class))); for (final ImporterFactory importerFactory : services) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java index ae55fca3a..926a70356 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java @@ -42,4 +42,26 @@ void testFallBackToTagImporterWhenXmlIsNotSpecObjectFile(@TempDir Path tempDir) hasProperty("id", hasToString(startsWith("impl~foobar"))) )); } + + // [itest->dsn~gherkin.importer-selection~1] + @Test + void testSelectsGherkinImporterBeforeTagImporter(@TempDir final Path tempDir) throws IOException { + final Oft oft = Oft.create(); + Files.writeString(tempDir.resolve("login.feature"), """ + @id:scn~login~1 + Scenario: Login + # [impl~login~1 -> dsn~login~1] + Given [impl~must-not-be-imported~1 -> dsn~login~1] + """); + final ImportSettings settings = ImportSettings.builder() + .addInputs(tempDir) + .filter(FilterSettings.builder().build()) + .build(); + + final List items = oft.importItems(settings); + + assertThat(items, containsInAnyOrder( + hasProperty("id", hasToString("scn~login~1")), + hasProperty("id", hasToString("impl~login~1")))); + } } From 49394ccaa6d6fd55cb478946ef371b9ecc9c976b Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 14:11:22 +0200 Subject: [PATCH 02/17] Fix self-trace and tests --- doc/spec/system_requirements.md | 39 +++++++++---------- .../importer/gherkin/GherkinImporterTest.java | 12 +++--- .../importer/tag/TestTagImporterFactory.java | 2 +- .../importer/ImporterFactoryLoaderIT.java | 5 +-- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index 8cc5f2b75..9ec3f6ab7 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -119,6 +119,12 @@ Needs: req OFT imports coverage tags from source code files. +Rationale: + +Coverage tags indicate parts of the source code that implements a certain requirement. + +Needs: req + ### Gherkin Import `feat~gherkin-import~1` @@ -156,16 +162,9 @@ OFT imports legacy coverage tags from comments in `.feature` files, but does not Covers: * [feat~gherkin-import~1](#gherkin-import) -* [feat~coverage-tag-import~1](#coverage-tag-import) Needs: dsn -Rationale: - -Coverage tags indicate parts of the source code that implements a certain requirement. - -Needs: req - ### ReqM2 Export `feat~reqm2-export~1` @@ -567,7 +566,7 @@ Covers: Needs: dsn -#### Include Items That Don't Have Tags Or Where at Least One Tag Matches +#### Include Items That Don't Have Tags Or Where at Least One Tag Matches `req~include-items-that-do-not-have-tags-or-where-at-least-one-tag-matches~1` OFT gives users the option to include only specification items that either do not have tags or have at least one tag from a configurable set of tags during processing. @@ -589,7 +588,7 @@ Reports are the main way to find out if a projects requirements are covered prop Users can choose to display the requirement origin (e.g. file and line number) in reports: * In the body of a specification item -* For each link to a specification item +* For each link to a specification item Rationale: @@ -847,29 +846,29 @@ Covers: Needs: dsn #### Common - + ##### CLI Help `req~cli.help~1` - + `help`, `-h` and `--help` show a short help text with command line usage. - + Covers: - + * [feat~command-line-interface~1](#command-line-interface) - + Needs: dsn - + ##### CLI Version `req~cli.version~1` - + `help`, `-h` and `--help` show the version of OFT. - + Covers: - + * [feat~command-line-interface~1](#command-line-interface) - + Needs: dsn - + ##### Input Selection `req~cli.input-selection~1` 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 c786aae73..dda6c3cdd 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 @@ -68,7 +68,7 @@ void testIgnoresScenarioWithoutOftMetadata() { assertThat(items, is(empty())); } - // [utest->dsn~gherkin.metadata-validation~1] + // [utest->dsn~gherkin.streaming-import~1] @Test void testImportsMultipleCoversDirectives() { final String source = """ @@ -84,7 +84,7 @@ void testImportsMultipleCoversDirectives() { hasToString("req~login~1"), hasToString("req~security~1"))); } - // [utest->dsn~gherkin.metadata-validation~1] + // [utest->dsn~gherkin.streaming-import~1] @Test void testRejectsDirectiveWithoutId() { final String source = """ @@ -101,11 +101,11 @@ void testRejectsDirectiveWithoutId() { // [utest->dsn~gherkin.comment-coverage-tags~1] @Test void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { - final List items = importText(""" - # [impl~gherkin-comment~1 -> dsn~gherkin~1] + final String source = """ + # [""" + "impl~gherkin-comment~1 -> dsn~gherkin~1]\n" + """ Scenario: ordinary - Given [impl~gherkin-executable~1 -> dsn~gherkin~1] - """); + Given [""" + "impl~gherkin-executable~1 -> dsn~gherkin~1]\n"; + final List items = importText(source); assertThat(items, contains(hasProperty("id", hasToString("impl~gherkin-comment~1")))); } diff --git a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterFactory.java b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterFactory.java index b25305b31..5bef2c5fd 100644 --- a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterFactory.java +++ b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporterFactory.java @@ -26,7 +26,7 @@ protected List getSupportedFilenames() { return asList("file.java", "FILE.java", "file.md.java", "file.ads", "file.adb", "foo.bash", "foo.bar.bash", "foo.bat", "foo.java", "foo.c", "foo.C", "foo.c++", "foo.c#", "foo.cc", "foo.cfg", - "foo.conf", "foo.cpp", "foo.dox", "foo.cs", "foo.feature", "foo.fxml", "foo.go", + "foo.conf", "foo.cpp", "foo.dox", "foo.cs", "foo.fxml", "foo.go", "foo.groovy", "foo.h", "foo.H", "foo.hh", "foo.h++", "foo.htm", "foo.html", "foo.ini", "foo.js", "foo.kt", "foo.kts", "foo.mjs", "foo.cjs", "foo.ejs", "foo.ts", "foo.json", diff --git a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java index 926a70356..11bf4c19c 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java @@ -50,9 +50,8 @@ void testSelectsGherkinImporterBeforeTagImporter(@TempDir final Path tempDir) th Files.writeString(tempDir.resolve("login.feature"), """ @id:scn~login~1 Scenario: Login - # [impl~login~1 -> dsn~login~1] - Given [impl~must-not-be-imported~1 -> dsn~login~1] - """); + # [""" + "impl~login~1 -> dsn~login~1]\n" + """ + Given [""" + "impl~must-not-be-imported~1 -> dsn~login~1]\n"); final ImportSettings settings = ImportSettings.builder() .addInputs(tempDir) .filter(FilterSettings.builder().build()) From 4dbb227f49f1510329fd321417451fa3583d4ca5 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 14:46:08 +0200 Subject: [PATCH 03/17] Fix sonar findings --- .../importer/gherkin/GherkinImporter.java | 95 +++++++----- .../importer/gherkin/GherkinImporterTest.java | 144 +++++++++++++++++- .../importer/ImporterFactoryLoaderIT.java | 6 +- 3 files changed, 204 insertions(+), 41 deletions(-) diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java index 501039d01..23eac4119 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java @@ -17,16 +17,19 @@ // [impl->dsn~gherkin.streaming-import~1] // [impl->dsn~gherkin.comment-coverage-tags~1] final class GherkinImporter implements Importer, LineConsumer { - private static final Pattern ID_TAG = Pattern.compile("@id:(\\S+)"); - private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:\\s*(.*)$"); + private static final int UNICODE = Pattern.UNICODE_CHARACTER_CLASS; + private static final Pattern ID_TAG = Pattern.compile("@id:([^\\s]+)", UNICODE); + private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:(.*)$", UNICODE); private static final Pattern BOUNDARY = Pattern - .compile("^\\s*(?:Scenario(?: Outline)?|Feature|Rule|Background|Examples):"); - private static final Pattern DIRECTIVE = Pattern.compile("^\\s*#\\s*(Covers|Needs):(.*)$"); + .compile("^\\s*(?:Scenario(?: Outline)?|Feature|Rule|Background|Examples):", UNICODE); + private static final Pattern DIRECTIVE = Pattern.compile("^\\s*#\\s*(Covers|Needs):(.*)$", UNICODE); + private static final Pattern ARTIFACT_TYPE = Pattern.compile("\\p{IsAlphabetic}+"); private final InputFile file; private final ImportEventListener listener; private final LineConsumer coverageTagParser; - // Legacy tags emit complete items and therefore must not interleave with an open scenario item. + // Legacy tags emit complete items and therefore must not interleave with an + // open scenario item. private final LineConsumer delayedCoverageTagParser; private final EventBuffer delayedCoverageEvents = new EventBuffer(); private final Set importedIds = new LinkedHashSet<>(); @@ -59,7 +62,7 @@ public void readLine(final int lineNumber, final String line) { final Matcher scenario = SCENARIO.matcher(line); if (scenario.matches()) { endScenario(); - beginScenario(lineNumber, scenario.group(1)); + beginScenario(lineNumber, scenario.group(1).trim()); return; } if (BOUNDARY.matcher(line).find()) { @@ -119,28 +122,50 @@ private void readDirective(final int lineNumber, final String name, final String if (!covers && this.hasNeedsDirective) { fail(lineNumber, "repeated " + name + " directive"); } - final String[] entries = values.trim().split(",", -1); - if (entries.length == 0 || values.trim().isEmpty()) { + final String[] entries = splitValues(lineNumber, name, values); + if (covers) { + readCoveredIds(lineNumber, entries); + return; + } + readNeededArtifactTypes(lineNumber, entries); + this.hasNeedsDirective = true; + } + + private String[] splitValues(final int lineNumber, final String name, final String values) { + if (values.trim().isEmpty()) { fail(lineNumber, name + " directive requires a non-empty list"); } + return values.trim().split(",", -1); + } + + private void readCoveredIds(final int lineNumber, final String[] entries) { for (final String entry : entries) { - if (entry.trim().isEmpty()) { - fail(lineNumber, name + " directive contains an empty value"); + final String value = requireValue(lineNumber, "Covers", entry); + final SpecificationItemId id = parseId(lineNumber, value); + if (!this.coveredIds.add(id)) { + fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); } - if (covers) { - final SpecificationItemId id = parseId(lineNumber, entry.trim()); - if (!this.coveredIds.add(id)) { - fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); - } - } else if (!entry.trim().matches("\\p{Alpha}+")) { - fail(lineNumber, "Needs directive contains invalid artifact type '" + entry.trim() + "'"); - } else if (!this.neededArtifactTypes.add(entry.trim())) { - fail(lineNumber, "Needs directive contains duplicate value '" + entry.trim() + "'"); + } + } + + private void readNeededArtifactTypes(final int lineNumber, final String[] entries) { + for (final String entry : entries) { + final String value = requireValue(lineNumber, "Needs", entry); + if (!ARTIFACT_TYPE.matcher(value).matches()) { + fail(lineNumber, "Needs directive contains invalid artifact type '" + value + "'"); + } + if (!this.neededArtifactTypes.add(value)) { + fail(lineNumber, "Needs directive contains duplicate value '" + value + "'"); } } - if (!covers) { - this.hasNeedsDirective = true; + } + + private String requireValue(final int lineNumber, final String name, final String entry) { + final String value = entry.trim(); + if (value.isEmpty()) { + fail(lineNumber, name + " directive contains an empty value"); } + return value; } private SpecificationItemId parseId(final int lineNumber, final String value) { @@ -190,7 +215,7 @@ private void fail(final int lineNumber, final String reason) { } /** Buffers legacy coverage-tag events until the current scenario has ended. */ - private static final class EventBuffer implements ImportEventListener { + static final class EventBuffer implements ImportEventListener { private final List> events = new ArrayList<>(); @Override @@ -200,57 +225,57 @@ public void beginSpecificationItem() { @Override public void setId(final SpecificationItemId id) { - this.events.add(listener -> listener.setId(id)); + this.events.add(target -> target.setId(id)); } @Override public void setTitle(final String title) { - this.events.add(listener -> listener.setTitle(title)); + this.events.add(target -> target.setTitle(title)); } @Override public void setStatus(final ItemStatus status) { - this.events.add(listener -> listener.setStatus(status)); + this.events.add(target -> target.setStatus(status)); } @Override public void appendDescription(final String fragment) { - this.events.add(listener -> listener.appendDescription(fragment)); + this.events.add(target -> target.appendDescription(fragment)); } @Override public void appendRationale(final String fragment) { - this.events.add(listener -> listener.appendRationale(fragment)); + this.events.add(target -> target.appendRationale(fragment)); } @Override public void appendComment(final String fragment) { - this.events.add(listener -> listener.appendComment(fragment)); + this.events.add(target -> target.appendComment(fragment)); } @Override public void addCoveredId(final SpecificationItemId id) { - this.events.add(listener -> listener.addCoveredId(id)); + this.events.add(target -> target.addCoveredId(id)); } @Override public void addDependsOnId(final SpecificationItemId id) { - this.events.add(listener -> listener.addDependsOnId(id)); + this.events.add(target -> target.addDependsOnId(id)); } @Override public void addNeededArtifactType(final String artifactType) { - this.events.add(listener -> listener.addNeededArtifactType(artifactType)); + this.events.add(target -> target.addNeededArtifactType(artifactType)); } @Override public void addTag(final String tag) { - this.events.add(listener -> listener.addTag(tag)); + this.events.add(target -> target.addTag(tag)); } @Override public void setLocation(final String path, final int line) { - this.events.add(listener -> listener.setLocation(path, line)); + this.events.add(target -> target.setLocation(path, line)); } @Override @@ -260,12 +285,12 @@ public void endSpecificationItem() { @Override public void setLocation(final Location location) { - this.events.add(listener -> listener.setLocation(location)); + this.events.add(target -> target.setLocation(location)); } @Override public void setForwards(final boolean forwards) { - this.events.add(listener -> listener.setForwards(forwards)); + this.events.add(target -> target.setForwards(forwards)); } void replay(final ImportEventListener listener) { 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 dda6c3cdd..51daa2683 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 @@ -3,16 +3,23 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.nio.file.Path; import java.util.List; +import java.util.stream.Stream; -import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.ImporterException; import org.itsallcode.openfasttrace.api.importer.input.InputFile; import org.itsallcode.openfasttrace.testutil.importer.ImportAssertions; import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput; 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 GherkinImporterTest { private static final GherkinImporterFactory FACTORY = new GherkinImporterFactory(); @@ -98,16 +105,145 @@ void testRejectsDirectiveWithoutId() { assertThat(exception.getMessage(), hasToString(org.hamcrest.Matchers.containsString("requires exactly one"))); } + // [utest->dsn~gherkin.streaming-import~1] + @Test + void testIgnoresDirectivesOutsideAnIdMetadataRegion() { + final List items = importText(""" + # Covers: req~login~1 + Scenario: Login + """); + + assertThat(items, is(empty())); + } + + // [utest->dsn~gherkin.streaming-import~1] + @Test + void testKeepsMetadataWhenAnUnrelatedCommentPrecedesTheScenario() { + final List items = importText(""" + @id:scn~login~1 + # A human-readable comment + Scenario: Login + """); + + assertThat(items, contains(hasProperty("id", hasToString("scn~login~1")))); + } + + // [utest->dsn~gherkin.streaming-import~1] + @ParameterizedTest + @MethodSource("invalidMetadata") + void testRejectsInvalidMetadata(final String source, final String reason) { + final ImporterException exception = assertThrows(ImporterException.class, () -> importText(source)); + + assertThat(exception.getMessage(), containsString(reason)); + } + + private static Stream invalidMetadata() { + return Stream.of( + Arguments.of(""" + @id:invalid + Scenario: Login + """, "invalid specification item ID"), + Arguments.of(""" + @id:scn~login~1 + @id:scn~another-login~1 + Scenario: Login + """, "multiple @id tags"), + Arguments.of(""" + @id:scn~login~1 + # Needs: dsn + # Needs: itest + Scenario: Login + """, "repeated Needs directive"), + Arguments.of(""" + @id:scn~login~1 + # Covers: + Scenario: Login + """, "requires a non-empty list"), + Arguments.of(""" + @id:scn~login~1 + # Covers: req~login~1, + Scenario: Login + """, "contains an empty value"), + Arguments.of(""" + @id:scn~login~1 + # Covers: req~login~1, req~login~1 + Scenario: Login + """, "contains duplicate value"), + Arguments.of(""" + @id:scn~login~1 + # Needs: invalid-type + Scenario: Login + """, "invalid artifact type"), + Arguments.of(""" + @id:scn~login~1 + # Needs: dsn, dsn + Scenario: Login + """, "contains duplicate value"), + Arguments.of(""" + @id:scn~login~1 + Scenario: Login + Feature: Another feature + @id:scn~login~1 + Scenario: Login again + """, "duplicate Gherkin ID")); + } + // [utest->dsn~gherkin.comment-coverage-tags~1] @Test void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { final String source = """ - # [""" + "impl~gherkin-comment~1 -> dsn~gherkin~1]\n" + """ + @id:scn~ordinary~1 Scenario: ordinary - Given [""" + "impl~gherkin-executable~1 -> dsn~gherkin~1]\n"; + # [%s] + Given [%s] + """.formatted("impl~gherkin-comment~1 -> dsn~gherkin~1", + "impl~gherkin-executable~1 -> dsn~gherkin~1"); final List items = importText(source); - assertThat(items, contains(hasProperty("id", hasToString("impl~gherkin-comment~1")))); + assertThat(items, containsInAnyOrder( + hasProperty("id", hasToString("scn~ordinary~1")), + hasProperty("id", hasToString("impl~gherkin-comment~1")))); + } + + @Test + void testReplaysAllBufferedEventTypes() { + final GherkinImporter.EventBuffer buffer = new GherkinImporter.EventBuffer(); + final ImportEventListener listener = mock(ImportEventListener.class); + final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); + final Location location = Location.create("file.feature", 2); + + buffer.beginSpecificationItem(); + buffer.setId(id); + buffer.setTitle("title"); + buffer.setStatus(ItemStatus.DRAFT); + buffer.appendDescription("description"); + buffer.appendRationale("rationale"); + buffer.appendComment("comment"); + buffer.addCoveredId(id); + buffer.addDependsOnId(id); + buffer.addNeededArtifactType("dsn"); + buffer.addTag("tag"); + buffer.setLocation("file.feature", 1); + buffer.setLocation(location); + buffer.setForwards(true); + buffer.endSpecificationItem(); + buffer.replay(listener); + + verify(listener).beginSpecificationItem(); + verify(listener).setId(id); + verify(listener).setTitle("title"); + verify(listener).setStatus(ItemStatus.DRAFT); + verify(listener).appendDescription("description"); + verify(listener).appendRationale("rationale"); + verify(listener).appendComment("comment"); + verify(listener).addCoveredId(id); + verify(listener).addDependsOnId(id); + verify(listener).addNeededArtifactType("dsn"); + verify(listener).addTag("tag"); + verify(listener).setLocation("file.feature", 1); + verify(listener).setLocation(location); + verify(listener).setForwards(true); + verify(listener).endSpecificationItem(); } private static List importText(final String source) { diff --git a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java index 11bf4c19c..aa50f1a68 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/importer/ImporterFactoryLoaderIT.java @@ -50,8 +50,10 @@ void testSelectsGherkinImporterBeforeTagImporter(@TempDir final Path tempDir) th Files.writeString(tempDir.resolve("login.feature"), """ @id:scn~login~1 Scenario: Login - # [""" + "impl~login~1 -> dsn~login~1]\n" + """ - Given [""" + "impl~must-not-be-imported~1 -> dsn~login~1]\n"); + # [%s] + Given [%s] + """.formatted("impl~login~1 -> dsn~login~1", + "impl~must-not-be-imported~1 -> dsn~login~1")); final ImportSettings settings = ImportSettings.builder() .addInputs(tempDir) .filter(FilterSettings.builder().build()) From b9ddc6be19d7a4038c3959f836f056844204b639 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 14:47:41 +0200 Subject: [PATCH 04/17] Fix test under windows --- .../openfasttrace/importer/gherkin/GherkinImporterTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 51daa2683..4499d3ca2 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 @@ -48,7 +48,8 @@ void testImportsScenarioOutlineWithScopedMetadataAndSteps() { final SpecificationItem item = items.get(0); assertThat(item.getTitle(), is("Login works")); assertThat(item.getLocation().getLine(), is(5)); - assertThat(item.getDescription(), is("Given a registered user\n When they log in\n Then access is granted")); + assertThat(item.getDescription(), is(String.join(System.lineSeparator(), + "Given a registered user", " When they log in", " Then access is granted"))); assertThat(item.getCoveredIds(), contains(hasToString("req~login~1"))); assertThat(item.getNeedsArtifactTypes(), containsInAnyOrder("dsn", "itest")); } From d96d46c1c4843259c4373ba1ea1a6c1931fb2756 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 16:55:36 +0200 Subject: [PATCH 05/17] Add changelog summary --- doc/changes/changes_4.7.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/changes/changes_4.7.0.md b/doc/changes/changes_4.7.0.md index 0358a2274..22c995bca 100644 --- a/doc/changes/changes_4.7.0.md +++ b/doc/changes/changes_4.7.0.md @@ -4,6 +4,11 @@ Code name: Gherkin Importer ## Summary +OpenFastTrace can now import traced specifications directly from Gherkin +`.feature` files. Annotate scenarios or scenario outlines with an OFT ID and +optional `Covers` and `Needs` metadata; existing coverage tags remain supported +when written in Gherkin comments. + ## New Features * #563: Added Gherkin `.feature` specification import for annotated scenarios and scenario outlines. From 37a263e9de9c13438d356bf7f562977a6d36e930 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 16:56:02 +0200 Subject: [PATCH 06/17] Extract classes from GherkinImporter --- .../importer/gherkin/EventBuffer.java | 93 ++++++ .../importer/gherkin/GherkinImporter.java | 287 +----------------- .../importer/gherkin/GherkinLineConsumer.java | 211 +++++++++++++ .../importer/gherkin/GherkinImporterTest.java | 58 ++-- 4 files changed, 331 insertions(+), 318 deletions(-) create mode 100644 importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java create mode 100644 importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java new file mode 100644 index 000000000..ef9d85b69 --- /dev/null +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java @@ -0,0 +1,93 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.importer.ImportEventListener; + +/** Buffers legacy coverage-tag events until the current scenario has ended. */ +final class EventBuffer implements ImportEventListener { + private final List> events = new ArrayList<>(); + + @Override + public void beginSpecificationItem() { + this.events.add(ImportEventListener::beginSpecificationItem); + } + + @Override + public void setId(final SpecificationItemId id) { + this.events.add(target -> target.setId(id)); + } + + @Override + public void setTitle(final String title) { + this.events.add(target -> target.setTitle(title)); + } + + @Override + public void setStatus(final ItemStatus status) { + this.events.add(target -> target.setStatus(status)); + } + + @Override + public void appendDescription(final String fragment) { + this.events.add(target -> target.appendDescription(fragment)); + } + + @Override + public void appendRationale(final String fragment) { + this.events.add(target -> target.appendRationale(fragment)); + } + + @Override + public void appendComment(final String fragment) { + this.events.add(target -> target.appendComment(fragment)); + } + + @Override + public void addCoveredId(final SpecificationItemId id) { + this.events.add(target -> target.addCoveredId(id)); + } + + @Override + public void addDependsOnId(final SpecificationItemId id) { + this.events.add(target -> target.addDependsOnId(id)); + } + + @Override + public void addNeededArtifactType(final String artifactType) { + this.events.add(target -> target.addNeededArtifactType(artifactType)); + } + + @Override + public void addTag(final String tag) { + this.events.add(target -> target.addTag(tag)); + } + + @Override + public void setLocation(final String path, final int line) { + this.events.add(target -> target.setLocation(path, line)); + } + + @Override + public void endSpecificationItem() { + this.events.add(ImportEventListener::endSpecificationItem); + } + + @Override + public void setLocation(final Location location) { + this.events.add(target -> target.setLocation(location)); + } + + @Override + public void setForwards(final boolean forwards) { + this.events.add(target -> target.setForwards(forwards)); + } + + void replay(final ImportEventListener listener) { + this.events.forEach(event -> event.accept(listener)); + this.events.clear(); + } +} diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java index 23eac4119..4695fd914 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java @@ -1,301 +1,24 @@ package org.itsallcode.openfasttrace.importer.gherkin; -import java.util.*; -import java.util.function.Consumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.Importer; import org.itsallcode.openfasttrace.api.importer.input.InputFile; -import org.itsallcode.openfasttrace.importer.tag.common.CoverageTagParser; import org.itsallcode.openfasttrace.importer.tag.common.LineReader; -import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer; /** Imports annotated Gherkin scenarios while streaming the input once. */ // [impl->dsn~gherkin.streaming-import~1] -// [impl->dsn~gherkin.comment-coverage-tags~1] -final class GherkinImporter implements Importer, LineConsumer { - private static final int UNICODE = Pattern.UNICODE_CHARACTER_CLASS; - private static final Pattern ID_TAG = Pattern.compile("@id:([^\\s]+)", UNICODE); - private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:(.*)$", UNICODE); - private static final Pattern BOUNDARY = Pattern - .compile("^\\s*(?:Scenario(?: Outline)?|Feature|Rule|Background|Examples):", UNICODE); - private static final Pattern DIRECTIVE = Pattern.compile("^\\s*#\\s*(Covers|Needs):(.*)$", UNICODE); - private static final Pattern ARTIFACT_TYPE = Pattern.compile("\\p{IsAlphabetic}+"); - +final class GherkinImporter implements Importer { private final InputFile file; - private final ImportEventListener listener; - private final LineConsumer coverageTagParser; - // Legacy tags emit complete items and therefore must not interleave with an - // open scenario item. - private final LineConsumer delayedCoverageTagParser; - private final EventBuffer delayedCoverageEvents = new EventBuffer(); - private final Set importedIds = new LinkedHashSet<>(); - private SpecificationItemId pendingId; - private Set coveredIds = new LinkedHashSet<>(); - private Set neededArtifactTypes = new LinkedHashSet<>(); - private boolean hasNeedsDirective; - private boolean metadataRegion; - private boolean tagRegion; - private boolean importingScenario; + private final GherkinLineConsumer lineConsumer; GherkinImporter(final InputFile file, final ImportEventListener listener) { this.file = file; - this.listener = listener; - this.coverageTagParser = CoverageTagParser.create(null, file, listener); - this.delayedCoverageTagParser = CoverageTagParser.create(null, file, this.delayedCoverageEvents); + this.lineConsumer = new GherkinLineConsumer(file, listener); } @Override public void runImport() { - LineReader.create(this.file).readLines(this); - endScenario(); - } - - @Override - public void readLine(final int lineNumber, final String line) { - if (line.trim().startsWith("#")) { - getCoverageTagParser().readLine(lineNumber, line); - } - final Matcher scenario = SCENARIO.matcher(line); - if (scenario.matches()) { - endScenario(); - beginScenario(lineNumber, scenario.group(1).trim()); - return; - } - if (BOUNDARY.matcher(line).find()) { - endScenario(); - clearMetadata(); - return; - } - if (this.importingScenario) { - if (!line.trim().startsWith("#")) { - this.listener.appendDescription(line + System.lineSeparator()); - } - return; - } - readMetadata(lineNumber, line); - } - - private LineConsumer getCoverageTagParser() { - return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; - } - - private void readMetadata(final int lineNumber, final String line) { - if (line.trim().startsWith("@")) { - readTagRegion(lineNumber, line.trim()); - return; - } - final Matcher directive = DIRECTIVE.matcher(line); - if (this.metadataRegion && directive.matches()) { - readDirective(lineNumber, directive.group(1), directive.group(2)); - return; - } - if (!line.trim().startsWith("#")) { - clearMetadata(); - } - } - - private void readTagRegion(final int lineNumber, final String tags) { - if (!this.tagRegion) { - clearMetadata(); - } - this.metadataRegion = true; - this.tagRegion = true; - final Matcher matcher = ID_TAG.matcher(tags); - while (matcher.find()) { - if (this.pendingId != null) { - fail(lineNumber, "multiple @id tags before a scenario"); - } - this.pendingId = parseId(lineNumber, matcher.group(1)); - } - } - - private void readDirective(final int lineNumber, final String name, final String values) { - this.tagRegion = false; - if (this.pendingId == null) { - fail(lineNumber, name + " directive requires exactly one preceding @id tag"); - } - final boolean covers = "Covers".equals(name); - if (!covers && this.hasNeedsDirective) { - fail(lineNumber, "repeated " + name + " directive"); - } - final String[] entries = splitValues(lineNumber, name, values); - if (covers) { - readCoveredIds(lineNumber, entries); - return; - } - readNeededArtifactTypes(lineNumber, entries); - this.hasNeedsDirective = true; - } - - private String[] splitValues(final int lineNumber, final String name, final String values) { - if (values.trim().isEmpty()) { - fail(lineNumber, name + " directive requires a non-empty list"); - } - return values.trim().split(",", -1); - } - - private void readCoveredIds(final int lineNumber, final String[] entries) { - for (final String entry : entries) { - final String value = requireValue(lineNumber, "Covers", entry); - final SpecificationItemId id = parseId(lineNumber, value); - if (!this.coveredIds.add(id)) { - fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); - } - } - } - - private void readNeededArtifactTypes(final int lineNumber, final String[] entries) { - for (final String entry : entries) { - final String value = requireValue(lineNumber, "Needs", entry); - if (!ARTIFACT_TYPE.matcher(value).matches()) { - fail(lineNumber, "Needs directive contains invalid artifact type '" + value + "'"); - } - if (!this.neededArtifactTypes.add(value)) { - fail(lineNumber, "Needs directive contains duplicate value '" + value + "'"); - } - } - } - - private String requireValue(final int lineNumber, final String name, final String entry) { - final String value = entry.trim(); - if (value.isEmpty()) { - fail(lineNumber, name + " directive contains an empty value"); - } - return value; - } - - private SpecificationItemId parseId(final int lineNumber, final String value) { - if (!SpecificationItemId.ID_PATTERN.matcher(value).matches()) { - fail(lineNumber, "invalid specification item ID '" + value + "'"); - } - return SpecificationItemId.parseId(value); - } - - private void beginScenario(final int lineNumber, final String title) { - if (this.pendingId == null) { - clearMetadata(); - return; - } - if (!this.importedIds.add(this.pendingId)) { - fail(lineNumber, "duplicate Gherkin ID '" + this.pendingId + "'"); - } - this.listener.beginSpecificationItem(); - this.listener.setLocation(this.file.getPath(), lineNumber); - this.listener.setId(this.pendingId); - this.listener.setTitle(title); - this.coveredIds.forEach(this.listener::addCoveredId); - this.neededArtifactTypes.forEach(this.listener::addNeededArtifactType); - this.importingScenario = true; - clearMetadata(); - } - - private void endScenario() { - if (this.importingScenario) { - this.listener.endSpecificationItem(); - this.importingScenario = false; - this.delayedCoverageEvents.replay(this.listener); - } - } - - private void clearMetadata() { - this.pendingId = null; - this.coveredIds = new LinkedHashSet<>(); - this.neededArtifactTypes = new LinkedHashSet<>(); - this.hasNeedsDirective = false; - this.metadataRegion = false; - this.tagRegion = false; - } - - private void fail(final int lineNumber, final String reason) { - throw new IllegalArgumentException(this.file.getPath() + ":" + lineNumber + ": " + reason); - } - - /** Buffers legacy coverage-tag events until the current scenario has ended. */ - static final class EventBuffer implements ImportEventListener { - private final List> events = new ArrayList<>(); - - @Override - public void beginSpecificationItem() { - this.events.add(ImportEventListener::beginSpecificationItem); - } - - @Override - public void setId(final SpecificationItemId id) { - this.events.add(target -> target.setId(id)); - } - - @Override - public void setTitle(final String title) { - this.events.add(target -> target.setTitle(title)); - } - - @Override - public void setStatus(final ItemStatus status) { - this.events.add(target -> target.setStatus(status)); - } - - @Override - public void appendDescription(final String fragment) { - this.events.add(target -> target.appendDescription(fragment)); - } - - @Override - public void appendRationale(final String fragment) { - this.events.add(target -> target.appendRationale(fragment)); - } - - @Override - public void appendComment(final String fragment) { - this.events.add(target -> target.appendComment(fragment)); - } - - @Override - public void addCoveredId(final SpecificationItemId id) { - this.events.add(target -> target.addCoveredId(id)); - } - - @Override - public void addDependsOnId(final SpecificationItemId id) { - this.events.add(target -> target.addDependsOnId(id)); - } - - @Override - public void addNeededArtifactType(final String artifactType) { - this.events.add(target -> target.addNeededArtifactType(artifactType)); - } - - @Override - public void addTag(final String tag) { - this.events.add(target -> target.addTag(tag)); - } - - @Override - public void setLocation(final String path, final int line) { - this.events.add(target -> target.setLocation(path, line)); - } - - @Override - public void endSpecificationItem() { - this.events.add(ImportEventListener::endSpecificationItem); - } - - @Override - public void setLocation(final Location location) { - this.events.add(target -> target.setLocation(location)); - } - - @Override - public void setForwards(final boolean forwards) { - this.events.add(target -> target.setForwards(forwards)); - } - - void replay(final ImportEventListener listener) { - this.events.forEach(event -> event.accept(listener)); - this.events.clear(); - } + LineReader.create(this.file).readLines(this.lineConsumer); + this.lineConsumer.finish(); } } 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 new file mode 100644 index 000000000..82ad5d055 --- /dev/null +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinLineConsumer.java @@ -0,0 +1,211 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.api.importer.ImportEventListener; +import org.itsallcode.openfasttrace.api.importer.input.InputFile; +import org.itsallcode.openfasttrace.importer.tag.common.CoverageTagParser; +import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer; + +/** Stateful parser for the Gherkin lines of one input file. */ +// [impl->dsn~gherkin.streaming-import~1] +// [impl->dsn~gherkin.comment-coverage-tags~1] +final class GherkinLineConsumer implements LineConsumer { + private static final int UNICODE = Pattern.UNICODE_CHARACTER_CLASS; + private static final Pattern ID_TAG = Pattern.compile("@id:([^\\s]+)", UNICODE); + private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:(.*)$", UNICODE); + private static final Pattern BOUNDARY = Pattern + .compile("^\\s*(?:Scenario(?: Outline)?|Feature|Rule|Background|Examples):", UNICODE); + private static final Pattern DIRECTIVE = Pattern.compile("^\\s*#\\s*(Covers|Needs):(.*)$", UNICODE); + private static final Pattern ARTIFACT_TYPE = Pattern.compile("\\p{IsAlphabetic}+"); + + private final InputFile file; + private final ImportEventListener listener; + private final LineConsumer coverageTagParser; + // Legacy tags emit complete items and must not interleave with an open scenario. + private final LineConsumer delayedCoverageTagParser; + private final EventBuffer delayedCoverageEvents = new EventBuffer(); + private final Set importedIds = new LinkedHashSet<>(); + private SpecificationItemId pendingId; + private Set coveredIds = new LinkedHashSet<>(); + private Set neededArtifactTypes = new LinkedHashSet<>(); + private boolean hasNeedsDirective; + private boolean metadataRegion; + private boolean tagRegion; + private boolean importingScenario; + + GherkinLineConsumer(final InputFile file, final ImportEventListener listener) { + this.file = file; + this.listener = listener; + this.coverageTagParser = CoverageTagParser.create(null, file, listener); + this.delayedCoverageTagParser = CoverageTagParser.create(null, file, this.delayedCoverageEvents); + } + + @Override + public void readLine(final int lineNumber, final String line) { + if (line.trim().startsWith("#")) { + getCoverageTagParser().readLine(lineNumber, line); + } + final Matcher scenario = SCENARIO.matcher(line); + if (scenario.matches()) { + endScenario(); + beginScenario(lineNumber, scenario.group(1).trim()); + return; + } + if (BOUNDARY.matcher(line).find()) { + endScenario(); + clearMetadata(); + return; + } + if (this.importingScenario) { + if (!line.trim().startsWith("#")) { + this.listener.appendDescription(line + System.lineSeparator()); + } + return; + } + readMetadata(lineNumber, line); + } + + void finish() { + endScenario(); + } + + private LineConsumer getCoverageTagParser() { + return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; + } + + private void readMetadata(final int lineNumber, final String line) { + if (line.trim().startsWith("@")) { + readTagRegion(lineNumber, line.trim()); + return; + } + final Matcher directive = DIRECTIVE.matcher(line); + if (this.metadataRegion && directive.matches()) { + readDirective(lineNumber, directive.group(1), directive.group(2)); + return; + } + if (!line.trim().startsWith("#")) { + clearMetadata(); + } + } + + private void readTagRegion(final int lineNumber, final String tags) { + if (!this.tagRegion) { + clearMetadata(); + } + this.metadataRegion = true; + this.tagRegion = true; + final Matcher matcher = ID_TAG.matcher(tags); + while (matcher.find()) { + if (this.pendingId != null) { + fail(lineNumber, "multiple @id tags before a scenario"); + } + this.pendingId = parseId(lineNumber, matcher.group(1)); + } + } + + private void readDirective(final int lineNumber, final String name, final String values) { + this.tagRegion = false; + if (this.pendingId == null) { + fail(lineNumber, name + " directive requires exactly one preceding @id tag"); + } + final boolean covers = "Covers".equals(name); + if (!covers && this.hasNeedsDirective) { + fail(lineNumber, "repeated " + name + " directive"); + } + final String[] entries = splitValues(lineNumber, name, values); + if (covers) { + readCoveredIds(lineNumber, entries); + return; + } + readNeededArtifactTypes(lineNumber, entries); + this.hasNeedsDirective = true; + } + + private String[] splitValues(final int lineNumber, final String name, final String values) { + if (values.trim().isEmpty()) { + fail(lineNumber, name + " directive requires a non-empty list"); + } + return values.trim().split(",", -1); + } + + private void readCoveredIds(final int lineNumber, final String[] entries) { + for (final String entry : entries) { + final String value = requireValue(lineNumber, "Covers", entry); + final SpecificationItemId id = parseId(lineNumber, value); + if (!this.coveredIds.add(id)) { + fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); + } + } + } + + private void readNeededArtifactTypes(final int lineNumber, final String[] entries) { + for (final String entry : entries) { + final String value = requireValue(lineNumber, "Needs", entry); + if (!ARTIFACT_TYPE.matcher(value).matches()) { + fail(lineNumber, "Needs directive contains invalid artifact type '" + value + "'"); + } + if (!this.neededArtifactTypes.add(value)) { + fail(lineNumber, "Needs directive contains duplicate value '" + value + "'"); + } + } + } + + private String requireValue(final int lineNumber, final String name, final String entry) { + final String value = entry.trim(); + if (value.isEmpty()) { + fail(lineNumber, name + " directive contains an empty value"); + } + return value; + } + + private SpecificationItemId parseId(final int lineNumber, final String value) { + if (!SpecificationItemId.ID_PATTERN.matcher(value).matches()) { + fail(lineNumber, "invalid specification item ID '" + value + "'"); + } + return SpecificationItemId.parseId(value); + } + + private void beginScenario(final int lineNumber, final String title) { + if (this.pendingId == null) { + clearMetadata(); + return; + } + if (!this.importedIds.add(this.pendingId)) { + fail(lineNumber, "duplicate Gherkin ID '" + this.pendingId + "'"); + } + this.listener.beginSpecificationItem(); + this.listener.setLocation(this.file.getPath(), lineNumber); + this.listener.setId(this.pendingId); + this.listener.setTitle(title); + this.coveredIds.forEach(this.listener::addCoveredId); + this.neededArtifactTypes.forEach(this.listener::addNeededArtifactType); + this.importingScenario = true; + clearMetadata(); + } + + private void endScenario() { + if (this.importingScenario) { + this.listener.endSpecificationItem(); + this.importingScenario = false; + this.delayedCoverageEvents.replay(this.listener); + } + } + + private void clearMetadata() { + this.pendingId = null; + this.coveredIds = new LinkedHashSet<>(); + this.neededArtifactTypes = new LinkedHashSet<>(); + this.hasNeedsDirective = false; + this.metadataRegion = false; + this.tagRegion = false; + } + + private void fail(final int lineNumber, final String reason) { + throw new IllegalArgumentException(this.file.getPath() + ":" + lineNumber + ": " + reason); + } +} 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 4499d3ca2..2666b3cbd 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 @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Stream; import org.itsallcode.openfasttrace.api.core.*; @@ -207,44 +208,29 @@ void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { } @Test - void testReplaysAllBufferedEventTypes() { - final GherkinImporter.EventBuffer buffer = new GherkinImporter.EventBuffer(); + void testEventBufferReplaysBeginItem() { assertEvent(EventBuffer::beginSpecificationItem, ImportEventListener::beginSpecificationItem); } + @Test void testEventBufferReplaysId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.setId(id), l -> verify(l).setId(id)); } + @Test void testEventBufferReplaysTitle() { assertEvent(b -> b.setTitle("title"), l -> verify(l).setTitle("title")); } + @Test void testEventBufferReplaysStatus() { assertEvent(b -> b.setStatus(ItemStatus.DRAFT), l -> verify(l).setStatus(ItemStatus.DRAFT)); } + @Test void testEventBufferReplaysDescription() { assertEvent(b -> b.appendDescription("description"), l -> verify(l).appendDescription("description")); } + @Test void testEventBufferReplaysRationale() { assertEvent(b -> b.appendRationale("rationale"), l -> verify(l).appendRationale("rationale")); } + @Test void testEventBufferReplaysComment() { assertEvent(b -> b.appendComment("comment"), l -> verify(l).appendComment("comment")); } + @Test void testEventBufferReplaysCoveredId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.addCoveredId(id), l -> verify(l).addCoveredId(id)); } + @Test void testEventBufferReplaysDependencyId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.addDependsOnId(id), l -> verify(l).addDependsOnId(id)); } + @Test void testEventBufferReplaysNeededArtifactType() { assertEvent(b -> b.addNeededArtifactType("dsn"), l -> verify(l).addNeededArtifactType("dsn")); } + @Test void testEventBufferReplaysTag() { assertEvent(b -> b.addTag("tag"), l -> verify(l).addTag("tag")); } + @Test void testEventBufferReplaysPathLocation() { assertEvent(b -> b.setLocation("file.feature", 1), l -> verify(l).setLocation("file.feature", 1)); } + @Test void testEventBufferReplaysLocation() { final Location location = Location.create("file.feature", 2); assertEvent(b -> b.setLocation(location), l -> verify(l).setLocation(location)); } + @Test void testEventBufferReplaysForwards() { assertEvent(b -> b.setForwards(true), l -> verify(l).setForwards(true)); } + @Test void testEventBufferReplaysEndItem() { assertEvent(EventBuffer::endSpecificationItem, ImportEventListener::endSpecificationItem); } + + private static void assertEvent(final Consumer addEvent, + final Consumer verifyEvent) { + final EventBuffer buffer = new EventBuffer(); final ImportEventListener listener = mock(ImportEventListener.class); - final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); - final Location location = Location.create("file.feature", 2); - - buffer.beginSpecificationItem(); - buffer.setId(id); - buffer.setTitle("title"); - buffer.setStatus(ItemStatus.DRAFT); - buffer.appendDescription("description"); - buffer.appendRationale("rationale"); - buffer.appendComment("comment"); - buffer.addCoveredId(id); - buffer.addDependsOnId(id); - buffer.addNeededArtifactType("dsn"); - buffer.addTag("tag"); - buffer.setLocation("file.feature", 1); - buffer.setLocation(location); - buffer.setForwards(true); - buffer.endSpecificationItem(); + addEvent.accept(buffer); buffer.replay(listener); - - verify(listener).beginSpecificationItem(); - verify(listener).setId(id); - verify(listener).setTitle("title"); - verify(listener).setStatus(ItemStatus.DRAFT); - verify(listener).appendDescription("description"); - verify(listener).appendRationale("rationale"); - verify(listener).appendComment("comment"); - verify(listener).addCoveredId(id); - verify(listener).addDependsOnId(id); - verify(listener).addNeededArtifactType("dsn"); - verify(listener).addTag("tag"); - verify(listener).setLocation("file.feature", 1); - verify(listener).setLocation(location); - verify(listener).setForwards(true); - verify(listener).endSpecificationItem(); + verifyEvent.accept(listener); } private static List importText(final String source) { From 2be32156e64150f3628b0b8d11aa246a2765e87b Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 23 Jul 2026 17:04:09 +0200 Subject: [PATCH 07/17] Add finish method to LineReader --- .../importer/gherkin/GherkinImporter.java | 1 - .../importer/gherkin/GherkinLineConsumer.java | 3 +- .../importer/gherkin/GherkinImporterTest.java | 48 ++++---- .../importer/gherkin/TestEventBuffer.java | 106 ++++++++++++++++++ .../tag/common/DelegatingLineConsumer.java | 6 + .../importer/tag/common/LineReader.java | 17 +++ .../common/TestDelegatingLineConsumer.java | 35 ++++++ .../importer/tag/common/TestLineReader.java | 11 ++ 8 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java create mode 100644 importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestDelegatingLineConsumer.java diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java index 4695fd914..ca8db6d7e 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java @@ -19,6 +19,5 @@ final class GherkinImporter implements Importer { @Override public void runImport() { LineReader.create(this.file).readLines(this.lineConsumer); - this.lineConsumer.finish(); } } 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 82ad5d055..1b0e079df 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 @@ -70,7 +70,8 @@ public void readLine(final int lineNumber, final String line) { readMetadata(lineNumber, line); } - void finish() { + @Override + public void finish() { endScenario(); } 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 2666b3cbd..fbd2bd8ab 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 @@ -3,15 +3,17 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import java.io.BufferedReader; +import java.io.StringReader; import java.nio.file.Path; import java.util.List; -import java.util.function.Consumer; 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.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.ImporterException; import org.itsallcode.openfasttrace.api.importer.input.InputFile; @@ -21,6 +23,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InOrder; class GherkinImporterTest { private static final GherkinImporterFactory FACTORY = new GherkinImporterFactory(); @@ -207,30 +210,25 @@ void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { hasProperty("id", hasToString("impl~gherkin-comment~1")))); } + // [utest->dsn~gherkin.comment-coverage-tags~1] @Test - void testEventBufferReplaysBeginItem() { assertEvent(EventBuffer::beginSpecificationItem, ImportEventListener::beginSpecificationItem); } - @Test void testEventBufferReplaysId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.setId(id), l -> verify(l).setId(id)); } - @Test void testEventBufferReplaysTitle() { assertEvent(b -> b.setTitle("title"), l -> verify(l).setTitle("title")); } - @Test void testEventBufferReplaysStatus() { assertEvent(b -> b.setStatus(ItemStatus.DRAFT), l -> verify(l).setStatus(ItemStatus.DRAFT)); } - @Test void testEventBufferReplaysDescription() { assertEvent(b -> b.appendDescription("description"), l -> verify(l).appendDescription("description")); } - @Test void testEventBufferReplaysRationale() { assertEvent(b -> b.appendRationale("rationale"), l -> verify(l).appendRationale("rationale")); } - @Test void testEventBufferReplaysComment() { assertEvent(b -> b.appendComment("comment"), l -> verify(l).appendComment("comment")); } - @Test void testEventBufferReplaysCoveredId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.addCoveredId(id), l -> verify(l).addCoveredId(id)); } - @Test void testEventBufferReplaysDependencyId() { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(b -> b.addDependsOnId(id), l -> verify(l).addDependsOnId(id)); } - @Test void testEventBufferReplaysNeededArtifactType() { assertEvent(b -> b.addNeededArtifactType("dsn"), l -> verify(l).addNeededArtifactType("dsn")); } - @Test void testEventBufferReplaysTag() { assertEvent(b -> b.addTag("tag"), l -> verify(l).addTag("tag")); } - @Test void testEventBufferReplaysPathLocation() { assertEvent(b -> b.setLocation("file.feature", 1), l -> verify(l).setLocation("file.feature", 1)); } - @Test void testEventBufferReplaysLocation() { final Location location = Location.create("file.feature", 2); assertEvent(b -> b.setLocation(location), l -> verify(l).setLocation(location)); } - @Test void testEventBufferReplaysForwards() { assertEvent(b -> b.setForwards(true), l -> verify(l).setForwards(true)); } - @Test void testEventBufferReplaysEndItem() { assertEvent(EventBuffer::endSpecificationItem, ImportEventListener::endSpecificationItem); } - - private static void assertEvent(final Consumer addEvent, - final Consumer verifyEvent) { - final EventBuffer buffer = new EventBuffer(); + void testDelaysCommentCoverageTagsUntilAfterTheScenario() { final ImportEventListener listener = mock(ImportEventListener.class); - addEvent.accept(buffer); - buffer.replay(listener); - verifyEvent.accept(listener); + final InputFile file = StreamInput.forReader(Path.of("specification.feature"), + new BufferedReader(new StringReader(""" + @id:scn~ordinary~1 + Scenario: ordinary + # [impl~gherkin-comment~1 -> dsn~gherkin~1] + Given a step + """))); + + new GherkinImporter(file, listener).runImport(); + + final InOrder events = inOrder(listener); + events.verify(listener).beginSpecificationItem(); + events.verify(listener).setId(SpecificationItemId.parseId("scn~ordinary~1")); + events.verify(listener).endSpecificationItem(); + events.verify(listener).beginSpecificationItem(); } private static List importText(final String source) { diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java new file mode 100644 index 000000000..3e0338fb9 --- /dev/null +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java @@ -0,0 +1,106 @@ +package org.itsallcode.openfasttrace.importer.gherkin; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.function.Consumer; + +import org.itsallcode.openfasttrace.api.core.*; +import org.itsallcode.openfasttrace.api.importer.ImportEventListener; +import org.junit.jupiter.api.Test; + +class TestEventBuffer { + @Test + void testReplaysBeginSpecificationItem() { + assertEvent(EventBuffer::beginSpecificationItem, ImportEventListener::beginSpecificationItem); + } + + @Test + void testReplaysId() { + final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); + assertEvent(buffer -> buffer.setId(id), listener -> verify(listener).setId(id)); + } + + @Test + void testReplaysTitle() { + assertEvent(buffer -> buffer.setTitle("title"), listener -> verify(listener).setTitle("title")); + } + + @Test + void testReplaysStatus() { + assertEvent(buffer -> buffer.setStatus(ItemStatus.DRAFT), + listener -> verify(listener).setStatus(ItemStatus.DRAFT)); + } + + @Test + void testReplaysDescription() { + assertEvent(buffer -> buffer.appendDescription("description"), + listener -> verify(listener).appendDescription("description")); + } + + @Test + void testReplaysRationale() { + assertEvent(buffer -> buffer.appendRationale("rationale"), + listener -> verify(listener).appendRationale("rationale")); + } + + @Test + void testReplaysComment() { + assertEvent(buffer -> buffer.appendComment("comment"), + listener -> verify(listener).appendComment("comment")); + } + + @Test + void testReplaysCoveredId() { + final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); + assertEvent(buffer -> buffer.addCoveredId(id), listener -> verify(listener).addCoveredId(id)); + } + + @Test + void testReplaysDependencyId() { + final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); + assertEvent(buffer -> buffer.addDependsOnId(id), listener -> verify(listener).addDependsOnId(id)); + } + + @Test + void testReplaysNeededArtifactType() { + assertEvent(buffer -> buffer.addNeededArtifactType("dsn"), + listener -> verify(listener).addNeededArtifactType("dsn")); + } + + @Test + void testReplaysTag() { + assertEvent(buffer -> buffer.addTag("tag"), listener -> verify(listener).addTag("tag")); + } + + @Test + void testReplaysPathLocation() { + assertEvent(buffer -> buffer.setLocation("file.feature", 1), + listener -> verify(listener).setLocation("file.feature", 1)); + } + + @Test + void testReplaysLocation() { + final Location location = Location.create("file.feature", 2); + assertEvent(buffer -> buffer.setLocation(location), listener -> verify(listener).setLocation(location)); + } + + @Test + void testReplaysForwards() { + assertEvent(buffer -> buffer.setForwards(true), listener -> verify(listener).setForwards(true)); + } + + @Test + void testReplaysEndSpecificationItem() { + assertEvent(EventBuffer::endSpecificationItem, ImportEventListener::endSpecificationItem); + } + + private static void assertEvent(final Consumer addEvent, + final Consumer verifyEvent) { + final EventBuffer buffer = new EventBuffer(); + final ImportEventListener listener = mock(ImportEventListener.class); + addEvent.accept(buffer); + buffer.replay(listener); + verifyEvent.accept(listener); + } +} diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/DelegatingLineConsumer.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/DelegatingLineConsumer.java index 0def4d7ce..6e2602154 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/DelegatingLineConsumer.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/DelegatingLineConsumer.java @@ -19,4 +19,10 @@ public void readLine(final int lineNumber, final String line) { this.delegates.forEach(delegate -> delegate.readLine(lineNumber, line)); } + + @Override + public void finish() + { + this.delegates.forEach(LineConsumer::finish); + } } diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java index 32bafc053..b7d10eb67 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java @@ -44,6 +44,7 @@ public void readLines(final LineConsumer consumer) { currentLineNumber = reader.getLineNumber(); processLine(consumer, currentLineNumber, line); } + finish(consumer); } catch (final IOException exception) { throw new ImporterException("Error reading \"" + this.file + "\" at line " + currentLineNumber, exception); } @@ -59,6 +60,15 @@ private void processLine(final LineConsumer consumer, final int currentLineNumbe } } + private void finish(final LineConsumer consumer) { + try { + consumer.finish(); + } catch (final RuntimeException exception) { + throw new ImporterException("Error finishing " + this.file.getPath() + ": " + exception, + exception); + } + } + /** * Receives a line read from an input file. */ @@ -73,5 +83,12 @@ public interface LineConsumer { * line content without its line separator */ void readLine(int lineNumber, String line); + + /** + * Finish consuming the input after its last line has been read. + */ + default void finish() { + // Default implementation intentionally does nothing. + } } } diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestDelegatingLineConsumer.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestDelegatingLineConsumer.java new file mode 100644 index 000000000..2ced642f1 --- /dev/null +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestDelegatingLineConsumer.java @@ -0,0 +1,35 @@ +package org.itsallcode.openfasttrace.importer.tag.common; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.List; + +import org.itsallcode.openfasttrace.importer.tag.common.LineReader.LineConsumer; +import org.junit.jupiter.api.Test; + +class TestDelegatingLineConsumer { + @Test + void testReadLineCallsAllDelegates() { + final LineConsumer firstDelegate = mock(LineConsumer.class); + final LineConsumer secondDelegate = mock(LineConsumer.class); + final DelegatingLineConsumer consumer = new DelegatingLineConsumer(List.of(firstDelegate, secondDelegate)); + + consumer.readLine(2, "line"); + + verify(firstDelegate).readLine(2, "line"); + verify(secondDelegate).readLine(2, "line"); + } + + @Test + void testFinishesAllDelegates() { + final LineConsumer firstDelegate = mock(LineConsumer.class); + final LineConsumer secondDelegate = mock(LineConsumer.class); + final DelegatingLineConsumer consumer = new DelegatingLineConsumer(List.of(firstDelegate, secondDelegate)); + + consumer.finish(); + + verify(firstDelegate).finish(); + verify(secondDelegate).finish(); + } +} diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java index 1da851f22..60f23ae8c 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java @@ -99,6 +99,16 @@ void testWrapsConsumerFailureWithLineInformation() { assertThat(exception.getMessage(), equalTo("Error processing line dummy:1 'line1': " + cause)); } + @Test + void testWrapsConsumerFinishFailure() { + final RuntimeException cause = new IllegalArgumentException("cannot finish"); + doThrow(cause).when(this.consumerMock).finish(); + + final ImporterException exception = assertThrows(ImporterException.class, () -> readContent("")); + + assertThat(exception.getMessage(), equalTo("Error finishing dummy: " + cause)); + } + @Test void testWrapsReaderCreationFailure() throws IOException { final InputFile file = mock(InputFile.class); @@ -126,6 +136,7 @@ private void assertLinesRead(final String... expectedLines) { inOrder.verify(this.consumerMock).readLine(lineNumber, line); lineNumber++; } + inOrder.verify(this.consumerMock).finish(); inOrder.verifyNoMoreInteractions(); } } From 32f32edc58b0022283a326b986a63294ea496d9b Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 06:46:10 +0200 Subject: [PATCH 08/17] Fix self-trace --- .../openfasttrace/importer/gherkin/GherkinImporterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 fbd2bd8ab..b268ec554 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 @@ -218,9 +218,9 @@ void testDelaysCommentCoverageTagsUntilAfterTheScenario() { new BufferedReader(new StringReader(""" @id:scn~ordinary~1 Scenario: ordinary - # [impl~gherkin-comment~1 -> dsn~gherkin~1] + # [%s] Given a step - """))); + """.formatted("impl~gherkin-comment~1 -> dsn~gherkin~1")))); new GherkinImporter(file, listener).runImport(); From e26c3f339782e319e1cd9e05d210d8ef7df30da9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 07:05:53 +0200 Subject: [PATCH 09/17] Code cleanup --- .../importer/gherkin/GherkinLineConsumer.java | 71 ++++++++++--------- .../importer/gherkin/GherkinImporterTest.java | 2 +- .../importer/gherkin/TestEventBuffer.java | 2 +- 3 files changed, 38 insertions(+), 37 deletions(-) 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 1b0e079df..decb98b53 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 @@ -26,7 +26,8 @@ final class GherkinLineConsumer implements LineConsumer { private final InputFile file; private final ImportEventListener listener; private final LineConsumer coverageTagParser; - // Legacy tags emit complete items and must not interleave with an open scenario. + // Legacy tags emit complete items and must not interleave with an open + // scenario. private final LineConsumer delayedCoverageTagParser; private final EventBuffer delayedCoverageEvents = new EventBuffer(); private final Set importedIds = new LinkedHashSet<>(); @@ -61,9 +62,9 @@ public void readLine(final int lineNumber, final String line) { clearMetadata(); return; } - if (this.importingScenario) { + if (importingScenario) { if (!line.trim().startsWith("#")) { - this.listener.appendDescription(line + System.lineSeparator()); + listener.appendDescription(line + System.lineSeparator()); } return; } @@ -76,7 +77,7 @@ public void finish() { } private LineConsumer getCoverageTagParser() { - return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; + return importingScenario ? delayedCoverageTagParser : coverageTagParser; } private void readMetadata(final int lineNumber, final String line) { @@ -85,7 +86,7 @@ private void readMetadata(final int lineNumber, final String line) { return; } final Matcher directive = DIRECTIVE.matcher(line); - if (this.metadataRegion && directive.matches()) { + if (metadataRegion && directive.matches()) { readDirective(lineNumber, directive.group(1), directive.group(2)); return; } @@ -95,27 +96,27 @@ private void readMetadata(final int lineNumber, final String line) { } private void readTagRegion(final int lineNumber, final String tags) { - if (!this.tagRegion) { + if (!tagRegion) { clearMetadata(); } - this.metadataRegion = true; - this.tagRegion = true; + metadataRegion = true; + tagRegion = true; final Matcher matcher = ID_TAG.matcher(tags); while (matcher.find()) { - if (this.pendingId != null) { + if (pendingId != null) { fail(lineNumber, "multiple @id tags before a scenario"); } - this.pendingId = parseId(lineNumber, matcher.group(1)); + pendingId = parseId(lineNumber, matcher.group(1)); } } private void readDirective(final int lineNumber, final String name, final String values) { - this.tagRegion = false; - if (this.pendingId == null) { + tagRegion = false; + if (pendingId == null) { fail(lineNumber, name + " directive requires exactly one preceding @id tag"); } final boolean covers = "Covers".equals(name); - if (!covers && this.hasNeedsDirective) { + if (!covers && hasNeedsDirective) { fail(lineNumber, "repeated " + name + " directive"); } final String[] entries = splitValues(lineNumber, name, values); @@ -124,7 +125,7 @@ private void readDirective(final int lineNumber, final String name, final String return; } readNeededArtifactTypes(lineNumber, entries); - this.hasNeedsDirective = true; + hasNeedsDirective = true; } private String[] splitValues(final int lineNumber, final String name, final String values) { @@ -172,41 +173,41 @@ private SpecificationItemId parseId(final int lineNumber, final String value) { } private void beginScenario(final int lineNumber, final String title) { - if (this.pendingId == null) { + if (pendingId == null) { clearMetadata(); return; } - if (!this.importedIds.add(this.pendingId)) { - fail(lineNumber, "duplicate Gherkin ID '" + this.pendingId + "'"); + if (!importedIds.add(pendingId)) { + fail(lineNumber, "duplicate Gherkin ID '" + pendingId + "'"); } - this.listener.beginSpecificationItem(); - this.listener.setLocation(this.file.getPath(), lineNumber); - this.listener.setId(this.pendingId); - this.listener.setTitle(title); - this.coveredIds.forEach(this.listener::addCoveredId); - this.neededArtifactTypes.forEach(this.listener::addNeededArtifactType); - this.importingScenario = true; + listener.beginSpecificationItem(); + listener.setLocation(file.getPath(), lineNumber); + listener.setId(pendingId); + listener.setTitle(title); + coveredIds.forEach(listener::addCoveredId); + neededArtifactTypes.forEach(listener::addNeededArtifactType); + importingScenario = true; clearMetadata(); } private void endScenario() { - if (this.importingScenario) { - this.listener.endSpecificationItem(); - this.importingScenario = false; - this.delayedCoverageEvents.replay(this.listener); + if (importingScenario) { + listener.endSpecificationItem(); + importingScenario = false; + delayedCoverageEvents.replay(listener); } } private void clearMetadata() { - this.pendingId = null; - this.coveredIds = new LinkedHashSet<>(); - this.neededArtifactTypes = new LinkedHashSet<>(); - this.hasNeedsDirective = false; - this.metadataRegion = false; - this.tagRegion = false; + pendingId = null; + coveredIds = new LinkedHashSet<>(); + neededArtifactTypes = new LinkedHashSet<>(); + hasNeedsDirective = false; + metadataRegion = false; + tagRegion = false; } private void fail(final int lineNumber, final String reason) { - throw new IllegalArgumentException(this.file.getPath() + ":" + lineNumber + ": " + reason); + throw new IllegalArgumentException(file.getPath() + ":" + lineNumber + ": " + reason); } } 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 b268ec554..04c6e9130 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 @@ -33,7 +33,7 @@ class GherkinImporterTest { void testImportsScenarioOutlineWithScopedMetadataAndSteps() { final String source = """ @smoke - @id:scn~account-login~1 + @unrelated @id:scn~account-login~1 @anotherTag # Covers: req~login~1 # Needs: dsn, itest Scenario Outline: Login works diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java index 3e0338fb9..46db9edcd 100644 --- a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java @@ -98,8 +98,8 @@ void testReplaysEndSpecificationItem() { private static void assertEvent(final Consumer addEvent, final Consumer verifyEvent) { final EventBuffer buffer = new EventBuffer(); - final ImportEventListener listener = mock(ImportEventListener.class); addEvent.accept(buffer); + final ImportEventListener listener = mock(ImportEventListener.class); buffer.replay(listener); verifyEvent.accept(listener); } From beab25415b06f5f838f2c4a89573c2005e5155bb Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 07:22:33 +0200 Subject: [PATCH 10/17] Fix review findings --- .../importer/gherkin/GherkinImporterTest.java | 26 +++++++++++-------- .../importer/tag/common/LineReader.java | 4 +-- .../importer/tag/common/TestLineReader.java | 4 +-- 3 files changed, 19 insertions(+), 15 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 04c6e9130..76083d170 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 @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -47,15 +48,16 @@ void testImportsScenarioOutlineWithScopedMetadataAndSteps() { final List items = importText(source); - assertThat(items, contains( - hasProperty("id", hasToString("scn~account-login~1")))); final SpecificationItem item = items.get(0); - assertThat(item.getTitle(), is("Login works")); - assertThat(item.getLocation().getLine(), is(5)); - assertThat(item.getDescription(), is(String.join(System.lineSeparator(), - "Given a registered user", " When they log in", " Then access is granted"))); - assertThat(item.getCoveredIds(), contains(hasToString("req~login~1"))); - assertThat(item.getNeedsArtifactTypes(), containsInAnyOrder("dsn", "itest")); + assertAll( + () -> assertThat(items, contains( + hasProperty("id", hasToString("scn~account-login~1")))), + () -> assertThat(item.getTitle(), is("Login works")), + () -> assertThat(item.getLocation().getLine(), is(5)), + () -> assertThat(item.getDescription(), is(String.join(System.lineSeparator(), + "Given a registered user", " When they log in", " Then access is granted"))), + () -> assertThat(item.getCoveredIds(), contains(hasToString("req~login~1"))), + () -> assertThat(item.getNeedsArtifactTypes(), containsInAnyOrder("dsn", "itest"))); } // [utest->dsn~gherkin.importer-selection~1] @@ -64,8 +66,9 @@ void testFactorySupportsFeatureFilesWithHigherPrecedenceThanTagImporter() { final InputFile file = StreamInput.forReader(Path.of("specification.feature"), new java.io.BufferedReader(new java.io.StringReader(""))); - assertThat(FACTORY.supportsFile(file), is(true)); - assertThat(FACTORY.getPriority(), is(9000)); + assertAll( + () -> assertThat(FACTORY.supportsFile(file), is(true)), + () -> assertThat(FACTORY.getPriority(), is(9000))); } // [utest->dsn~gherkin.streaming-import~1] @@ -107,7 +110,8 @@ void testRejectsDirectiveWithoutId() { final ImporterException exception = assertThrows(ImporterException.class, () -> importText(source)); - assertThat(exception.getMessage(), hasToString(org.hamcrest.Matchers.containsString("requires exactly one"))); + assertThat(exception.getMessage(), equalTo( + "Error processing line specification.feature:2 '# Needs: dsn': specification.feature:2: Needs directive requires exactly one preceding @id tag")); } // [utest->dsn~gherkin.streaming-import~1] diff --git a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java index b7d10eb67..0fa22d21c 100644 --- a/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java +++ b/importer/tag-importer-common/src/main/java/org/itsallcode/openfasttrace/importer/tag/common/LineReader.java @@ -56,7 +56,7 @@ private void processLine(final LineConsumer consumer, final int currentLineNumbe consumer.readLine(currentLineNumber, line); } catch (final RuntimeException exception) { throw new ImporterException("Error processing line " + this.file.getPath() + ":" + currentLineNumber + " '" - + line + "': " + exception, exception); + + line + "': " + exception.getMessage(), exception); } } @@ -64,7 +64,7 @@ private void finish(final LineConsumer consumer) { try { consumer.finish(); } catch (final RuntimeException exception) { - throw new ImporterException("Error finishing " + this.file.getPath() + ": " + exception, + throw new ImporterException("Error finishing " + this.file.getPath() + ": " + exception.getMessage(), exception); } } diff --git a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java index 60f23ae8c..69ffb2ad0 100644 --- a/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java +++ b/importer/tag-importer-common/src/test/java/org/itsallcode/openfasttrace/importer/tag/common/TestLineReader.java @@ -96,7 +96,7 @@ void testWrapsConsumerFailureWithLineInformation() { doThrow(cause).when(this.consumerMock).readLine(1, "line1"); final ImporterException exception = assertThrows(ImporterException.class, () -> readContent("line1")); - assertThat(exception.getMessage(), equalTo("Error processing line dummy:1 'line1': " + cause)); + assertThat(exception.getMessage(), equalTo("Error processing line dummy:1 'line1': invalid line")); } @Test @@ -106,7 +106,7 @@ void testWrapsConsumerFinishFailure() { final ImporterException exception = assertThrows(ImporterException.class, () -> readContent("")); - assertThat(exception.getMessage(), equalTo("Error finishing dummy: " + cause)); + assertThat(exception.getMessage(), equalTo("Error finishing dummy: cannot finish")); } @Test From 2ae911b36ec759ea42ab90f4012b13ec57f63a4c Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 09:07:34 +0200 Subject: [PATCH 11/17] Add missing eclipse formatter config --- .../.settings/org.eclipse.jdt.core.prefs | 413 ++++++++++++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 146 +++++++ .../.settings/org.eclipse.jdt.core.prefs | 413 ++++++++++++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 146 +++++++ 4 files changed, 1118 insertions(+) create mode 100644 importer/gherkin/.settings/org.eclipse.jdt.core.prefs create mode 100644 importer/gherkin/.settings/org.eclipse.jdt.ui.prefs create mode 100644 importer/tag-importer-common/.settings/org.eclipse.jdt.core.prefs create mode 100644 importer/tag-importer-common/.settings/org.eclipse.jdt.ui.prefs diff --git a/importer/gherkin/.settings/org.eclipse.jdt.core.prefs b/importer/gherkin/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..a40522564 --- /dev/null +++ b/importer/gherkin/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,413 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=warning +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected +org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.processAnnotations=disabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_record_components=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=next_line_on_wrap +org.eclipse.jdt.core.formatter.brace_position_for_block=next_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=next_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=next_line +org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=next_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=next_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=false +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=true +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.text_block_indentation=0 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/importer/gherkin/.settings/org.eclipse.jdt.ui.prefs b/importer/gherkin/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..0a69ece30 --- /dev/null +++ b/importer/gherkin/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,146 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_itsallcode style +formatter_settings_version=21 +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com; +org.eclipse.jdt.ui.ondemandthreshold=4 +org.eclipse.jdt.ui.staticondemandthreshold=4 +sp_cleanup.add_all=false +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.array_with_curly=false +sp_cleanup.arrays_fill=false +sp_cleanup.bitwise_conditional_expression=false +sp_cleanup.boolean_literal=false +sp_cleanup.boolean_value_rather_than_comparison=false +sp_cleanup.break_loop=false +sp_cleanup.collection_cloning=false +sp_cleanup.comparing_on_criteria=false +sp_cleanup.comparison_statement=false +sp_cleanup.controlflow_merge=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false +sp_cleanup.convert_to_switch_expressions=false +sp_cleanup.correct_indentation=false +sp_cleanup.do_while_rather_than_while=false +sp_cleanup.double_negation=false +sp_cleanup.else_if=false +sp_cleanup.embedded_if=false +sp_cleanup.evaluate_nullable=false +sp_cleanup.extract_increment=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.hash=false +sp_cleanup.if_condition=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.instanceof=false +sp_cleanup.instanceof_keyword=false +sp_cleanup.invert_equals=false +sp_cleanup.join=false +sp_cleanup.lazy_logical_operator=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=true +sp_cleanup.map_cloning=false +sp_cleanup.merge_conditional_blocks=false +sp_cleanup.multi_catch=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.no_string_creation=false +sp_cleanup.no_super=false +sp_cleanup.number_suffix=false +sp_cleanup.objects_equals=false +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false +sp_cleanup.operand_factorization=false +sp_cleanup.organize_imports=true +sp_cleanup.overridden_assignment=false +sp_cleanup.plain_replacement=false +sp_cleanup.precompile_regex=false +sp_cleanup.primitive_comparison=false +sp_cleanup.primitive_parsing=false +sp_cleanup.primitive_rather_than_wrapper=false +sp_cleanup.primitive_serialization=false +sp_cleanup.pull_out_if_from_if_else=false +sp_cleanup.pull_up_assignment=false +sp_cleanup.push_down_negation=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.reduce_indentation=false +sp_cleanup.redundant_comparator=false +sp_cleanup.redundant_falling_through_block_end=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_modifiers=false +sp_cleanup.remove_redundant_semicolons=false +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_array_creation=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.return_expression=false +sp_cleanup.simplify_lambda_expression_and_method_ref=false +sp_cleanup.single_used_field=false +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.standard_comparison=false +sp_cleanup.static_inner_class=false +sp_cleanup.strictly_equal_or_different=false +sp_cleanup.stringbuffer_to_stringbuilder=false +sp_cleanup.stringbuilder=false +sp_cleanup.stringbuilder_for_local_vars=false +sp_cleanup.stringconcat_to_textblock=false +sp_cleanup.substring=false +sp_cleanup.switch=false +sp_cleanup.system_property=false +sp_cleanup.system_property_boolean=false +sp_cleanup.system_property_file_encoding=false +sp_cleanup.system_property_file_separator=false +sp_cleanup.system_property_line_separator=false +sp_cleanup.system_property_path_separator=false +sp_cleanup.ternary_operator=false +sp_cleanup.try_with_resource=false +sp_cleanup.unlooped_while=false +sp_cleanup.unreachable_block=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_autoboxing=false +sp_cleanup.use_blocks=true +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_directly_map_method=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_string_is_blank=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_unboxing=false +sp_cleanup.use_var=false +sp_cleanup.useless_continue=false +sp_cleanup.useless_return=false +sp_cleanup.valueof_rather_than_instantiation=false diff --git a/importer/tag-importer-common/.settings/org.eclipse.jdt.core.prefs b/importer/tag-importer-common/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..a40522564 --- /dev/null +++ b/importer/tag-importer-common/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,413 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=warning +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected +org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.processAnnotations=disabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_record_components=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=next_line_on_wrap +org.eclipse.jdt.core.formatter.brace_position_for_block=next_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=next_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=next_line +org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=next_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=next_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=false +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=true +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.text_block_indentation=0 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/importer/tag-importer-common/.settings/org.eclipse.jdt.ui.prefs b/importer/tag-importer-common/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..0a69ece30 --- /dev/null +++ b/importer/tag-importer-common/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,146 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_itsallcode style +formatter_settings_version=21 +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com; +org.eclipse.jdt.ui.ondemandthreshold=4 +org.eclipse.jdt.ui.staticondemandthreshold=4 +sp_cleanup.add_all=false +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.array_with_curly=false +sp_cleanup.arrays_fill=false +sp_cleanup.bitwise_conditional_expression=false +sp_cleanup.boolean_literal=false +sp_cleanup.boolean_value_rather_than_comparison=false +sp_cleanup.break_loop=false +sp_cleanup.collection_cloning=false +sp_cleanup.comparing_on_criteria=false +sp_cleanup.comparison_statement=false +sp_cleanup.controlflow_merge=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false +sp_cleanup.convert_to_switch_expressions=false +sp_cleanup.correct_indentation=false +sp_cleanup.do_while_rather_than_while=false +sp_cleanup.double_negation=false +sp_cleanup.else_if=false +sp_cleanup.embedded_if=false +sp_cleanup.evaluate_nullable=false +sp_cleanup.extract_increment=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.hash=false +sp_cleanup.if_condition=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.instanceof=false +sp_cleanup.instanceof_keyword=false +sp_cleanup.invert_equals=false +sp_cleanup.join=false +sp_cleanup.lazy_logical_operator=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=true +sp_cleanup.map_cloning=false +sp_cleanup.merge_conditional_blocks=false +sp_cleanup.multi_catch=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.no_string_creation=false +sp_cleanup.no_super=false +sp_cleanup.number_suffix=false +sp_cleanup.objects_equals=false +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false +sp_cleanup.operand_factorization=false +sp_cleanup.organize_imports=true +sp_cleanup.overridden_assignment=false +sp_cleanup.plain_replacement=false +sp_cleanup.precompile_regex=false +sp_cleanup.primitive_comparison=false +sp_cleanup.primitive_parsing=false +sp_cleanup.primitive_rather_than_wrapper=false +sp_cleanup.primitive_serialization=false +sp_cleanup.pull_out_if_from_if_else=false +sp_cleanup.pull_up_assignment=false +sp_cleanup.push_down_negation=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.reduce_indentation=false +sp_cleanup.redundant_comparator=false +sp_cleanup.redundant_falling_through_block_end=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_modifiers=false +sp_cleanup.remove_redundant_semicolons=false +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_array_creation=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.return_expression=false +sp_cleanup.simplify_lambda_expression_and_method_ref=false +sp_cleanup.single_used_field=false +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.standard_comparison=false +sp_cleanup.static_inner_class=false +sp_cleanup.strictly_equal_or_different=false +sp_cleanup.stringbuffer_to_stringbuilder=false +sp_cleanup.stringbuilder=false +sp_cleanup.stringbuilder_for_local_vars=false +sp_cleanup.stringconcat_to_textblock=false +sp_cleanup.substring=false +sp_cleanup.switch=false +sp_cleanup.system_property=false +sp_cleanup.system_property_boolean=false +sp_cleanup.system_property_file_encoding=false +sp_cleanup.system_property_file_separator=false +sp_cleanup.system_property_line_separator=false +sp_cleanup.system_property_path_separator=false +sp_cleanup.ternary_operator=false +sp_cleanup.try_with_resource=false +sp_cleanup.unlooped_while=false +sp_cleanup.unreachable_block=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_autoboxing=false +sp_cleanup.use_blocks=true +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_directly_map_method=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_string_is_blank=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_unboxing=false +sp_cleanup.use_var=false +sp_cleanup.useless_continue=false +sp_cleanup.useless_return=false +sp_cleanup.valueof_rather_than_instantiation=false From 56c9523d464ada990e87a7cfaead1155b4957eda Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 09:11:46 +0200 Subject: [PATCH 12/17] Reformat sources, add qualifying this --- .../gherkin/src/main/java/module-info.java | 9 +- .../importer/gherkin/EventBuffer.java | 51 +++-- .../importer/gherkin/GherkinImporter.java | 9 +- .../gherkin/GherkinImporterFactory.java | 12 +- .../importer/gherkin/GherkinLineConsumer.java | 178 +++++++++++------- .../importer/gherkin/GherkinImporterTest.java | 39 ++-- .../importer/gherkin/TestEventBuffer.java | 51 +++-- 7 files changed, 225 insertions(+), 124 deletions(-) diff --git a/importer/gherkin/src/main/java/module-info.java b/importer/gherkin/src/main/java/module-info.java index 040409eca..397577fd2 100644 --- a/importer/gherkin/src/main/java/module-info.java +++ b/importer/gherkin/src/main/java/module-info.java @@ -1,7 +1,12 @@ import org.itsallcode.openfasttrace.importer.gherkin.GherkinImporterFactory; -/** Provides an importer for Gherkin {@code .feature} files. */ -module org.itsallcode.openfasttrace.importer.gherkin { +/** + * Provides an importer for Gherkin {@code .feature} files. + * + * @provides org.itsallcode.openfasttrace.api.importer.ImporterFactory + */ +module org.itsallcode.openfasttrace.importer.gherkin +{ requires transitive org.itsallcode.openfasttrace.api; requires org.itsallcode.openfasttrace.importer.tag.common; diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java index ef9d85b69..1333f0bfa 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java @@ -8,85 +8,102 @@ import org.itsallcode.openfasttrace.api.importer.ImportEventListener; /** Buffers legacy coverage-tag events until the current scenario has ended. */ -final class EventBuffer implements ImportEventListener { +final class EventBuffer implements ImportEventListener +{ private final List> events = new ArrayList<>(); @Override - public void beginSpecificationItem() { + public void beginSpecificationItem() + { this.events.add(ImportEventListener::beginSpecificationItem); } @Override - public void setId(final SpecificationItemId id) { + public void setId(final SpecificationItemId id) + { this.events.add(target -> target.setId(id)); } @Override - public void setTitle(final String title) { + public void setTitle(final String title) + { this.events.add(target -> target.setTitle(title)); } @Override - public void setStatus(final ItemStatus status) { + public void setStatus(final ItemStatus status) + { this.events.add(target -> target.setStatus(status)); } @Override - public void appendDescription(final String fragment) { + public void appendDescription(final String fragment) + { this.events.add(target -> target.appendDescription(fragment)); } @Override - public void appendRationale(final String fragment) { + public void appendRationale(final String fragment) + { this.events.add(target -> target.appendRationale(fragment)); } @Override - public void appendComment(final String fragment) { + public void appendComment(final String fragment) + { this.events.add(target -> target.appendComment(fragment)); } @Override - public void addCoveredId(final SpecificationItemId id) { + public void addCoveredId(final SpecificationItemId id) + { this.events.add(target -> target.addCoveredId(id)); } @Override - public void addDependsOnId(final SpecificationItemId id) { + public void addDependsOnId(final SpecificationItemId id) + { this.events.add(target -> target.addDependsOnId(id)); } @Override - public void addNeededArtifactType(final String artifactType) { + public void addNeededArtifactType(final String artifactType) + { this.events.add(target -> target.addNeededArtifactType(artifactType)); } @Override - public void addTag(final String tag) { + public void addTag(final String tag) + { this.events.add(target -> target.addTag(tag)); } @Override - public void setLocation(final String path, final int line) { + public void setLocation(final String path, final int line) + { this.events.add(target -> target.setLocation(path, line)); } @Override - public void endSpecificationItem() { + public void endSpecificationItem() + { this.events.add(ImportEventListener::endSpecificationItem); } @Override - public void setLocation(final Location location) { + public void setLocation(final Location location) + { this.events.add(target -> target.setLocation(location)); } @Override - public void setForwards(final boolean forwards) { + public void setForwards(final boolean forwards) + { this.events.add(target -> target.setForwards(forwards)); } - void replay(final ImportEventListener listener) { + void replay(final ImportEventListener listener) + { this.events.forEach(event -> event.accept(listener)); this.events.clear(); } diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java index ca8db6d7e..f3b80ad05 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporter.java @@ -7,17 +7,20 @@ /** Imports annotated Gherkin scenarios while streaming the input once. */ // [impl->dsn~gherkin.streaming-import~1] -final class GherkinImporter implements Importer { +final class GherkinImporter implements Importer +{ private final InputFile file; private final GherkinLineConsumer lineConsumer; - GherkinImporter(final InputFile file, final ImportEventListener listener) { + GherkinImporter(final InputFile file, final ImportEventListener listener) + { this.file = file; this.lineConsumer = new GherkinLineConsumer(file, listener); } @Override - public void runImport() { + public void runImport() + { LineReader.create(this.file).readLines(this.lineConsumer); } } diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java index f67805f4b..b083ea5a0 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/GherkinImporterFactory.java @@ -5,19 +5,23 @@ /** Factory for importing OpenFastTrace specifications from Gherkin files. */ // [impl->dsn~gherkin.importer-selection~1] -public class GherkinImporterFactory extends AbstractRegexMatchingImporterFactory { +public class GherkinImporterFactory extends AbstractRegexMatchingImporterFactory +{ /** Create a factory that accepts Gherkin {@code .feature} files. */ - public GherkinImporterFactory() { + public GherkinImporterFactory() + { super("(?i).*\\.feature"); } @Override - public int getPriority() { + public int getPriority() + { return 9000; } @Override - public Importer createImporter(final InputFile file, final ImportEventListener listener) { + public Importer createImporter(final InputFile file, final ImportEventListener listener) + { return new GherkinImporter(file, listener); } } 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 decb98b53..1071bc1ac 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 @@ -14,7 +14,8 @@ /** Stateful parser for the Gherkin lines of one input file. */ // [impl->dsn~gherkin.streaming-import~1] // [impl->dsn~gherkin.comment-coverage-tags~1] -final class GherkinLineConsumer implements LineConsumer { +final class GherkinLineConsumer implements LineConsumer +{ private static final int UNICODE = Pattern.UNICODE_CHARACTER_CLASS; private static final Pattern ID_TAG = Pattern.compile("@id:([^\\s]+)", UNICODE); private static final Pattern SCENARIO = Pattern.compile("^\\s*Scenario(?: Outline)?:(.*)$", UNICODE); @@ -39,7 +40,8 @@ final class GherkinLineConsumer implements LineConsumer { private boolean tagRegion; private boolean importingScenario; - GherkinLineConsumer(final InputFile file, final ImportEventListener listener) { + GherkinLineConsumer(final InputFile file, final ImportEventListener listener) + { this.file = file; this.listener = listener; this.coverageTagParser = CoverageTagParser.create(null, file, listener); @@ -47,24 +49,30 @@ final class GherkinLineConsumer implements LineConsumer { } @Override - public void readLine(final int lineNumber, final String line) { - if (line.trim().startsWith("#")) { + public void readLine(final int lineNumber, final String line) + { + if (line.trim().startsWith("#")) + { getCoverageTagParser().readLine(lineNumber, line); } final Matcher scenario = SCENARIO.matcher(line); - if (scenario.matches()) { + if (scenario.matches()) + { endScenario(); beginScenario(lineNumber, scenario.group(1).trim()); return; } - if (BOUNDARY.matcher(line).find()) { + if (BOUNDARY.matcher(line).find()) + { endScenario(); clearMetadata(); return; } - if (importingScenario) { - if (!line.trim().startsWith("#")) { - listener.appendDescription(line + System.lineSeparator()); + if (this.importingScenario) + { + if (!line.trim().startsWith("#")) + { + this.listener.appendDescription(line + System.lineSeparator()); } return; } @@ -72,142 +80,176 @@ public void readLine(final int lineNumber, final String line) { } @Override - public void finish() { + public void finish() + { endScenario(); } - private LineConsumer getCoverageTagParser() { - return importingScenario ? delayedCoverageTagParser : coverageTagParser; + private LineConsumer getCoverageTagParser() + { + return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; } - private void readMetadata(final int lineNumber, final String line) { - if (line.trim().startsWith("@")) { + private void readMetadata(final int lineNumber, final String line) + { + if (line.trim().startsWith("@")) + { readTagRegion(lineNumber, line.trim()); return; } final Matcher directive = DIRECTIVE.matcher(line); - if (metadataRegion && directive.matches()) { + if (this.metadataRegion && directive.matches()) + { readDirective(lineNumber, directive.group(1), directive.group(2)); return; } - if (!line.trim().startsWith("#")) { + if (!line.trim().startsWith("#")) + { clearMetadata(); } } - private void readTagRegion(final int lineNumber, final String tags) { - if (!tagRegion) { + private void readTagRegion(final int lineNumber, final String tags) + { + if (!this.tagRegion) + { clearMetadata(); } - metadataRegion = true; - tagRegion = true; + this.metadataRegion = true; + this.tagRegion = true; final Matcher matcher = ID_TAG.matcher(tags); - while (matcher.find()) { - if (pendingId != null) { + while (matcher.find()) + { + if (this.pendingId != null) + { fail(lineNumber, "multiple @id tags before a scenario"); } - pendingId = parseId(lineNumber, matcher.group(1)); + this.pendingId = parseId(lineNumber, matcher.group(1)); } } - private void readDirective(final int lineNumber, final String name, final String values) { - tagRegion = false; - if (pendingId == null) { + private void readDirective(final int lineNumber, final String name, final String values) + { + this.tagRegion = false; + if (this.pendingId == null) + { fail(lineNumber, name + " directive requires exactly one preceding @id tag"); } final boolean covers = "Covers".equals(name); - if (!covers && hasNeedsDirective) { + if (!covers && this.hasNeedsDirective) + { fail(lineNumber, "repeated " + name + " directive"); } final String[] entries = splitValues(lineNumber, name, values); - if (covers) { + if (covers) + { readCoveredIds(lineNumber, entries); return; } readNeededArtifactTypes(lineNumber, entries); - hasNeedsDirective = true; + this.hasNeedsDirective = true; } - private String[] splitValues(final int lineNumber, final String name, final String values) { - if (values.trim().isEmpty()) { + private String[] splitValues(final int lineNumber, final String name, final String values) + { + if (values.trim().isEmpty()) + { fail(lineNumber, name + " directive requires a non-empty list"); } return values.trim().split(",", -1); } - private void readCoveredIds(final int lineNumber, final String[] entries) { - for (final String entry : entries) { + private void readCoveredIds(final int lineNumber, final String[] entries) + { + for (final String entry : entries) + { final String value = requireValue(lineNumber, "Covers", entry); final SpecificationItemId id = parseId(lineNumber, value); - if (!this.coveredIds.add(id)) { + if (!this.coveredIds.add(id)) + { fail(lineNumber, "Covers directive contains duplicate value '" + id + "'"); } } } - private void readNeededArtifactTypes(final int lineNumber, final String[] entries) { - for (final String entry : entries) { + private void readNeededArtifactTypes(final int lineNumber, final String[] entries) + { + for (final String entry : entries) + { final String value = requireValue(lineNumber, "Needs", entry); - if (!ARTIFACT_TYPE.matcher(value).matches()) { + if (!ARTIFACT_TYPE.matcher(value).matches()) + { fail(lineNumber, "Needs directive contains invalid artifact type '" + value + "'"); } - if (!this.neededArtifactTypes.add(value)) { + if (!this.neededArtifactTypes.add(value)) + { fail(lineNumber, "Needs directive contains duplicate value '" + value + "'"); } } } - private String requireValue(final int lineNumber, final String name, final String entry) { + private String requireValue(final int lineNumber, final String name, final String entry) + { final String value = entry.trim(); - if (value.isEmpty()) { + if (value.isEmpty()) + { fail(lineNumber, name + " directive contains an empty value"); } return value; } - private SpecificationItemId parseId(final int lineNumber, final String value) { - if (!SpecificationItemId.ID_PATTERN.matcher(value).matches()) { + private SpecificationItemId parseId(final int lineNumber, final String value) + { + if (!SpecificationItemId.ID_PATTERN.matcher(value).matches()) + { fail(lineNumber, "invalid specification item ID '" + value + "'"); } return SpecificationItemId.parseId(value); } - private void beginScenario(final int lineNumber, final String title) { - if (pendingId == null) { + private void beginScenario(final int lineNumber, final String title) + { + if (this.pendingId == null) + { clearMetadata(); return; } - if (!importedIds.add(pendingId)) { - fail(lineNumber, "duplicate Gherkin ID '" + pendingId + "'"); - } - listener.beginSpecificationItem(); - listener.setLocation(file.getPath(), lineNumber); - listener.setId(pendingId); - listener.setTitle(title); - coveredIds.forEach(listener::addCoveredId); - neededArtifactTypes.forEach(listener::addNeededArtifactType); - importingScenario = true; + if (!this.importedIds.add(this.pendingId)) + { + fail(lineNumber, "duplicate Gherkin ID '" + this.pendingId + "'"); + } + this.listener.beginSpecificationItem(); + this.listener.setLocation(this.file.getPath(), lineNumber); + this.listener.setId(this.pendingId); + this.listener.setTitle(title); + this.coveredIds.forEach(this.listener::addCoveredId); + this.neededArtifactTypes.forEach(this.listener::addNeededArtifactType); + this.importingScenario = true; clearMetadata(); } - private void endScenario() { - if (importingScenario) { - listener.endSpecificationItem(); - importingScenario = false; - delayedCoverageEvents.replay(listener); + private void endScenario() + { + if (this.importingScenario) + { + this.listener.endSpecificationItem(); + this.importingScenario = false; + this.delayedCoverageEvents.replay(this.listener); } } - private void clearMetadata() { - pendingId = null; - coveredIds = new LinkedHashSet<>(); - neededArtifactTypes = new LinkedHashSet<>(); - hasNeedsDirective = false; - metadataRegion = false; - tagRegion = false; + private void clearMetadata() + { + this.pendingId = null; + this.coveredIds = new LinkedHashSet<>(); + this.neededArtifactTypes = new LinkedHashSet<>(); + this.hasNeedsDirective = false; + this.metadataRegion = false; + this.tagRegion = false; } - private void fail(final int lineNumber, final String reason) { - throw new IllegalArgumentException(file.getPath() + ":" + lineNumber + ": " + reason); + private void fail(final int lineNumber, final String reason) + { + throw new IllegalArgumentException(this.file.getPath() + ":" + lineNumber + ": " + reason); } } 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 76083d170..b12d5f991 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 @@ -26,12 +26,14 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InOrder; -class GherkinImporterTest { +class GherkinImporterTest +{ private static final GherkinImporterFactory FACTORY = new GherkinImporterFactory(); // [utest->dsn~gherkin.streaming-import~1] @Test - void testImportsScenarioOutlineWithScopedMetadataAndSteps() { + void testImportsScenarioOutlineWithScopedMetadataAndSteps() + { final String source = """ @smoke @unrelated @id:scn~account-login~1 @anotherTag @@ -62,7 +64,8 @@ void testImportsScenarioOutlineWithScopedMetadataAndSteps() { // [utest->dsn~gherkin.importer-selection~1] @Test - void testFactorySupportsFeatureFilesWithHigherPrecedenceThanTagImporter() { + void testFactorySupportsFeatureFilesWithHigherPrecedenceThanTagImporter() + { final InputFile file = StreamInput.forReader(Path.of("specification.feature"), new java.io.BufferedReader(new java.io.StringReader(""))); @@ -73,7 +76,8 @@ void testFactorySupportsFeatureFilesWithHigherPrecedenceThanTagImporter() { // [utest->dsn~gherkin.streaming-import~1] @Test - void testIgnoresScenarioWithoutOftMetadata() { + void testIgnoresScenarioWithoutOftMetadata() + { final List items = importText(""" Feature: login Scenario: ordinary scenario @@ -85,7 +89,8 @@ void testIgnoresScenarioWithoutOftMetadata() { // [utest->dsn~gherkin.streaming-import~1] @Test - void testImportsMultipleCoversDirectives() { + void testImportsMultipleCoversDirectives() + { final String source = """ @id:scn~account-login~1 # Covers: req~login~1 @@ -101,7 +106,8 @@ void testImportsMultipleCoversDirectives() { // [utest->dsn~gherkin.streaming-import~1] @Test - void testRejectsDirectiveWithoutId() { + void testRejectsDirectiveWithoutId() + { final String source = """ @ordinary # Needs: dsn @@ -116,7 +122,8 @@ void testRejectsDirectiveWithoutId() { // [utest->dsn~gherkin.streaming-import~1] @Test - void testIgnoresDirectivesOutsideAnIdMetadataRegion() { + void testIgnoresDirectivesOutsideAnIdMetadataRegion() + { final List items = importText(""" # Covers: req~login~1 Scenario: Login @@ -127,7 +134,8 @@ void testIgnoresDirectivesOutsideAnIdMetadataRegion() { // [utest->dsn~gherkin.streaming-import~1] @Test - void testKeepsMetadataWhenAnUnrelatedCommentPrecedesTheScenario() { + void testKeepsMetadataWhenAnUnrelatedCommentPrecedesTheScenario() + { final List items = importText(""" @id:scn~login~1 # A human-readable comment @@ -140,13 +148,15 @@ void testKeepsMetadataWhenAnUnrelatedCommentPrecedesTheScenario() { // [utest->dsn~gherkin.streaming-import~1] @ParameterizedTest @MethodSource("invalidMetadata") - void testRejectsInvalidMetadata(final String source, final String reason) { + void testRejectsInvalidMetadata(final String source, final String reason) + { final ImporterException exception = assertThrows(ImporterException.class, () -> importText(source)); assertThat(exception.getMessage(), containsString(reason)); } - private static Stream invalidMetadata() { + private static Stream invalidMetadata() + { return Stream.of( Arguments.of(""" @id:invalid @@ -199,7 +209,8 @@ private static Stream invalidMetadata() { // [utest->dsn~gherkin.comment-coverage-tags~1] @Test - void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { + void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() + { final String source = """ @id:scn~ordinary~1 Scenario: ordinary @@ -216,7 +227,8 @@ void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() { // [utest->dsn~gherkin.comment-coverage-tags~1] @Test - void testDelaysCommentCoverageTagsUntilAfterTheScenario() { + void testDelaysCommentCoverageTagsUntilAfterTheScenario() + { final ImportEventListener listener = mock(ImportEventListener.class); final InputFile file = StreamInput.forReader(Path.of("specification.feature"), new BufferedReader(new StringReader(""" @@ -235,7 +247,8 @@ void testDelaysCommentCoverageTagsUntilAfterTheScenario() { events.verify(listener).beginSpecificationItem(); } - private static List importText(final String source) { + private static List importText(final String source) + { return ImportAssertions.runImporterOnText(Path.of("specification.feature"), source, FACTORY); } } diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java index 46db9edcd..55983e28e 100644 --- a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java +++ b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java @@ -9,94 +9,111 @@ import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.junit.jupiter.api.Test; -class TestEventBuffer { +class TestEventBuffer +{ @Test - void testReplaysBeginSpecificationItem() { + void testReplaysBeginSpecificationItem() + { assertEvent(EventBuffer::beginSpecificationItem, ImportEventListener::beginSpecificationItem); } @Test - void testReplaysId() { + void testReplaysId() + { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(buffer -> buffer.setId(id), listener -> verify(listener).setId(id)); } @Test - void testReplaysTitle() { + void testReplaysTitle() + { assertEvent(buffer -> buffer.setTitle("title"), listener -> verify(listener).setTitle("title")); } @Test - void testReplaysStatus() { + void testReplaysStatus() + { assertEvent(buffer -> buffer.setStatus(ItemStatus.DRAFT), listener -> verify(listener).setStatus(ItemStatus.DRAFT)); } @Test - void testReplaysDescription() { + void testReplaysDescription() + { assertEvent(buffer -> buffer.appendDescription("description"), listener -> verify(listener).appendDescription("description")); } @Test - void testReplaysRationale() { + void testReplaysRationale() + { assertEvent(buffer -> buffer.appendRationale("rationale"), listener -> verify(listener).appendRationale("rationale")); } @Test - void testReplaysComment() { + void testReplaysComment() + { assertEvent(buffer -> buffer.appendComment("comment"), listener -> verify(listener).appendComment("comment")); } @Test - void testReplaysCoveredId() { + void testReplaysCoveredId() + { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(buffer -> buffer.addCoveredId(id), listener -> verify(listener).addCoveredId(id)); } @Test - void testReplaysDependencyId() { + void testReplaysDependencyId() + { final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); assertEvent(buffer -> buffer.addDependsOnId(id), listener -> verify(listener).addDependsOnId(id)); } @Test - void testReplaysNeededArtifactType() { + void testReplaysNeededArtifactType() + { assertEvent(buffer -> buffer.addNeededArtifactType("dsn"), listener -> verify(listener).addNeededArtifactType("dsn")); } @Test - void testReplaysTag() { + void testReplaysTag() + { assertEvent(buffer -> buffer.addTag("tag"), listener -> verify(listener).addTag("tag")); } @Test - void testReplaysPathLocation() { + void testReplaysPathLocation() + { assertEvent(buffer -> buffer.setLocation("file.feature", 1), listener -> verify(listener).setLocation("file.feature", 1)); } @Test - void testReplaysLocation() { + void testReplaysLocation() + { final Location location = Location.create("file.feature", 2); assertEvent(buffer -> buffer.setLocation(location), listener -> verify(listener).setLocation(location)); } @Test - void testReplaysForwards() { + void testReplaysForwards() + { assertEvent(buffer -> buffer.setForwards(true), listener -> verify(listener).setForwards(true)); } @Test - void testReplaysEndSpecificationItem() { + void testReplaysEndSpecificationItem() + { assertEvent(EventBuffer::endSpecificationItem, ImportEventListener::endSpecificationItem); } private static void assertEvent(final Consumer addEvent, - final Consumer verifyEvent) { + final Consumer verifyEvent) + { final EventBuffer buffer = new EventBuffer(); addEvent.accept(buffer); final ImportEventListener listener = mock(ImportEventListener.class); From 58f550234f31d3a02f8fcd030546c359763373f5 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 09:57:02 +0200 Subject: [PATCH 13/17] Add method ImportEventListener.addSpecificationItem() This simplifies adding already completed items --- .../api/importer/ImportEventListener.java | 34 +++++++++++++++---- .../importer/SpecificationListBuilder.java | 20 +++++++---- .../TestSpecificationListBuilder.java | 20 ++++++++--- .../importer/gherkin/EventBuffer.java | 6 ++++ 4 files changed, 61 insertions(+), 19 deletions(-) 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 aedb7ef5c..8c99be5a3 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 @@ -35,7 +35,7 @@ public interface ImportEventListener /** * The importer found the status of the specification item - * + * * @param status * the status */ @@ -94,7 +94,7 @@ public interface ImportEventListener /** * Add a tag - * + * * @param tag * the tag */ @@ -102,7 +102,7 @@ public interface ImportEventListener /** * Set the location of the specification item in the imported file - * + * * @param path * the path of the imported file * @param line @@ -117,19 +117,39 @@ public interface ImportEventListener /** * Set the location of the specification item in the imported file - * + * * @param location * the location */ void setLocation(Location location); /** - * Set to {@code true} if the specification item forwards needed - * coverage - * + * Set to {@code true} if the specification item forwards needed coverage + * * @param forwards * {@code true} if the specification item forwards needed * coverage */ void setForwards(boolean forwards); + + /** + * Add a complete specification item to the list of items being built. Use + * this method if the specification item is already complete and does not + * need to be built from events. + *

+ * This is equivalent to the following sequence of method calls: + *

    + *
  • {@link #beginSpecificationItem()}
  • + *
  • {@link #setId(SpecificationItemId)}
  • + *
  • {@link #addCoveredId(SpecificationItemId)}
  • + *
  • {@link #addNeededArtifactType(String)}
  • + *
  • {@link #setLocation(String, int)}
  • + *
  • ...
  • + *
  • {@link #endSpecificationItem()}
  • + *
+ * + * @param item + * the complete specification item + */ + void addSpecificationItem(final SpecificationItem item); } 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 a99ceb6c1..f3bc5d5db 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 @@ -28,7 +28,7 @@ private SpecificationListBuilder(final FilterSettings filterSettings) /** * Creates a new {@link SpecificationListBuilder}. - * + * * @return a new {@link SpecificationListBuilder}. */ public static SpecificationListBuilder create() @@ -39,7 +39,7 @@ public static SpecificationListBuilder create() /** * Creates a new {@link SpecificationListBuilder} with the given * {@link FilterSettings}. - * + * * @param filterSettings * the filter settings for the new builder. * @return a new {@link SpecificationListBuilder}. @@ -139,7 +139,7 @@ public void addTag(final String tag) public List build() { this.endSpecificationItem(); - return Collections.unmodifiableList(this.items) ; + return Collections.unmodifiableList(this.items); } /** @@ -177,14 +177,20 @@ public void endSpecificationItem() { final SpecificationItem item = createNewSpecificationItem(); // [impl->dsn~filtering-by-artifact-types-during-import~1] - if (isAccepted(item)) - { - addNewItemToList(item); - } + addSpecificationItem(item); } resetState(); } + @Override + public void addSpecificationItem(final SpecificationItem item) + { + if (isAccepted(item)) + { + addNewItemToList(item); + } + } + // [impl->dsn~cleaning-imported-multi-line-text-elements~1] private SpecificationItem createNewSpecificationItem() { 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 890d113e1..34205379a 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 @@ -1,8 +1,7 @@ package org.itsallcode.openfasttrace.api.importer; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertAll; import java.util.*; @@ -57,6 +56,17 @@ void testBuildWithTags() assertThat(items.get(0).getTags(), containsInAnyOrder("foo", "bar")); } + @Test + void testAddSpecificationItem() + { + final SpecificationItem item = SpecificationItem.builder().id(ID).build(); + final SpecificationListBuilder builder = SpecificationListBuilder.create(); + + builder.addSpecificationItem(item); + + assertThat(builder.build(), contains(item)); + } + // [utest->dsn~filtering-by-artifact-types-during-import~1] @Test void testFilterArtifactOfType() @@ -164,7 +174,8 @@ void testFilterSpecificationItemsByStatus() addItemWithStatus(builder, "out-B", ItemStatus.APPROVED); addItemWithStatus(builder, "out-C", ItemStatus.PROPOSED); addItemWithStatus(builder, "out-D", ItemStatus.REJECTED); - addItemWithStatus(builder, "out-E", null); // becomes APPROVED by default + addItemWithStatus(builder, "out-E", null); // becomes APPROVED by + // default final List items = builder.build(); assertThat(items.stream().map(SpecificationItem::getName).toList(), containsInAnyOrder("in-A")); @@ -255,7 +266,6 @@ void testMultilineTextFieldsGetTrimmed() assertAll( () -> assertThat(item.getComment(), equalTo("a comment")), () -> assertThat(item.getDescription(), equalTo("a description")), - () -> assertThat(item.getRationale(), equalTo("a rationale")) - ); + () -> assertThat(item.getRationale(), equalTo("a rationale"))); } } diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java index 1333f0bfa..78842d0dd 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java @@ -107,4 +107,10 @@ void replay(final ImportEventListener listener) this.events.forEach(event -> event.accept(listener)); this.events.clear(); } + + @Override + public void addSpecificationItem(final SpecificationItem item) + { + throw new UnsupportedOperationException("Unimplemented method 'addSpecificationItem'"); + } } From ff8b7f2d959cfab895914d317498c3efc93a05c4 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 10:02:12 +0200 Subject: [PATCH 14/17] Simplify tag importers by removing unnecessary complexity and improving readability. --- .../importer/gherkin/EventBuffer.java | 2 +- .../importer/gherkin/GherkinImporterTest.java | 3 +- .../AbstractLightWeightMarkupImporter.java | 15 ++-- .../common/LongTagImportingLineConsumer.java | 78 ++++++++++++------- .../common/ShortTagImportingLineConsumer.java | 16 ++-- .../tag/TestTagImporterWithConfig.java | 18 +++-- 6 files changed, 79 insertions(+), 53 deletions(-) diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java index 78842d0dd..0c3b52d11 100644 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java +++ b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java @@ -111,6 +111,6 @@ void replay(final ImportEventListener listener) @Override public void addSpecificationItem(final SpecificationItem item) { - throw new UnsupportedOperationException("Unimplemented method 'addSpecificationItem'"); + this.events.add(target -> target.addSpecificationItem(item)); } } 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 b12d5f991..1e5b26f00 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 @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -244,7 +245,7 @@ void testDelaysCommentCoverageTagsUntilAfterTheScenario() events.verify(listener).beginSpecificationItem(); events.verify(listener).setId(SpecificationItemId.parseId("scn~ordinary~1")); events.verify(listener).endSpecificationItem(); - events.verify(listener).beginSpecificationItem(); + events.verify(listener).addSpecificationItem(any(SpecificationItem.class)); } private static List importText(final String source) 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 400ae7647..5a05de32a 100644 --- a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java +++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/AbstractLightWeightMarkupImporter.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.importer.lightweightmarkup; import org.itsallcode.openfasttrace.api.core.ItemStatus; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.Importer; @@ -268,15 +269,15 @@ protected void forward() { final ForwardingSpecificationItem forward = new ForwardingSpecificationItem( this.stateMachine.getLastToken()); - this.listener.beginSpecificationItem(); - this.listener.setId(forward.getSkippedId()); - this.listener.addCoveredId(forward.getOriginalId()); + final SpecificationItem.Builder item = SpecificationItem.builder() + .id(forward.getSkippedId()) + .addCoveredId(forward.getOriginalId()) + .forwards(true) + .location(this.file.getPath(), this.currentContext.lineNumber()); for (final String targetArtifactType : forward.getTargetArtifactTypes()) { - this.listener.addNeededArtifactType(targetArtifactType.trim()); + item.addNeedsArtifactType(targetArtifactType.trim()); } - this.listener.setForwards(true); - this.listener.setLocation(this.file.getPath(), this.currentContext.lineNumber()); - this.listener.endSpecificationItem(); + this.listener.addSpecificationItem(item.build()); } } 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 b4aaebd2a..6290593a9 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,6 +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.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.input.InputFile; @@ -14,7 +15,8 @@ // [impl->dsn~import.full-coverage-tag~1] // [impl->dsn~import.full-coverage-tag-with-needed-coverage~1] // [impl->dsn~import.full-coverage-tag-multiple-needed-coverage~1] -class LongTagImportingLineConsumer extends AbstractRegexLineConsumer { +class LongTagImportingLineConsumer extends AbstractRegexLineConsumer +{ private static final Logger LOG = Logger .getLogger(LongTagImportingLineConsumer.class.getName()); @@ -44,42 +46,51 @@ class LongTagImportingLineConsumer extends AbstractRegexLineConsumer { private final InputFile file; private final ImportEventListener listener; - LongTagImportingLineConsumer(final InputFile file, final ImportEventListener listener) { + LongTagImportingLineConsumer(final InputFile file, final ImportEventListener listener) + { super(TAG_REGEX); this.file = file; this.listener = listener; } @Override - public void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount) { + public void processMatch(final Matcher matcher, final int lineNumber, final int lineMatchCount) + { final List coveredIds = parseCoveredIds(matcher.group("coveredIds")); final List neededArtifactTypes = parseNeededArtifactTypes(matcher.group("neededArtifactTypes")); final List generatedIds = createItemIds(matcher, lineNumber, lineMatchCount, coveredIds, neededArtifactTypes); - if (generatedIds.size() > 1) { + if (generatedIds.size() > 1) + { assert generatedIds.size() == coveredIds.size(); - for (int i = 0; i < generatedIds.size(); i++) { + for (int i = 0; i < generatedIds.size(); i++) + { addSpecificationItem(lineNumber, generatedIds.get(i), List.of(coveredIds.get(i)), neededArtifactTypes); } - } else { + } + else + { addSpecificationItem(lineNumber, generatedIds.get(0), coveredIds, neededArtifactTypes); } } private void addSpecificationItem(final int lineNumber, final SpecificationItemId generatedId, - final List coveredIds, final List neededArtifactTypes) { - this.listener.beginSpecificationItem(); - this.listener.setLocation(this.file.getPath(), lineNumber); - this.listener.setId(generatedId); - coveredIds.forEach(this.listener::addCoveredId); - neededArtifactTypes.forEach(this.listener::addNeededArtifactType); - this.listener.endSpecificationItem(); + final List coveredIds, final List neededArtifactTypes) + { + final SpecificationItem.Builder item = SpecificationItem.builder() + .id(generatedId) + .location(this.file.getPath(), lineNumber); + coveredIds.forEach(item::addCoveredId); + neededArtifactTypes.forEach(item::addNeedsArtifactType); + this.listener.addSpecificationItem(item.build()); logItem(lineNumber, coveredIds, neededArtifactTypes, generatedId); } - private static List parseCoveredIds(final String input) { - if (input == null) { + private static List parseCoveredIds(final String input) + { + if (input == null) + { return emptyList(); } return Arrays.stream(input.split(",")) @@ -89,8 +100,10 @@ private static List parseCoveredIds(final String input) { .toList(); } - private static List parseNeededArtifactTypes(final String input) { - if (input == null) { + private static List parseNeededArtifactTypes(final String input) + { + if (input == null) + { return emptyList(); } return Arrays.stream(input.split(",")) @@ -101,16 +114,19 @@ private static List parseNeededArtifactTypes(final String input) { private List createItemIds(final Matcher matcher, final int lineNumber, final int lineMatchCount, final List coveredIds, - final List neededArtifactTypes) { + final List neededArtifactTypes) + { final String artifactType = matcher.group("artifactType"); final String customName = matcher.group("customName"); final String revision = matcher.group("revision"); - if (customName != null) { + if (customName != null) + { return List.of(SpecificationItemId.createId(artifactType, customName, parseRevision(revision))); } final List result = new java.util.ArrayList<>(coveredIds.size()); - for (final SpecificationItemId coveredId : coveredIds) { + for (final SpecificationItemId coveredId : coveredIds) + { final String name = getItemName(lineNumber, lineMatchCount, coveredId, neededArtifactTypes); result.add(SpecificationItemId.createId(artifactType, name, parseRevision(revision))); } @@ -118,30 +134,38 @@ private List createItemIds(final Matcher matcher, final int } private void logItem(final int lineNumber, final List coveredIds, - final List neededArtifactTypes, final SpecificationItemId generatedId) { - if (neededArtifactTypes.isEmpty()) { + final List neededArtifactTypes, final SpecificationItemId generatedId) + { + if (neededArtifactTypes.isEmpty()) + { LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + generatedId + "' covering IDs " + coveredIds); - } else { + } + else + { LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + generatedId + "' covering IDs " + coveredIds + ", needs artifact types " + neededArtifactTypes); } } - private static int parseRevision(final String revision) { + private static int parseRevision(final String revision) + { return Optional.ofNullable(revision).map(Integer::parseInt).orElse(0); } // [impl->dsn~import.full-coverage-tag-with-needed-coverage-readable-names~1] private String getItemName(final int lineNumber, final int lineMatchCount, final SpecificationItemId coveredId, - final List neededArtifactTypes) { - if (neededArtifactTypes.isEmpty()) { + final List neededArtifactTypes) + { + if (neededArtifactTypes.isEmpty()) + { return generateUniqueName(coveredId, lineNumber, lineMatchCount); } return coveredId.getName(); } - private String generateUniqueName(final SpecificationItemId coveredId, final int lineNumber, final int counter) { + private String generateUniqueName(final SpecificationItemId coveredId, final int lineNumber, final int counter) + { final String uniqueName = this.file.getPath() + lineNumber + counter + coveredId; final String checksum = Long.toString(ChecksumCalculator.calculateCrc32(uniqueName)); return coveredId.getName() + "-" + checksum; 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 52c7877a9..d246dbe09 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,6 +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.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.ImporterException; @@ -44,16 +45,11 @@ void processMatch(final Matcher matcher, final int lineNumber, final int lineMat LOG.finest(() -> "File " + this.file + ":" + lineNumber + ": found '" + tagItemId + "' covering id '" + coveredId + "'"); - addItem(lineNumber, coveredId, tagItemId); - } - - private void addItem(final int lineNumber, final SpecificationItemId coveredId, - final SpecificationItemId tagItemId) { - this.listener.beginSpecificationItem(); - this.listener.setLocation(this.file.toString(), lineNumber); - this.listener.setId(tagItemId); - this.listener.addCoveredId(coveredId); - this.listener.endSpecificationItem(); + this.listener.addSpecificationItem(SpecificationItem.builder() + .id(tagItemId) + .location(this.file.toString(), lineNumber) + .addCoveredId(coveredId) + .build()); } private SpecificationItemId createCoveredItem(final String name, final String revision) { 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 17bfb1d1d..103a9a394 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,6 +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.importer.ImportEventListener; import org.itsallcode.openfasttrace.api.importer.ImporterException; @@ -70,8 +71,11 @@ void testFileWithNewTagFormatAlsoSupported() // avoid error in // self-trace runImport("[type->" + itemName + "]"); - verify(this.listenerMock) - .setId(SpecificationItemId.createId("type", "coveredname" + "-3264583751", 0)); + verify(this.listenerMock).addSpecificationItem(SpecificationItem.builder() + .id(SpecificationItemId.createId("type", "coveredname" + "-3264583751", 0)) + .location(FILE.toString(), 1) + .addCoveredId(SpecificationItemId.parseId(itemName)) + .build()); } @Test @@ -139,11 +143,11 @@ void testFileWithLegacyTagFormatWithPrefix() private void verifyTag(final int lineNumber, final SpecificationItemId coveredId, final SpecificationItemId tagItemId) { - this.inOrderListener.verify(this.listenerMock).beginSpecificationItem(); - this.inOrderListener.verify(this.listenerMock).setLocation(FILE.toString(), lineNumber); - this.inOrderListener.verify(this.listenerMock).setId(tagItemId); - this.inOrderListener.verify(this.listenerMock).addCoveredId(coveredId); - this.inOrderListener.verify(this.listenerMock).endSpecificationItem(); + this.inOrderListener.verify(this.listenerMock).addSpecificationItem(SpecificationItem.builder() + .id(tagItemId) + .location(FILE.toString(), lineNumber) + .addCoveredId(coveredId) + .build()); } private void runImport(final String content) From 7fdda54fe30a4c3ed81806d4a2f19a869f4efe12 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 10:10:54 +0200 Subject: [PATCH 15/17] Remove unnecessary EventBuffer --- .../importer/gherkin/EventBuffer.java | 116 ----------------- .../importer/gherkin/GherkinLineConsumer.java | 13 +- .../importer/gherkin/GherkinImporterTest.java | 4 +- .../importer/gherkin/TestEventBuffer.java | 123 ------------------ 4 files changed, 3 insertions(+), 253 deletions(-) delete mode 100644 importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java delete mode 100644 importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java diff --git a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java b/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java deleted file mode 100644 index 0c3b52d11..000000000 --- a/importer/gherkin/src/main/java/org/itsallcode/openfasttrace/importer/gherkin/EventBuffer.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.itsallcode.openfasttrace.importer.gherkin; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import org.itsallcode.openfasttrace.api.core.*; -import org.itsallcode.openfasttrace.api.importer.ImportEventListener; - -/** Buffers legacy coverage-tag events until the current scenario has ended. */ -final class EventBuffer implements ImportEventListener -{ - private final List> events = new ArrayList<>(); - - @Override - public void beginSpecificationItem() - { - this.events.add(ImportEventListener::beginSpecificationItem); - } - - @Override - public void setId(final SpecificationItemId id) - { - this.events.add(target -> target.setId(id)); - } - - @Override - public void setTitle(final String title) - { - this.events.add(target -> target.setTitle(title)); - } - - @Override - public void setStatus(final ItemStatus status) - { - this.events.add(target -> target.setStatus(status)); - } - - @Override - public void appendDescription(final String fragment) - { - this.events.add(target -> target.appendDescription(fragment)); - } - - @Override - public void appendRationale(final String fragment) - { - this.events.add(target -> target.appendRationale(fragment)); - } - - @Override - public void appendComment(final String fragment) - { - this.events.add(target -> target.appendComment(fragment)); - } - - @Override - public void addCoveredId(final SpecificationItemId id) - { - this.events.add(target -> target.addCoveredId(id)); - } - - @Override - public void addDependsOnId(final SpecificationItemId id) - { - this.events.add(target -> target.addDependsOnId(id)); - } - - @Override - public void addNeededArtifactType(final String artifactType) - { - this.events.add(target -> target.addNeededArtifactType(artifactType)); - } - - @Override - public void addTag(final String tag) - { - this.events.add(target -> target.addTag(tag)); - } - - @Override - public void setLocation(final String path, final int line) - { - this.events.add(target -> target.setLocation(path, line)); - } - - @Override - public void endSpecificationItem() - { - this.events.add(ImportEventListener::endSpecificationItem); - } - - @Override - public void setLocation(final Location location) - { - this.events.add(target -> target.setLocation(location)); - } - - @Override - public void setForwards(final boolean forwards) - { - this.events.add(target -> target.setForwards(forwards)); - } - - void replay(final ImportEventListener listener) - { - this.events.forEach(event -> event.accept(listener)); - this.events.clear(); - } - - @Override - public void addSpecificationItem(final SpecificationItem item) - { - this.events.add(target -> target.addSpecificationItem(item)); - } -} 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 1071bc1ac..d49f0b0e8 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 @@ -27,10 +27,6 @@ final class GherkinLineConsumer implements LineConsumer private final InputFile file; private final ImportEventListener listener; private final LineConsumer coverageTagParser; - // Legacy tags emit complete items and must not interleave with an open - // scenario. - private final LineConsumer delayedCoverageTagParser; - private final EventBuffer delayedCoverageEvents = new EventBuffer(); private final Set importedIds = new LinkedHashSet<>(); private SpecificationItemId pendingId; private Set coveredIds = new LinkedHashSet<>(); @@ -45,7 +41,6 @@ final class GherkinLineConsumer implements LineConsumer this.file = file; this.listener = listener; this.coverageTagParser = CoverageTagParser.create(null, file, listener); - this.delayedCoverageTagParser = CoverageTagParser.create(null, file, this.delayedCoverageEvents); } @Override @@ -53,7 +48,7 @@ public void readLine(final int lineNumber, final String line) { if (line.trim().startsWith("#")) { - getCoverageTagParser().readLine(lineNumber, line); + this.coverageTagParser.readLine(lineNumber, line); } final Matcher scenario = SCENARIO.matcher(line); if (scenario.matches()) @@ -85,11 +80,6 @@ public void finish() endScenario(); } - private LineConsumer getCoverageTagParser() - { - return this.importingScenario ? this.delayedCoverageTagParser : this.coverageTagParser; - } - private void readMetadata(final int lineNumber, final String line) { if (line.trim().startsWith("@")) @@ -234,7 +224,6 @@ private void endScenario() { this.listener.endSpecificationItem(); this.importingScenario = false; - this.delayedCoverageEvents.replay(this.listener); } } 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 1e5b26f00..a149a62f5 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 @@ -228,7 +228,7 @@ void testImportsCommentCoverageTagsButIgnoresExecutableCoverageTags() // [utest->dsn~gherkin.comment-coverage-tags~1] @Test - void testDelaysCommentCoverageTagsUntilAfterTheScenario() + void testImportsCommentCoverageTagsWhileScenarioIsOpen() { final ImportEventListener listener = mock(ImportEventListener.class); final InputFile file = StreamInput.forReader(Path.of("specification.feature"), @@ -244,8 +244,8 @@ void testDelaysCommentCoverageTagsUntilAfterTheScenario() final InOrder events = inOrder(listener); events.verify(listener).beginSpecificationItem(); events.verify(listener).setId(SpecificationItemId.parseId("scn~ordinary~1")); - events.verify(listener).endSpecificationItem(); events.verify(listener).addSpecificationItem(any(SpecificationItem.class)); + events.verify(listener).endSpecificationItem(); } private static List importText(final String source) diff --git a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java b/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java deleted file mode 100644 index 55983e28e..000000000 --- a/importer/gherkin/src/test/java/org/itsallcode/openfasttrace/importer/gherkin/TestEventBuffer.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.itsallcode.openfasttrace.importer.gherkin; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.util.function.Consumer; - -import org.itsallcode.openfasttrace.api.core.*; -import org.itsallcode.openfasttrace.api.importer.ImportEventListener; -import org.junit.jupiter.api.Test; - -class TestEventBuffer -{ - @Test - void testReplaysBeginSpecificationItem() - { - assertEvent(EventBuffer::beginSpecificationItem, ImportEventListener::beginSpecificationItem); - } - - @Test - void testReplaysId() - { - final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); - assertEvent(buffer -> buffer.setId(id), listener -> verify(listener).setId(id)); - } - - @Test - void testReplaysTitle() - { - assertEvent(buffer -> buffer.setTitle("title"), listener -> verify(listener).setTitle("title")); - } - - @Test - void testReplaysStatus() - { - assertEvent(buffer -> buffer.setStatus(ItemStatus.DRAFT), - listener -> verify(listener).setStatus(ItemStatus.DRAFT)); - } - - @Test - void testReplaysDescription() - { - assertEvent(buffer -> buffer.appendDescription("description"), - listener -> verify(listener).appendDescription("description")); - } - - @Test - void testReplaysRationale() - { - assertEvent(buffer -> buffer.appendRationale("rationale"), - listener -> verify(listener).appendRationale("rationale")); - } - - @Test - void testReplaysComment() - { - assertEvent(buffer -> buffer.appendComment("comment"), - listener -> verify(listener).appendComment("comment")); - } - - @Test - void testReplaysCoveredId() - { - final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); - assertEvent(buffer -> buffer.addCoveredId(id), listener -> verify(listener).addCoveredId(id)); - } - - @Test - void testReplaysDependencyId() - { - final SpecificationItemId id = SpecificationItemId.parseId("req~login~1"); - assertEvent(buffer -> buffer.addDependsOnId(id), listener -> verify(listener).addDependsOnId(id)); - } - - @Test - void testReplaysNeededArtifactType() - { - assertEvent(buffer -> buffer.addNeededArtifactType("dsn"), - listener -> verify(listener).addNeededArtifactType("dsn")); - } - - @Test - void testReplaysTag() - { - assertEvent(buffer -> buffer.addTag("tag"), listener -> verify(listener).addTag("tag")); - } - - @Test - void testReplaysPathLocation() - { - assertEvent(buffer -> buffer.setLocation("file.feature", 1), - listener -> verify(listener).setLocation("file.feature", 1)); - } - - @Test - void testReplaysLocation() - { - final Location location = Location.create("file.feature", 2); - assertEvent(buffer -> buffer.setLocation(location), listener -> verify(listener).setLocation(location)); - } - - @Test - void testReplaysForwards() - { - assertEvent(buffer -> buffer.setForwards(true), listener -> verify(listener).setForwards(true)); - } - - @Test - void testReplaysEndSpecificationItem() - { - assertEvent(EventBuffer::endSpecificationItem, ImportEventListener::endSpecificationItem); - } - - private static void assertEvent(final Consumer addEvent, - final Consumer verifyEvent) - { - final EventBuffer buffer = new EventBuffer(); - addEvent.accept(buffer); - final ImportEventListener listener = mock(ImportEventListener.class); - buffer.replay(listener); - verifyEvent.accept(listener); - } -} From b41045a5591fb8b56da499b41bc54b91f22c12af Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 10:47:56 +0200 Subject: [PATCH 16/17] Improve comments --- .../api/importer/ImportEventListener.java | 3 ++- .../api/importer/TestSpecificationListBuilder.java | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) 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 8c99be5a3..19d5ca7e7 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 @@ -135,7 +135,8 @@ public interface ImportEventListener /** * Add a complete specification item to the list of items being built. Use * this method if the specification item is already complete and does not - * need to be built from events. + * need to be built from events, e.g. when the item is specified in a single + * line and does not span multiple lines in the input file. *

* This is equivalent to the following sequence of method calls: *

    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 34205379a..8acc9209d 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 @@ -163,10 +163,8 @@ void testDuplicateIdNotIgnored() @Test void testFilterSpecificationItemsByStatus() { - final Set wantedStatuses = new HashSet<>(); - wantedStatuses.add(ItemStatus.DRAFT); - final FilterSettings filterSettings = FilterSettings.builder() // - .wantedStatuses(wantedStatuses) // + final FilterSettings filterSettings = FilterSettings.builder() + .wantedStatuses(Set.of(ItemStatus.DRAFT)) .build(); final SpecificationListBuilder builder = SpecificationListBuilder .createWithFilter(filterSettings); @@ -174,8 +172,8 @@ void testFilterSpecificationItemsByStatus() addItemWithStatus(builder, "out-B", ItemStatus.APPROVED); addItemWithStatus(builder, "out-C", ItemStatus.PROPOSED); addItemWithStatus(builder, "out-D", ItemStatus.REJECTED); - addItemWithStatus(builder, "out-E", null); // becomes APPROVED by - // default + // out-E becomes APPROVED by default + addItemWithStatus(builder, "out-E", null); final List items = builder.build(); assertThat(items.stream().map(SpecificationItem::getName).toList(), containsInAnyOrder("in-A")); From a2e77c33a831bb99dc948fceca8f0ca9506ec1d9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Fri, 24 Jul 2026 10:48:57 +0200 Subject: [PATCH 17/17] Remove already implemented changeset --- ...gherkin-feature-specification-documents.md | 156 ------------------ 1 file changed, 156 deletions(-) delete mode 100644 doc/changesets/563-support-gherkin-feature-specification-documents.md diff --git a/doc/changesets/563-support-gherkin-feature-specification-documents.md b/doc/changesets/563-support-gherkin-feature-specification-documents.md deleted file mode 100644 index 1a5661722..000000000 --- a/doc/changesets/563-support-gherkin-feature-specification-documents.md +++ /dev/null @@ -1,156 +0,0 @@ -# GH-563 Support Gherkin `.feature` Files As OpenFastTrace Specification Documents - -## Goal - -Allow OpenFastTrace to import specification items from Gherkin `Scenario` and -`Scenario Outline` blocks in `.feature` files. Preserve legacy coverage tags -when they are written in Gherkin comments, while avoiding coverage-tag regular -expression evaluation for executable Gherkin lines. - -## Scope - -In scope: - -* Extract shared line scanning and long/short coverage-tag parsing into a - prerequisite module. -* Add a dedicated Gherkin importer that composes the shared tag parser. -* Import OFT scenario items identified by `@id:`. -* Parse scoped `# Covers:` and `# Needs:` metadata strictly. -* Preserve comment-based legacy coverage tags in `.feature` files and all - existing tag importer behavior for non-`.feature` inputs. -* Update traced requirements, design, tests, and user documentation. - -Out of scope: - -* Importing `Feature`, `Rule`, `Background`, or `Examples` as specification - items. -* Supporting coverage tags in executable Gherkin lines. -* Adding an external Gherkin parser dependency. -* Moving Gherkin grammar or validation into the shared parser module. - -## Design References - -* [System Requirements](../spec/system_requirements.md) -* [Design](../spec/design.md) -* [Quality Requirements](../spec/design/quality_requirements.md) -* [User Guide](../user_guide.md) - -## Strategy - -1. Merge a behavior-preserving refactoring PR that introduces a shared module - for line scanning and long/short coverage-tag parsing. It retains `.feature` - support in the tag importer. -2. Atomically move `.feature` ownership from the tag importer to a new Gherkin - importer with higher precedence when the Gherkin importer is registered. -3. Implement Gherkin parsing as a single-pass state machine. It receives every - line but forwards only comment lines to the shared coverage-tag parser. -4. Keep all Gherkin syntax, state, validation, and `ImportEventListener` - mapping in the Gherkin importer. - -## Gherkin Syntax And Behavior - -* A Gherkin scenario is an OFT item only when its immediately preceding, - contiguous Gherkin tag region contains exactly one - `@id:` tag. Other Gherkin tags are ignored. -* Directives are recognized only in `#` comment lines after that ID tag region - and before the associated `Scenario:` or `Scenario Outline:` header. - Comments elsewhere are ignored by Gherkin metadata parsing. -* `# Covers:` and `# Needs:` are case-sensitive. Each is optional but may occur - at most once. When present, it must contain a non-empty comma-separated list; - duplicate values and malformed IDs or artifact types are errors. -* A repeated or invalid `@id` tag, or a scoped directive without exactly one - valid ID, fails the import with an `ImporterException` containing the file, - line, and reason. Scenarios without OFT metadata remain ignored. -* The scenario header line is the item location. Text after `Scenario:` or - `Scenario Outline:` is the title. The importer streams the scenario-step - block into the description, excluding comments and `Examples`; it ends the - item at the next `Scenario`, `Scenario Outline`, `Feature`, `Rule`, - `Background`, `Examples`, or end of file. -* The importer retains only active metadata and previously imported Gherkin IDs - for duplicate detection. It does not buffer a complete file or description. - -## Task List - -- [ ] Create and checkout branch - `feature/563_support_gherkin_feature_specification_documents`. - -### PR 1: Shared Coverage-Tag Parser Refactoring - -- [ ] Add `importer/tag-importer-common` with artifact ID - `openfasttrace-importer-tag-importer-common` to the Maven reactor. -- [ ] Add JPMS module `org.itsallcode.openfasttrace.importer.tag.common` and - export only `LineReader` (including its line-consumer contract) and - `CoverageTagParser` from - `org.itsallcode.openfasttrace.importer.tag.common`. -- [ ] Move line scanning, line-handler composition, regex matching, long/short - coverage-tag parsing, and CRC32 ID generation from `importer/tag` into - the shared module without changing parsing semantics, generated IDs, - listener events, logging, or exception wrapping. -- [ ] Define `CoverageTagParser.create(PathConfig, InputFile, - ImportEventListener)` to compose the long-tag parser and, when a path - configuration is present, the short-tag parser into one line consumer. - Keep parser implementation classes encapsulated. -- [ ] Refactor `openfasttrace-importer-tag` into a thin adapter that creates - the shared parser and scans its input once with the shared `LineReader`. - Keep all supported extensions, including `.feature`, unchanged in this - refactoring PR. -- [ ] Move scanner tests to the shared module and add focused shared-parser - tests for representative long and configured short tags, asserting - listener events, locations, generated IDs, coverage links, and needed - artifact types. -- [ ] Keep the existing tag-importer parsing and factory/configuration tests as - regression tests, including `.feature` support, to prove that the - refactoring is behavior-preserving. - -### Requirements And Design - -- [ ] Add requirements for importing Gherkin scenarios and outlines, strict - scoped metadata validation, Gherkin importer selection, and comment-only - legacy coverage-tag compatibility. -- [ ] Stop and ask user for review of the updated system requirements. -- [ ] Add design items for factory precedence, the streaming Gherkin state - machine, metadata scope, event mapping, and shared-parser delegation. -- [ ] Stop and ask user for review of the updated design. - -### PR 2: Gherkin Importer - -- [ ] Add `importer/gherkin` with artifact ID - `openfasttrace-importer-gherkin`; register it in the Maven reactor and - product dependencies. -- [ ] Provide a Gherkin importer factory for `.feature` files with priority - `9000`, ahead of the tag importer's priority `10000`. -- [ ] Implement the defined single-pass Gherkin state machine and map imported - scenario fragments to `ImportEventListener` events. -- [ ] Inject the shared coverage-tag parser and forward only lines whose - trimmed form starts with `#` to it. -- [ ] Implement the specified `@id:`, `Covers`, `Needs`, title, description, - boundary, duplicate-ID, and error behavior. -- [ ] Preserve the shared parser's existing `ImporterException` behavior for - legacy coverage tags; do not introduce a new shared validation exception. - -### Verification - -- [ ] Add Gherkin importer unit tests for valid scenarios and outlines, - location/title/description extraction, coverage metadata, non-OFT tags, - and ignored ordinary scenarios. -- [ ] Add validation tests for invalid or multiple IDs, orphan directives, - repeated or empty directives, malformed list entries, duplicate metadata - values, and duplicate Gherkin IDs. Assert exception type and relevant - message content. -- [ ] Add regression tests proving comment coverage tags import in `.feature` - files, non-comment coverage-tag text is ignored in `.feature` files, and - tag importer behavior for non-`.feature` inputs is unchanged. -- [ ] Add pipeline tests proving each Gherkin file is scanned once and only - comment lines reach the shared coverage-tag parser. -- [ ] Add product-level tests for Gherkin importer precedence and mixed - scenario specifications with comment-based legacy coverage tags. -- [ ] Run `./oft-self-trace.sh` and ensure the trace stays clean. -- [ ] Run `mvn -T 1C verify` and ensure all quality gates pass. - -### Documentation And Changelog - -- [ ] Extend [doc/user_guide.md](../user_guide.md) with the `.feature` syntax, - placement rules, validation behavior, and examples. -- [ ] Update [.agents/skills/openfasttrace/SKILL.md](../../.agents/skills/openfasttrace/SKILL.md) - with the Gherkin syntax and comment-only compatibility rule. -- [ ] Add the GH-563 entry to [doc/changes/changes_4.6.0.md](../changes/changes_4.6.0.md).