From 1793dd649f920192662876ac5b713e298a70a717 Mon Sep 17 00:00:00 2001 From: Iwan Eising Date: Wed, 5 Aug 2026 15:19:05 +0400 Subject: [PATCH 1/2] docs(gherkin-to-asciidoc): document the indexing property and the includeSubDirs/groupByFeature default changes PR #99 shipped the indexing DSL property and flipped includeSubDirs/ groupByFeature to default to true, but never touched the plugin's own README - it still showed the old false defaults everywhere and had no mention of indexing at all. Updates the Groovy/Kotlin DSL samples, the configuration properties table, and the validation notes for the new defaults and the new property. Adds a "Numbering Features and Scenarios" section describing all four indexing values, the file processing/numbering order, and a before/after example, linking to the new indexing example. Updates "Sample Output"/"Grouping by Feature" to show the new default (grouped) output first, with groupByFeature = false now documented as the opt-out. Co-Authored-By: Claude Sonnet 5 --- gherkin-to-asciidoc/README.adoc | 170 ++++++++++++++++++++++++++++---- 1 file changed, 150 insertions(+), 20 deletions(-) diff --git a/gherkin-to-asciidoc/README.adoc b/gherkin-to-asciidoc/README.adoc index d786275..b7d62dc 100644 --- a/gherkin-to-asciidoc/README.adoc +++ b/gherkin-to-asciidoc/README.adoc @@ -67,10 +67,12 @@ All properties have sensible defaults and do not need to be set for a typical pr .Groovy DSL [source,groovy] ---- +import com.arc_e_tect.gradle.gherkin.indexing.IndexingMode // only needed if you set `indexing` + gherkinToAsciidoc { - // One or more directories containing .feature files to scan (scanned non-recursively - // by default). Default: 'src/test/resources/features' relative to the project directory, - // used only when sourceDirs is left empty and sourceFile is not set. + // One or more directories containing .feature files to scan (scanned recursively by + // default - see includeSubDirs below). Default: 'src/test/resources/features' relative + // to the project directory, used only when sourceDirs is left empty and sourceFile is not set. // Mutually exclusive with sourceFile. sourceDirs.from('src/test/resources/features') // Add more directories by calling .from(...) again, or in the same call: @@ -81,9 +83,9 @@ gherkinToAsciidoc { // sourceFile = file('src/test/resources/features/login.feature') // When true, recursively scan all sub-directories of every directory in sourceDirs. - // Cannot be used when sourceFile is set. - // Default: false - includeSubDirs = false + // Cannot be used when sourceFile is set - explicitly set this to false when using sourceFile. + // Default: true + includeSubDirs = true // Directory where the generated AsciiDoc file will be written. // Default: build/generated-docs @@ -107,8 +109,8 @@ gherkinToAsciidoc { // When true, groups scenarios under their enclosing Feature instead of a flat list // (see "Grouping by Feature" below). Forced to true whenever trackProgress is true. - // Default: false - groupByFeature = false + // Default: true + groupByFeature = true // Directory that the listed.adoc/defined.adoc/implemented.adoc report snippets are written // to when trackProgress is true (see "Report Snippets and Custom Templates" below). @@ -125,23 +127,32 @@ gherkinToAsciidoc { // top of the generated document (see "System Under Test Version" below). // Default: the project's own `version` // systemUnderTestVersion = 'v1.0.0' + + // Numbers Feature/Scenario titles directly in the source .feature files (see "Numbering + // Features and Scenarios" below). Requires includeSubDirs = true; FEATURE and ALL also + // require groupByFeature = true. + // Default: IndexingMode.OFF + indexing = IndexingMode.OFF } ---- .Kotlin DSL (`build.gradle.kts`) [source,kotlin] ---- +import com.arc_e_tect.gradle.gherkin.indexing.IndexingMode // only needed if you set `indexing` + gherkinToAsciidoc { sourceDirs.from("src/test/resources/features") - includeSubDirs.set(false) + includeSubDirs.set(true) outputDir.set(layout.buildDirectory.dir("generated-docs")) outputFileName.set("features.adoc") trackProgress.set(false) // glueCodeDirs.from("src/test/java/com/example/steps") - groupByFeature.set(false) + groupByFeature.set(true) // snippetDir.set(layout.buildDirectory.dir("generated-docs/features/snippets")) // template.set(file("templates/report.mustache")) // systemUnderTestVersion.set("v1.0.0") + indexing.set(IndexingMode.OFF) } ---- @@ -154,21 +165,23 @@ gherkinToAsciidoc { Mutually exclusive with `sourceFile`. | `sourceFile` | File | — | Single `.feature` file to parse. Mutually exclusive with `sourceDirs` and `includeSubDirs`. -| `includeSubDirs`| Boolean | `false` | When `true`, recursively scans all sub-directories of every directory in `sourceDirs`. -Cannot be combined with `sourceFile`. +| `includeSubDirs`| Boolean | `true` | When `true`, recursively scans all sub-directories of every directory in `sourceDirs`. +Cannot be combined with `sourceFile` - explicitly set this to `false` when using `sourceFile`. | `outputDir` | Directory | `build/generated-docs` | Directory where the generated AsciiDoc file is written. | `outputFileName`| String | `features.adoc` | Name of the generated AsciiDoc file. | `trackProgress` | Boolean | `false` | When `true`, classifies every scenario as `listed`, `defined`, or `implemented` and adds a progress summary to the generated document. Requires `sourceDirs` and `glueCodeDirs`; implies `includeSubDirs = true`. | `glueCodeDirs` | File collection | — | One or more directories containing the Cucumber-JVM glue code, each scanned recursively for step definitions. Required when `trackProgress` is `true`. -| `groupByFeature`| Boolean | `false` | When `true`, groups scenarios under their enclosing `Feature` instead of a flat list. +| `groupByFeature`| Boolean | `true` | When `true`, groups scenarios under their enclosing `Feature` instead of a flat list. Forced to `true` whenever `trackProgress` is `true`. | `snippetDir` | Directory | `build/generated-docs/features/snippets` | Directory that the `listed.adoc`/`defined.adoc`/`implemented.adoc` report snippets are written to. Only used when `trackProgress` is `true`. | `template` | File | — | Optional Mustache template used to render the report so it references the snippets via `include::` directives instead of embedding their content verbatim. Only consulted when `trackProgress` is `true`. | `systemUnderTestVersion` | String | the project's `version` | Version of the system under test that the reported scenarios exercise, printed near the top of the generated document (see "System Under Test Version" below). +| `indexing` | `IndexingMode` (`OFF`/`FEATURE`/`SCENARIO`/`ALL`) | `OFF` | Numbers `Feature`/`Scenario` titles directly in the source `.feature` files (see "Numbering Features and Scenarios" below). +Requires `includeSubDirs = true`; `FEATURE` and `ALL` additionally require `groupByFeature = true`. |=== [IMPORTANT] @@ -176,13 +189,19 @@ Only consulted when `trackProgress` is `true`. `sourceDirs` and `sourceFile` are mutually exclusive. Setting both will cause the `generateFeatureDocs` task to fail with a descriptive error. -`includeSubDirs` cannot be combined with `sourceFile`. +`includeSubDirs` cannot be combined with `sourceFile`. Since `includeSubDirs` now defaults to `true`, +a project using `sourceFile` must explicitly set `includeSubDirs = false` - otherwise +`generateFeatureDocs` fails with a descriptive error. `trackProgress` can only be enabled when `sourceDirs` is configured, and requires `glueCodeDirs` to be set. Enabling it implies `includeSubDirs = true` and `groupByFeature = true`, regardless of those properties' own configured values. `snippetDir` and `template` are only consulted when `trackProgress` is `true`; both are ignored otherwise. + +`indexing` can only be set to something other than `OFF` when `includeSubDirs` is `true`. `FEATURE` and `ALL` +additionally require `groupByFeature` to be `true`; with `groupByFeature = false`, only `OFF` and `SCENARIO` +are allowed. Setting an invalid combination fails `generateFeatureDocs` with a descriptive error. ==== == Running the Task @@ -234,6 +253,9 @@ sub-project that inherits or sets `trackProgress = true` still needs its own `gl | Inherited from the root project by default (subject to the usual `trackProgress` implication). | `groupByFeature` | Inherited from the root project by default (subject to the usual `trackProgress` implication). +| `indexing` +| Inherited from the root project by default; a sub-project can override it independently, subject to +the usual `includeSubDirs`/`groupByFeature` validation constraints for whichever value is in effect. | `outputFileName` | Inherited from the root project by default. | `template` @@ -330,7 +352,7 @@ Feature: User authentication | bob | wrong | failure | ---- -The plugin generates: +By default (`groupByFeature = true`), the plugin generates: [source,asciidoc] ---- @@ -342,15 +364,16 @@ System Under Test version: 1.0.0 This document lists every `Scenario` and `Scenario Outline` found under the configured feature file directories. +== User authentication + * Scenario: User logs in successfully * Scenario Outline: User logs in with different credential sets ---- == Grouping by Feature -By default, scenarios are listed as one flat list regardless of which `.feature` file they came from. Setting -`groupByFeature = true` groups them under their enclosing `Feature` instead — useful once `sourceDirs` spans -more than one feature area. Given two feature files: +By default, scenarios are grouped under their enclosing `Feature`, as shown above — useful once `sourceDirs` +spans more than one feature area, as with two feature files: [source,gherkin] ---- @@ -365,8 +388,6 @@ Feature: Invoice payment Given an outstanding invoice ---- -with `groupByFeature = true`, the plugin generates: - [source,asciidoc] ---- = Feature Scenarios @@ -386,6 +407,115 @@ This document lists every `Scenario` and `Scenario Outline` found under the conf * Scenario: User pays an invoice ---- +Setting `groupByFeature = false` instead lists every scenario as one flat list, regardless of which +`.feature` file it came from: + +[source,asciidoc] +---- += Feature Scenarios +:toc: +:toclevels: 2 + +System Under Test version: 1.0.0 + +This document lists every `Scenario` and `Scenario Outline` found under the configured feature file directories. + +* Scenario: User logs in successfully +* Scenario: User pays an invoice +---- + +== Numbering Features and Scenarios + +Setting `indexing` numbers `Feature`/`Scenario` titles directly in the source `.feature` files - not just in +the generated report - so the numbering is visible wherever the feature file itself is read (in an editor, in +a Cucumber test run, ...), not only in `generateFeatureDocs`'s own output. + +[cols="1,3",options="header"] +|=== +| Value | Effect + +| `OFF` | Nothing is numbered (default). Any numbering left over from a previous run is removed. +| `FEATURE` | Every feature is numbered, e.g. `Feature: 1 - User authentication`. Scenario titles are untouched. +| `SCENARIO` | Every scenario is numbered continuously across all feature files, e.g. `Scenario: 1 - User logs in`. Feature titles are untouched. +| `ALL` | Both are numbered; scenarios are numbered per feature as `.`, e.g. `Scenario: 1.1 - User logs in` within `Feature: 1 - User authentication`. +|=== + +Feature files are processed - and numbered - in the same order the generated report lists them in: for each +source directory (directories themselves ordered alphabetically by path when more than one is configured), +that directory's own feature files first, alphabetically by file name, and only then its sub-directories' +files, each sub-directory visited the same way, alphabetically by name. Scenario numbers additionally follow +document order within each file. + +Given two feature files, `features-auth/authentication.feature`: + +[source,gherkin] +---- +Feature: User authentication + + Scenario: User requests a password reset + + Scenario: User logs in successfully + Given the login page is open +---- + +and `features-billing/invoice.feature`: + +[source,gherkin] +---- +Feature: Invoice payment + + Scenario: User pays an invoice + Given an outstanding invoice +---- + +with `indexing = IndexingMode.ALL`, the plugin rewrites both files in place: + +[source,gherkin] +---- +Feature: 1 - User authentication + + Scenario: 1.1 - User requests a password reset + + Scenario: 1.2 - User logs in successfully + Given the login page is open +---- + +[source,gherkin] +---- +Feature: 2 - Invoice payment + + Scenario: 2.1 - User pays an invoice + Given an outstanding invoice +---- + +and the generated report reflects the same numbering, since it's parsed from the now-rewritten source files: + +[source,asciidoc] +---- +== 1 - User authentication + +* Scenario: 1.1 - User requests a password reset +* Scenario: 1.2 - User logs in successfully + +== 2 - Invoice payment + +* Scenario: 2.1 - User pays an invoice +---- + +[IMPORTANT] +==== +Changing `indexing` - including setting it back to `OFF` - rewrites the source `.feature` files on the next +`generateFeatureDocs` run: any numbering left over from a previous run is stripped first, then fresh +numbering is applied for the new mode. This makes the operation idempotent (re-running with the same mode is +a no-op once the files are already correctly numbered), but it does mean the task mutates files that are also +its own inputs - commit the resulting numbered `.feature` files like any other source change. +==== + +See the +link:../examples/gherkin-to-asciidoc/indexing/README.adoc[indexing example] +for all four modes applied to the same feature files side by side, with the exact before/after content and +generated reports for each. + == Tracking Implementation Progress Setting `trackProgress = true` (with `sourceDirs` and `glueCodeDirs` configured) switches the generated document From f26987b71fc50f3ea9c687fb8b07943d6afb289e Mon Sep 17 00:00:00 2001 From: Iwan Eising Date: Wed, 5 Aug 2026 15:49:18 +0400 Subject: [PATCH 2/2] feat(gherkin-to-asciidoc): add ci indexing value and a CLI override for the whole build Adds IndexingMode.CI: unlike OFF, it skips indexing entirely - the source .feature files aren't touched at all, not even to strip prior numbering. Adds the -PgherkinToAsciidoc.indexing= project property, which overrides indexing for every project in the build regardless of what each project's own gherkinToAsciidoc { } block configures, matched against IndexingMode case-insensitively. Intended usage is forcing `ci` in a CI pipeline so generateFeatureDocs never mutates source files there, without changing any build script. Documents both in the plugin README (new "Overriding indexing from the Command Line" subsection, updated tables/validation notes) and adds a fifth `ci` sub-project to the indexing example, whose feature files start out already numbered specifically to prove CI leaves that numbering untouched, plus a walkthrough of the CLI override forcing all five sub-projects to skip indexing for a single run. Also fixes a pre-existing bug in the example (introduced when it was first added): the feature/scenario/all sub-projects' committed .feature files were left numbered from local verification instead of the clean baseline the README describes. Pins the example to gherkin-to-asciidoc = 2.1.0, the version this purely additive change is expected to release as; verified locally against the plugin's own source (via a temporary includeBuild, since removed) before pinning. Co-Authored-By: Claude Sonnet 5 --- .../gherkin-to-asciidoc/indexing/README.adoc | 130 +++++++++++++++--- .../features-auth/authentication.feature | 8 +- .../features-billing/invoice.feature | 4 +- .../gherkin-to-asciidoc/indexing/build.gradle | 12 +- .../indexing/ci/build.gradle | 16 +++ .../features-auth/authentication.feature | 19 +++ .../features-billing/invoice.feature | 6 + .../features-auth/authentication.feature | 2 +- .../features-billing/invoice.feature | 2 +- .../indexing/gradle/libs.versions.toml | 2 +- .../features-auth/authentication.feature | 6 +- .../features-billing/invoice.feature | 2 +- .../indexing/settings.gradle | 2 +- gherkin-to-asciidoc/README.adoc | 46 +++++-- .../gherkin/GenerateFeatureDocsTask.java | 21 ++- .../gherkin/GherkinToAsciidocExtension.java | 28 +++- .../gherkin/GherkinToAsciidocPlugin.java | 33 ++++- .../gherkin/indexing/FeatureIndexer.java | 4 + .../gradle/gherkin/indexing/IndexingMode.java | 12 +- .../gherkin/GherkinToAsciidocPluginTest.java | 95 ++++++++++++- 20 files changed, 388 insertions(+), 62 deletions(-) create mode 100644 examples/gherkin-to-asciidoc/indexing/ci/build.gradle create mode 100644 examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-auth/authentication.feature create mode 100644 examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-billing/invoice.feature diff --git a/examples/gherkin-to-asciidoc/indexing/README.adoc b/examples/gherkin-to-asciidoc/indexing/README.adoc index 38290e0..43f1631 100644 --- a/examples/gherkin-to-asciidoc/indexing/README.adoc +++ b/examples/gherkin-to-asciidoc/indexing/README.adoc @@ -4,18 +4,20 @@ == Description This example applies the Gherkin to AsciiDoc plugin to a Gradle multi-project build made up of a root -project and four sub-projects - `off`, `feature`, `scenario`, and `all` - one per value of the `indexing` -DSL property. Every sub-project starts out with byte-for-byte identical copies of the same two -`.feature` files; the *only* difference between them is the `indexing` value each one configures. +project and five sub-projects - `off`, `feature`, `scenario`, `all`, and `ci` - one per value of the +`indexing` DSL property. `off`, `feature`, `scenario`, and `all` start out with byte-for-byte identical +copies of the same two `.feature` files, differing only in the `indexing` value each one configures; +`ci`'s copies start out *already numbered* instead (see "Intent" below). == Intent This example shows: -* What each of the four `indexing` values actually does to the source `.feature` files: - `off` (nothing), `feature` (numbers `Feature` titles only), `scenario` (numbers `Scenario`/ - `Scenario Outline` titles only, continuously across every feature file), and `all` (numbers both, - scenarios as `.`). +* What each of the five `indexing` values actually does to the source `.feature` files: + `off` (nothing, but strips any prior numbering), `feature` (numbers `Feature` titles only), + `scenario` (numbers `Scenario`/`Scenario Outline` titles only, continuously across every feature + file), `all` (numbers both, scenarios as `.`), and `ci` (skips + indexing entirely - unlike `off`, not even to strip prior numbering). * That indexing numbers features/scenarios alphabetically by feature file name - `authentication.feature` is processed before `invoice.feature` - not in whatever order the filesystem happens to list them. @@ -24,6 +26,9 @@ This example shows: source files. * That running the build a second time is a no-op: indexing strips any numbering left over from a previous run before reapplying it, so already-correctly-numbered files are left untouched. +* That the `-PgherkinToAsciidoc.indexing=` command-line override forces the same `indexing` + value onto every sub-project at once, regardless of what each one's own `build.gradle` configures - + typically used to force `ci` for the whole build in a CI pipeline. == Project Layout @@ -42,9 +47,12 @@ indexing/ ├── scenario/ │ ├── build.gradle (indexing = IndexingMode.SCENARIO) │ └── src/test/resources/... (same two files, byte-for-byte identical to off/) -└── all/ - ├── build.gradle (indexing = IndexingMode.ALL) - └── src/test/resources/... (same two files, byte-for-byte identical to off/) +├── all/ +│ ├── build.gradle (indexing = IndexingMode.ALL) +│ └── src/test/resources/... (same two files, byte-for-byte identical to off/) +└── ci/ + ├── build.gradle (indexing = IndexingMode.CI) + └── src/test/resources/... (same two files, but already numbered - see "Intent" above) ---- == Configuration @@ -104,8 +112,19 @@ gherkinToAsciidoc { } ---- +.`ci/build.gradle` +[source,groovy] +---- +import com.arc_e_tect.gradle.gherkin.indexing.IndexingMode + +gherkinToAsciidoc { + indexing = IndexingMode.CI +} +---- + `feature` and `all` both require `groupByFeature = true`, which - like `includeSubDirs = true` - is -already the plugin's default, so neither sub-project needs to configure it explicitly. +already the plugin's default, so neither sub-project needs to configure it explicitly. `ci` has no such +requirement - `IndexingMode.CI` is always allowed, regardless of `includeSubDirs`/`groupByFeature`. == Build And Run @@ -116,24 +135,44 @@ cd examples/gherkin-to-asciidoc/indexing ---- Running `generateFeatureDocs` from the root project without a project path runs it in *every* project -that has it - root, `off`, `feature`, `scenario`, and `all` - in one command. +that has it - root, `off`, `feature`, `scenario`, `all`, and `ci` - in one command. [IMPORTANT] ==== This rewrites the `.feature` files under `off/`, `feature/`, `scenario/`, and `all/` *in place* - that -is the whole point of the `indexing` property. Running it again afterwards is a no-op (the files are -already correctly numbered for their sub-project's mode), but the working tree will show the four -sub-projects' `.feature` files as modified the first time you run it. Run `git diff` afterwards to see -exactly what each mode changed - or `git checkout -- off feature scenario all` to put the four pristine, -identically-worded copies back if you want to try it again from a clean baseline. +is the whole point of the `indexing` property (`ci/`'s files are never touched, by design). Running it +again afterwards is a no-op (the files are already correctly numbered for their sub-project's mode), but +the working tree will show `off/`, `feature/`, `scenario/`, and `all/`'s `.feature` files as modified the +first time you run it. Run `git diff` afterwards to see exactly what each mode changed - or +`git checkout -- off feature scenario all` to put the four pristine, identically-worded copies back if you +want to try it again from a clean baseline. ==== +=== Overriding indexing from the command line + +To see the `-PgherkinToAsciidoc.indexing` override in action, run this *before* the plain command above, +against a clean checkout: + +[source,bash] +---- +./gradlew generateFeatureDocs -PgherkinToAsciidoc.indexing=ci +---- + +This forces `indexing = IndexingMode.CI` onto all five sub-projects for this run alone, regardless of what +each one's own `build.gradle` configures - so even though `feature/`, `scenario/`, and `all/` are +configured to number their files, none of the five sub-projects' `.feature` files are touched. `git status` +afterwards shows a clean working tree; only the five generated reports (still produced normally, just from +each project's pristine, un-numbered starting content) land under each sub-project's own `build/` directory. +Run the plain `./gradlew generateFeatureDocs` afterwards to see `off/`, `feature/`, `scenario/`, and `all/` +get numbered as normal - proving the override only affects the run it's passed to. + == What To Expect Before the build runs, `off/`, `feature/`, `scenario/`, and `all/` all contain the exact same `authentication.feature` (three scenarios: a title-only scenario, a fully-worded scenario, and a `Scenario Outline`) and `invoice.feature` (one scenario) - reproduced here from `off/`, which stays -this way after the build since its `indexing` is `off`: +this way after the build since its `indexing` is `off`. `ci/`'s copies of the same two files start out +*already* numbered instead (see "Ci" below) - and stay that way, untouched, after the build too. .`off/src/test/resources/features-auth/authentication.feature` (unchanged by any mode) [source,gherkin] @@ -231,9 +270,58 @@ the scenario count for each feature, unlike `scenario` mode's continuous count. * Scenario: 2.1 - User pays an invoice ---- +=== Ci — indexing skipped entirely + +Unlike the other four, `ci/`'s copies of the two feature files start out *already* numbered - as `all` +mode would leave them - specifically to prove that `IndexingMode.CI` leaves that numbering completely +untouched, rather than stripping it the way `off` would: + +.`ci/src/test/resources/features-auth/authentication.feature` (before *and* after the build - byte-for-byte identical) +[source,gherkin] +---- +Feature: 1 - User authentication + + Scenario: 1.1 - User requests a password reset + # Not yet fleshed out - title only, no steps yet. + + Scenario: 1.2 - User logs in successfully + Given the login page is open + When the user submits valid credentials + Then the dashboard is displayed + + Scenario Outline: 1.3 - User logs in with different credential sets + Given the login page is open + When the user submits "" and "" + Then the result is "" + + Examples: + | username | password | outcome | + | alice | secret | success | + | bob | wrong | failure | +---- + +`ci/build/generated-docs/features.adoc` is still generated normally - reflecting whatever numbering the +source files already happen to have, since `generateFeatureDocs` always parses and reports on the current +state of the source files, indexing or not: + +[source,asciidoc] +---- +== 1 - User authentication + +* Scenario: 1.1 - User requests a password reset +* Scenario: 1.2 - User logs in successfully +* Scenario Outline: 1.3 - User logs in with different credential sets + +== 2 - Invoice payment + +* Scenario: 2.1 - User pays an invoice +---- + [NOTE] ==== -This example is pinned to `gherkin-to-asciidoc = "2.0.0"` in `gradle/libs.versions.toml` - the version -`indexing` (and the breaking `includeSubDirs`/`groupByFeature` default changes it ships alongside) was -released as, resolved from the Gradle Plugin Portal like any other example in this repository. +This example is pinned to `gherkin-to-asciidoc = "2.1.0"` in `gradle/libs.versions.toml` - the version the +`ci` indexing value and the `-PgherkinToAsciidoc.indexing` command-line override are expected to release +as (a purely additive change on top of `indexing`, itself released as `2.0.0`). Verified locally against +the plugin's own source (via a temporary `includeBuild`, since removed) before pinning; won't build +against the Gradle Plugin Portal until `2.1.0` is actually released. ==== diff --git a/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-auth/authentication.feature b/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-auth/authentication.feature index ba1de52..c25f9e8 100644 --- a/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-auth/authentication.feature +++ b/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-auth/authentication.feature @@ -1,14 +1,14 @@ -Feature: 1 - User authentication +Feature: User authentication - Scenario: 1.1 - User requests a password reset + Scenario: User requests a password reset # Not yet fleshed out - title only, no steps yet. - Scenario: 1.2 - User logs in successfully + Scenario: User logs in successfully Given the login page is open When the user submits valid credentials Then the dashboard is displayed - Scenario Outline: 1.3 - User logs in with different credential sets + Scenario Outline: User logs in with different credential sets Given the login page is open When the user submits "" and "" Then the result is "" diff --git a/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-billing/invoice.feature b/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-billing/invoice.feature index 7716ca1..b62bf83 100644 --- a/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-billing/invoice.feature +++ b/examples/gherkin-to-asciidoc/indexing/all/src/test/resources/features-billing/invoice.feature @@ -1,6 +1,6 @@ -Feature: 2 - Invoice payment +Feature: Invoice payment - Scenario: 2.1 - User pays an invoice + Scenario: User pays an invoice Given an outstanding invoice When the user pays the invoice Then the invoice is marked as paid diff --git a/examples/gherkin-to-asciidoc/indexing/build.gradle b/examples/gherkin-to-asciidoc/indexing/build.gradle index 0bfbe2f..edd4980 100644 --- a/examples/gherkin-to-asciidoc/indexing/build.gradle +++ b/examples/gherkin-to-asciidoc/indexing/build.gradle @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.gherkin.to.asciidoc) } -// Configuration here applies to every project below (root and all four sub-projects) unless a +// Configuration here applies to every project below (root and all five sub-projects) unless a // sub-project's own build.gradle overrides a given property for itself. Every sub-project below // overrides only the one property this example is about: indexing. gherkinToAsciidoc { @@ -10,16 +10,18 @@ gherkinToAsciidoc { // None of the sub-projects configure sourceDirs themselves, so each one resolves these same // relative paths against its own directory, e.g. off/src/test/resources/features-auth and - // off/src/test/resources/features-billing. Every sub-project's features-auth/authentication.feature + // off/src/test/resources/features-billing. off/feature/scenario/all's features-auth/authentication.feature // and features-billing/invoice.feature start out byte-for-byte identical - only the indexing - // property differs, so the four generated reports (and the numbering baked into the four copies - // of the source files) are directly comparable. + // property differs, so those four generated reports (and the numbering baked into those four + // copies of the source files) are directly comparable. ci/'s copies start out already numbered + // (as ALL mode would leave them) instead, to demonstrate that indexing = IndexingMode.CI leaves + // that numbering completely untouched rather than stripping it the way OFF would. sourceDirs.from( 'src/test/resources/features-auth', 'src/test/resources/features-billing') } -// Applies the plugin to off/feature/scenario/all too, reusing the version already resolved above. +// Applies the plugin to off/feature/scenario/all/ci too, reusing the version already resolved above. // Each sub-project gets its own generateFeatureDocs task and its own generated report. subprojects { apply plugin: 'com.arc-e-tect.gherkin-to-asciidoc' diff --git a/examples/gherkin-to-asciidoc/indexing/ci/build.gradle b/examples/gherkin-to-asciidoc/indexing/ci/build.gradle new file mode 100644 index 0000000..313484c --- /dev/null +++ b/examples/gherkin-to-asciidoc/indexing/ci/build.gradle @@ -0,0 +1,16 @@ +import com.arc_e_tect.gradle.gherkin.indexing.IndexingMode + +// The plugin is already applied to this project by the root project's `subprojects { }` block. +// This sub-project overrides indexing = IndexingMode.CI: indexing is skipped entirely, so the +// source .feature files are never touched - not even to strip numbering. Unlike the other three +// sub-projects, this one's feature files start out ALREADY numbered (as ALL mode would have left +// them), specifically to prove that CI mode leaves that numbering completely untouched rather +// than stripping it the way OFF would. +// +// This value is also - and more commonly - set from the command line for the whole build at +// once, without editing any sub-project's build.gradle, e.g.: +// ./gradlew generateFeatureDocs -PgherkinToAsciidoc.indexing=ci +// See the root README for that in action against the feature/scenario/all sub-projects too. +gherkinToAsciidoc { + indexing = IndexingMode.CI +} diff --git a/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-auth/authentication.feature b/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-auth/authentication.feature new file mode 100644 index 0000000..ba1de52 --- /dev/null +++ b/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-auth/authentication.feature @@ -0,0 +1,19 @@ +Feature: 1 - User authentication + + Scenario: 1.1 - User requests a password reset + # Not yet fleshed out - title only, no steps yet. + + Scenario: 1.2 - User logs in successfully + Given the login page is open + When the user submits valid credentials + Then the dashboard is displayed + + Scenario Outline: 1.3 - User logs in with different credential sets + Given the login page is open + When the user submits "" and "" + Then the result is "" + + Examples: + | username | password | outcome | + | alice | secret | success | + | bob | wrong | failure | diff --git a/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-billing/invoice.feature b/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-billing/invoice.feature new file mode 100644 index 0000000..7716ca1 --- /dev/null +++ b/examples/gherkin-to-asciidoc/indexing/ci/src/test/resources/features-billing/invoice.feature @@ -0,0 +1,6 @@ +Feature: 2 - Invoice payment + + Scenario: 2.1 - User pays an invoice + Given an outstanding invoice + When the user pays the invoice + Then the invoice is marked as paid diff --git a/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-auth/authentication.feature b/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-auth/authentication.feature index 4ffa363..c25f9e8 100644 --- a/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-auth/authentication.feature +++ b/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-auth/authentication.feature @@ -1,4 +1,4 @@ -Feature: 1 - User authentication +Feature: User authentication Scenario: User requests a password reset # Not yet fleshed out - title only, no steps yet. diff --git a/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-billing/invoice.feature b/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-billing/invoice.feature index 1042c02..b62bf83 100644 --- a/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-billing/invoice.feature +++ b/examples/gherkin-to-asciidoc/indexing/feature/src/test/resources/features-billing/invoice.feature @@ -1,4 +1,4 @@ -Feature: 2 - Invoice payment +Feature: Invoice payment Scenario: User pays an invoice Given an outstanding invoice diff --git a/examples/gherkin-to-asciidoc/indexing/gradle/libs.versions.toml b/examples/gherkin-to-asciidoc/indexing/gradle/libs.versions.toml index e7d92f4..7d9b697 100644 --- a/examples/gherkin-to-asciidoc/indexing/gradle/libs.versions.toml +++ b/examples/gherkin-to-asciidoc/indexing/gradle/libs.versions.toml @@ -6,4 +6,4 @@ gherkin-to-asciidoc = { id = "com.arc-e-tect.gherkin-to-asciidoc", version.ref = [versions] -gherkin-to-asciidoc = "2.0.0" +gherkin-to-asciidoc = "2.1.0" diff --git a/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-auth/authentication.feature b/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-auth/authentication.feature index c8afd84..c25f9e8 100644 --- a/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-auth/authentication.feature +++ b/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-auth/authentication.feature @@ -1,14 +1,14 @@ Feature: User authentication - Scenario: 1 - User requests a password reset + Scenario: User requests a password reset # Not yet fleshed out - title only, no steps yet. - Scenario: 2 - User logs in successfully + Scenario: User logs in successfully Given the login page is open When the user submits valid credentials Then the dashboard is displayed - Scenario Outline: 3 - User logs in with different credential sets + Scenario Outline: User logs in with different credential sets Given the login page is open When the user submits "" and "" Then the result is "" diff --git a/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-billing/invoice.feature b/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-billing/invoice.feature index a91987d..b62bf83 100644 --- a/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-billing/invoice.feature +++ b/examples/gherkin-to-asciidoc/indexing/scenario/src/test/resources/features-billing/invoice.feature @@ -1,6 +1,6 @@ Feature: Invoice payment - Scenario: 4 - User pays an invoice + Scenario: User pays an invoice Given an outstanding invoice When the user pays the invoice Then the invoice is marked as paid diff --git a/examples/gherkin-to-asciidoc/indexing/settings.gradle b/examples/gherkin-to-asciidoc/indexing/settings.gradle index 153e349..19d1cac 100644 --- a/examples/gherkin-to-asciidoc/indexing/settings.gradle +++ b/examples/gherkin-to-asciidoc/indexing/settings.gradle @@ -17,4 +17,4 @@ refreshVersions { rootProject.name = 'gherkin-to-asciidoc-indexing-example' -include 'off', 'feature', 'scenario', 'all' +include 'off', 'feature', 'scenario', 'all', 'ci' diff --git a/gherkin-to-asciidoc/README.adoc b/gherkin-to-asciidoc/README.adoc index b7d62dc..cad3fbb 100644 --- a/gherkin-to-asciidoc/README.adoc +++ b/gherkin-to-asciidoc/README.adoc @@ -180,8 +180,8 @@ Only used when `trackProgress` is `true`. | `template` | File | — | Optional Mustache template used to render the report so it references the snippets via `include::` directives instead of embedding their content verbatim. Only consulted when `trackProgress` is `true`. | `systemUnderTestVersion` | String | the project's `version` | Version of the system under test that the reported scenarios exercise, printed near the top of the generated document (see "System Under Test Version" below). -| `indexing` | `IndexingMode` (`OFF`/`FEATURE`/`SCENARIO`/`ALL`) | `OFF` | Numbers `Feature`/`Scenario` titles directly in the source `.feature` files (see "Numbering Features and Scenarios" below). -Requires `includeSubDirs = true`; `FEATURE` and `ALL` additionally require `groupByFeature = true`. +| `indexing` | `IndexingMode` (`OFF`/`FEATURE`/`SCENARIO`/`ALL`/`CI`) | `OFF` | Numbers `Feature`/`Scenario` titles directly in the source `.feature` files (see "Numbering Features and Scenarios" below). +`FEATURE`/`SCENARIO`/`ALL` require `includeSubDirs = true`; `FEATURE` and `ALL` additionally require `groupByFeature = true`. `CI` skips indexing entirely and is always allowed; overridable for the whole build via the `-PgherkinToAsciidoc.indexing` project property. |=== [IMPORTANT] @@ -199,9 +199,15 @@ configured values. `snippetDir` and `template` are only consulted when `trackProgress` is `true`; both are ignored otherwise. -`indexing` can only be set to something other than `OFF` when `includeSubDirs` is `true`. `FEATURE` and `ALL` -additionally require `groupByFeature` to be `true`; with `groupByFeature = false`, only `OFF` and `SCENARIO` -are allowed. Setting an invalid combination fails `generateFeatureDocs` with a descriptive error. +`OFF` and `CI` are always allowed for `indexing`. `FEATURE`, `SCENARIO`, and `ALL` require `includeSubDirs` +to be `true`; `FEATURE` and `ALL` additionally require `groupByFeature` to be `true` - with +`groupByFeature = false`, only `SCENARIO` of those three is allowed. Setting an invalid combination fails +`generateFeatureDocs` with a descriptive error. + +The `-PgherkinToAsciidoc.indexing=` project property, when set, overrides `indexing` for every +project in the build regardless of what any project's own `gherkinToAsciidoc { }` block configures - see +"Overriding `indexing` from the Command Line" below. An unrecognised value fails `generateFeatureDocs` with +a descriptive error too. ==== == Running the Task @@ -255,7 +261,9 @@ sub-project that inherits or sets `trackProgress = true` still needs its own `gl | Inherited from the root project by default (subject to the usual `trackProgress` implication). | `indexing` | Inherited from the root project by default; a sub-project can override it independently, subject to -the usual `includeSubDirs`/`groupByFeature` validation constraints for whichever value is in effect. +the usual `includeSubDirs`/`groupByFeature` validation constraints for whichever value is in effect. The +`-PgherkinToAsciidoc.indexing` command-line override (see "Overriding `indexing` from the Command Line" +above) takes precedence over both, for every project in the build at once. | `outputFileName` | Inherited from the root project by default. | `template` @@ -438,6 +446,7 @@ a Cucumber test run, ...), not only in `generateFeatureDocs`'s own output. | `FEATURE` | Every feature is numbered, e.g. `Feature: 1 - User authentication`. Scenario titles are untouched. | `SCENARIO` | Every scenario is numbered continuously across all feature files, e.g. `Scenario: 1 - User logs in`. Feature titles are untouched. | `ALL` | Both are numbered; scenarios are numbered per feature as `.`, e.g. `Scenario: 1.1 - User logs in` within `Feature: 1 - User authentication`. +| `CI` | Indexing is skipped entirely: the source files aren't touched at all - unlike `OFF`, not even to strip prior numbering. See "Overriding `indexing` from the Command Line" below. |=== Feature files are processed - and numbered - in the same order the generated report lists them in: for each @@ -511,10 +520,31 @@ a no-op once the files are already correctly numbered), but it does mean the tas its own inputs - commit the resulting numbered `.feature` files like any other source change. ==== +=== Overriding `indexing` from the Command Line + +A CI pipeline typically shouldn't have `generateFeatureDocs` rewrite committed `.feature` files during a +build - a fresh checkout has no numbering to add, and a build that mutates its own source tree can leave a +CI job with unexpected local changes for no benefit, since nothing there will ever commit them. Rather than +requiring every project's build script to special-case CI, override `indexing` from the command line +instead: + +[source,bash] +---- +./gradlew generateFeatureDocs -PgherkinToAsciidoc.indexing=ci +---- + +This sets `indexing` to `IndexingMode.CI` for every project in the build - root and every sub-project alike +- regardless of what each project's own `gherkinToAsciidoc { }` block configures. A project can keep +`indexing = IndexingMode.FEATURE` (or any other value) for local/day-to-day use and still build cleanly in +CI without any source file ever being mutated there, with no change to the build script itself. The +property's value is matched against `IndexingMode` case-insensitively and accepts any of the five values +(`off`, `feature`, `scenario`, `all`, `ci`), not just `ci` - useful for e.g. temporarily forcing a specific +mode across an entire multi-project build without editing every sub-project's configuration. + See the link:../examples/gherkin-to-asciidoc/indexing/README.adoc[indexing example] -for all four modes applied to the same feature files side by side, with the exact before/after content and -generated reports for each. +for all five modes applied to the same feature files side by side, with the exact before/after content and +generated reports for each, plus the command-line override in action. == Tracking Implementation Progress diff --git a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GenerateFeatureDocsTask.java b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GenerateFeatureDocsTask.java index 8427040..9138a63 100644 --- a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GenerateFeatureDocsTask.java +++ b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GenerateFeatureDocsTask.java @@ -161,9 +161,11 @@ public abstract class GenerateFeatureDocsTask extends DefaultTask { /** * Whether - and how - to number {@code Feature}/{@code Scenario} titles directly in the - * source {@code .feature} files. Only allowed when {@link #getIncludeSubDirs()} is - * {@code true}; when {@link #getGroupByFeature()} is {@code false}, only - * {@link IndexingMode#OFF} and {@link IndexingMode#SCENARIO} are allowed. + * source {@code .feature} files. {@link IndexingMode#OFF} and {@link IndexingMode#CI} are + * always allowed; {@link IndexingMode#FEATURE}, {@link IndexingMode#SCENARIO}, and + * {@link IndexingMode#ALL} are only allowed when {@link #getIncludeSubDirs()} is + * {@code true}, and when {@link #getGroupByFeature()} is {@code false}, only + * {@link IndexingMode#SCENARIO} of those three is allowed. * * @return mutable property for the indexing mode */ @@ -224,21 +226,26 @@ public void generate() { IndexingMode indexing = getIndexing().get(); boolean groupByFeature = getGroupByFeature().get(); - if (indexing != IndexingMode.OFF && !getIncludeSubDirs().get()) { + boolean indexingActive = indexing != IndexingMode.OFF && indexing != IndexingMode.CI; + if (indexingActive && !getIncludeSubDirs().get()) { throw new GradleException( "gherkinToAsciidoc: indexing can only be used when includeSubDirs is true."); } - if (indexing != IndexingMode.OFF && indexing != IndexingMode.SCENARIO && !groupByFeature) { + if (indexingActive && indexing != IndexingMode.SCENARIO && !groupByFeature) { throw new GradleException( "gherkinToAsciidoc: when groupByFeature is false, indexing can only be " - + "'off' or 'scenario'."); + + "'off', 'ci', or 'scenario'."); } // trackProgress implies recursive scanning, regardless of includeSubDirs's own value. boolean recursive = trackProgress || getIncludeSubDirs().get(); List featureFiles = collectFeatureFiles(sourceDirsSet, sourceFileSet, recursive); - new FeatureIndexer().reindex(featureFiles, indexing); + // CI skips indexing entirely - the feature files are left completely untouched, not even + // to strip numbering left over from a previous run, unlike OFF. + if (indexing != IndexingMode.CI) { + new FeatureIndexer().reindex(featureFiles, indexing); + } List scenarios = new ArrayList<>(); FeatureParser featureParser = new FeatureParser(); diff --git a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocExtension.java b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocExtension.java index c9718f0..b8544ac 100644 --- a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocExtension.java +++ b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocExtension.java @@ -24,6 +24,9 @@ * indexing = IndexingMode.OFF // default; requires includeSubDirs = true * } * + * + *

{@code indexing} can be overridden for the whole build from the command line, e.g. + * {@code -PgherkinToAsciidoc.indexing=ci} - see {@link #getIndexing()}.

*/ public abstract class GherkinToAsciidocExtension { @@ -42,6 +45,14 @@ public GherkinToAsciidocExtension() {} /** Default relative path of the directory report snippets are written to. */ public static final String DEFAULT_SNIPPET_DIR = "generated-docs/features/snippets"; + /** + * Name of the Gradle project property that overrides {@link #getIndexing()} from the command + * line for every project in the build, e.g. {@code -PgherkinToAsciidoc.indexing=ci}. Takes + * precedence over any project's own configured {@code indexing} value. The value is matched + * against {@link IndexingMode} enum constant names case-insensitively. + */ + public static final String INDEXING_OVERRIDE_PROPERTY = "gherkinToAsciidoc.indexing"; + /** * Source directories that contain the {@code .feature} files to process. One or more * directories may be configured, e.g. via {@code sourceDirs.from(file('a'), file('b'))}. @@ -163,6 +174,9 @@ public GherkinToAsciidocExtension() {} * feature files, e.g. {@code Scenario: 1 - User logs in}. *
  • {@link IndexingMode#ALL} - both are numbered, scenarios as * {@code .}, e.g. {@code Scenario: 1.1 - User logs in}.
  • + *
  • {@link IndexingMode#CI} - indexing is skipped entirely; unlike {@link IndexingMode#OFF}, + * the source files aren't even stripped of prior numbering. See + * {@value #INDEXING_OVERRIDE_PROPERTY} below.
  • * * *

    Feature files are processed in the same order the generated report lists them in: for each @@ -175,9 +189,17 @@ public GherkinToAsciidocExtension() {} * fresh numbering is applied for the new mode - including removing all numbering when set back * to {@link IndexingMode#OFF}.

    * - *

    Only allowed when {@link #getIncludeSubDirs()} is {@code true}. When - * {@link #getGroupByFeature()} is {@code false}, only {@link IndexingMode#OFF} and - * {@link IndexingMode#SCENARIO} are allowed.

    + *

    {@link IndexingMode#OFF} and {@link IndexingMode#CI} are always allowed. + * {@link IndexingMode#FEATURE}, {@link IndexingMode#SCENARIO}, and {@link IndexingMode#ALL} are + * only allowed when {@link #getIncludeSubDirs()} is {@code true}; when + * {@link #getGroupByFeature()} is {@code false}, only {@link IndexingMode#SCENARIO} of those + * three is allowed.

    + * + *

    The {@value #INDEXING_OVERRIDE_PROPERTY} project property, when set (e.g. + * {@code -PgherkinToAsciidoc.indexing=ci}), overrides this property for every project in the + * build regardless of what any project configures here - typically used to force + * {@link IndexingMode#CI} in a CI pipeline so {@code generateFeatureDocs} never mutates source + * files there, without having to change the build script itself.

    * * @return mutable property for the indexing mode */ diff --git a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPlugin.java b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPlugin.java index 9f19617..69668e0 100644 --- a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPlugin.java +++ b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPlugin.java @@ -1,6 +1,7 @@ package com.arc_e_tect.gradle.gherkin; import com.arc_e_tect.gradle.gherkin.indexing.IndexingMode; +import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; @@ -10,6 +11,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Locale; /** * Gradle plugin that registers the {@code generateFeatureDocs} task and wires @@ -57,6 +59,12 @@ * regardless of what the root project configures, so that every project's report lands in its * own build output rather than colliding with another project's. Set these explicitly on a * specific project to relocate that project's report.

    + * + *

    Overriding {@code indexing} from the command line

    + *

    The {@code -PgherkinToAsciidoc.indexing=<value>} project property overrides {@code indexing} + * for every project in the build, regardless of what any project's own {@code gherkinToAsciidoc { }} + * block configures - typically set to {@code ci} in a CI pipeline so {@code generateFeatureDocs} never + * mutates source {@code .feature} files there, without having to change the build script itself.

    */ public class GherkinToAsciidocPlugin implements Plugin { @@ -110,6 +118,13 @@ public void apply(Project project) { Project rootProject = project.getRootProject(); + // The -PgherkinToAsciidoc.indexing= project property, when set, overrides indexing + // for every project in the build - regardless of what any project's own extension + // configures - typically used to force `ci` in a CI pipeline without touching build scripts. + Provider indexingCliOverride = project.getProviders() + .gradleProperty(GherkinToAsciidocExtension.INDEXING_OVERRIDE_PROPERTY) + .map(GherkinToAsciidocPlugin::parseIndexingMode); + project.getTasks().register(TASK_NAME, GenerateFeatureDocsTask.class, task -> { wireSourceLocation(project, rootProject, ext, rootExt, task); task.getIncludeSubDirs().set(ext.getIncludeSubDirs()); @@ -121,11 +136,27 @@ public void apply(Project project) { task.getSnippetDir().set(ext.getSnippetDir()); task.getTemplate().set(ext.getTemplate()); task.getSystemUnderTestVersion().set(ext.getSystemUnderTestVersion()); - task.getIndexing().set(ext.getIndexing()); + task.getIndexing().set(indexingCliOverride.orElse(ext.getIndexing())); task.getProjectDirectory().set(project.getLayout().getProjectDirectory()); }); } + /** + * Parses the {@code -PgherkinToAsciidoc.indexing=} project property's value, matching + * {@link IndexingMode} enum constant names case-insensitively (e.g. {@code ci} -> + * {@link IndexingMode#CI}). + */ + private static IndexingMode parseIndexingMode(String value) { + try { + return IndexingMode.valueOf(value.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new GradleException( + "gherkinToAsciidoc: invalid value '" + value + "' for -P" + + GherkinToAsciidocExtension.INDEXING_OVERRIDE_PROPERTY + + "; expected one of: off, feature, scenario, all, ci"); + } + } + /** * Wires the task's {@code sourceDirs}/{@code sourceFile} from this project's own extension, or - * when neither is configured locally and a root extension exists - from the root project's own diff --git a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/FeatureIndexer.java b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/FeatureIndexer.java index 90f17e8..ab9d5ee 100644 --- a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/FeatureIndexer.java +++ b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/FeatureIndexer.java @@ -20,6 +20,10 @@ * {@link IndexingMode#OFF} - applies fresh numbering. This makes the operation idempotent and * makes switching between modes (including back to {@code OFF}) simply undo the previous * numbering rather than requiring any state to be tracked between runs.

    + * + *

    Never called with {@link IndexingMode#CI}: the caller skips invoking this class entirely for + * that mode, since {@code CI} means the feature files must be left completely untouched, not even + * to strip prior numbering the way {@link IndexingMode#OFF} does.

    */ public class FeatureIndexer { diff --git a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/IndexingMode.java b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/IndexingMode.java index 6eb88e8..0b42267 100644 --- a/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/IndexingMode.java +++ b/gherkin-to-asciidoc/src/main/java/com/arc_e_tect/gradle/gherkin/indexing/IndexingMode.java @@ -27,5 +27,15 @@ public enum IndexingMode { * {@code .}, e.g. {@code Scenario: 1.1 - User logs in} * within {@code Feature: 1 - User authentication}. */ - ALL + ALL, + + /** + * Indexing is skipped entirely: the source {@code .feature} files are left completely + * untouched, not even to strip numbering left over from a previous run - unlike {@link #OFF}, + * which does strip it. Intended to be set via the {@code -PgherkinToAsciidoc.indexing=ci} + * command-line override (see {@code GherkinToAsciidocExtension#getIndexing()}) so a CI + * pipeline never mutates source files, regardless of the {@code indexing} value configured in + * the build script - but it can also be configured directly like any other value. + */ + CI } diff --git a/gherkin-to-asciidoc/src/test/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPluginTest.java b/gherkin-to-asciidoc/src/test/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPluginTest.java index 9699166..e443461 100644 --- a/gherkin-to-asciidoc/src/test/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPluginTest.java +++ b/gherkin-to-asciidoc/src/test/java/com/arc_e_tect/gradle/gherkin/GherkinToAsciidocPluginTest.java @@ -15,6 +15,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("GherkinToAsciidocPlugin") @@ -786,7 +787,7 @@ void throwsWhenIndexingFeatureAndGroupByFeatureFalse() throws IOException { assertThatThrownBy(task::generate) .isInstanceOf(org.gradle.api.GradleException.class) - .hasMessageContaining("when groupByFeature is false, indexing can only be 'off' or 'scenario'"); + .hasMessageContaining("when groupByFeature is false, indexing can only be 'off', 'ci', or 'scenario'"); } @Test @@ -806,7 +807,7 @@ void throwsWhenIndexingAllAndGroupByFeatureFalse() throws IOException { assertThatThrownBy(task::generate) .isInstanceOf(org.gradle.api.GradleException.class) - .hasMessageContaining("when groupByFeature is false, indexing can only be 'off' or 'scenario'"); + .hasMessageContaining("when groupByFeature is false, indexing can only be 'off', 'ci', or 'scenario'"); } @Test @@ -929,6 +930,96 @@ void changingIndexingToOffRemovesNumberingOnNextRun() throws IOException { .doesNotContain("1.1 -"); } + @Test + @DisplayName("indexing CI leaves feature files completely untouched, even numbering left over from a previous run") + void indexingCiLeavesFilesCompletelyUntouched() throws IOException { + Project project = projectWithPlugin(); + File featuresDir = new File(tempDir.toFile(), "features"); + featuresDir.mkdirs(); + // Simulates numbering left over from an earlier ALL-mode run. + writeFeatureFile(featuresDir, "sample.feature", + "Feature: 1 - Sample\n\n Scenario: 1.1 - A scenario\n Given g\n"); + String before = Files.readString(featuresDir.toPath().resolve("sample.feature")); + File outputDir = new File(tempDir.toFile(), "output"); + + GenerateFeatureDocsTask task = task(project); + task.getSourceDirs().from(featuresDir); + task.getIndexing().set(IndexingMode.CI); + task.getOutputDir().set(outputDir); + task.getProjectDirectory().set(project.getLayout().getProjectDirectory()); + task.generate(); + + // Unlike OFF, CI does not even strip prior numbering - the file is byte-for-byte unchanged. + assertThat(Files.readString(featuresDir.toPath().resolve("sample.feature"))).isEqualTo(before); + String content = Files.readString(new File(outputDir, "features.adoc").toPath()); + assertThat(content).contains("* Scenario: 1.1 - A scenario"); + } + + @Test + @DisplayName("indexing CI is allowed even when includeSubDirs and groupByFeature are both false") + void indexingCiAllowedRegardlessOfIncludeSubDirsAndGroupByFeature() throws IOException { + Project project = projectWithPlugin(); + File featuresDir = new File(tempDir.toFile(), "features"); + featuresDir.mkdirs(); + writeFeatureFile(featuresDir, "sample.feature", "Feature: Sample\n\n Scenario: A scenario\n Given g\n"); + + GenerateFeatureDocsTask task = task(project); + task.getSourceDirs().from(featuresDir); + task.getIncludeSubDirs().set(false); + task.getGroupByFeature().set(false); + task.getIndexing().set(IndexingMode.CI); + task.getOutputDir().set(new File(tempDir.toFile(), "output")); + task.getProjectDirectory().set(project.getLayout().getProjectDirectory()); + + assertThatCode(task::generate).doesNotThrowAnyException(); + } + + @Test + @DisplayName("the -PgherkinToAsciidoc.indexing project property overrides indexing regardless of the configured value") + void cliPropertyOverridesConfiguredIndexing() throws IOException { + Files.writeString(tempDir.resolve("gradle.properties"), "gherkinToAsciidoc.indexing=ci\n"); + Project project = projectWithPlugin(); + File featuresDir = new File(tempDir.toFile(), "features"); + featuresDir.mkdirs(); + writeFeatureFile(featuresDir, "sample.feature", + "Feature: 1 - Sample\n\n Scenario: 1.1 - A scenario\n Given g\n"); + String before = Files.readString(featuresDir.toPath().resolve("sample.feature")); + + GenerateFeatureDocsTask task = task(project); + task.getSourceDirs().from(featuresDir); + // Configured to ALL, but the CLI override must win. + extension(project).getIndexing().set(IndexingMode.ALL); + task.getOutputDir().set(new File(tempDir.toFile(), "output")); + task.getProjectDirectory().set(project.getLayout().getProjectDirectory()); + task.generate(); + + assertThat(task.getIndexing().get()).isEqualTo(IndexingMode.CI); + assertThat(Files.readString(featuresDir.toPath().resolve("sample.feature"))).isEqualTo(before); + } + + @Test + @DisplayName("an invalid -PgherkinToAsciidoc.indexing value throws a descriptive GradleException") + void cliPropertyInvalidValueThrowsDescriptiveError() throws IOException { + Files.writeString(tempDir.resolve("gradle.properties"), "gherkinToAsciidoc.indexing=bogus\n"); + Project project = projectWithPlugin(); + File featuresDir = new File(tempDir.toFile(), "features"); + featuresDir.mkdirs(); + writeFeatureFile(featuresDir, "sample.feature", "Feature: Sample\n\n Scenario: A scenario\n Given g\n"); + + GenerateFeatureDocsTask task = task(project); + task.getSourceDirs().from(featuresDir); + task.getOutputDir().set(new File(tempDir.toFile(), "output")); + task.getProjectDirectory().set(project.getLayout().getProjectDirectory()); + + // Gradle wraps the exception thrown while lazily resolving the "indexing" property's + // value (the -P override is parsed lazily via a Provider) in a PropertyQueryException; + // the GradleException with the actual descriptive message is its root cause. + assertThatThrownBy(task::generate) + .hasRootCauseInstanceOf(org.gradle.api.GradleException.class) + .hasRootCauseMessage("gherkinToAsciidoc: invalid value 'bogus' for -PgherkinToAsciidoc.indexing; " + + "expected one of: off, feature, scenario, all, ci"); + } + // --- helpers --- private Project projectWithPlugin() {