diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1f502..9c7cf61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [3.2.0] - ???? +## [3.2.0] - 2026-08-18 +- [PR #79](https://github.com/itsallcode/openfasttrace-gradle/pull/79) + - Add support for filtering by item status +- [PR #78](https://github.com/itsallcode/openfasttrace-gradle/pull/78) + - Simplify Gradle plugin integration tests - [PR #73](https://github.com/itsallcode/openfasttrace-gradle/pull/73) - Upgrade to OpenFastTrace [4.8.0](https://github.com/itsallcode/openfasttrace/releases/tag/4.8.0) - Upgrade integration tests to use Gradle 9.7.0. Gradle 8 is no longer supported. diff --git a/README.md b/README.md index c1bb107..3f5c92e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ requirementTracing { reportVerbosity = 'failure_details' detailsSectionDisplay = 'collapse' filteredArtifactTypes = ["req", "dsn"] + filterWantedStatuses = ["draft", "approved"] } ``` @@ -67,6 +68,7 @@ You can configure the following properties: * `collapse` - hide details (default) * `expand` - show details * `filteredArtifactTypes`: Use only the listed artifact types during tracing +* `filterWantedStatuses`: Import only specification items that have a status contained in the list of statuses. Possible values: `draft`, `proposed`, `approved`, `rejected`. See the [OFT user guide](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide/user_guide.md#filtering-by-status) for details. ### Configuring the Short Tag Importer diff --git a/example-projects/custom-config/build.gradle b/example-projects/custom-config/build.gradle index 9c30279..5a8895d 100644 --- a/example-projects/custom-config/build.gradle +++ b/example-projects/custom-config/build.gradle @@ -5,6 +5,8 @@ plugins { def artifactTypes = findProperty('filteredArtifactTypes') artifactTypes = artifactTypes ? artifactTypes.split(',').toList() : null +def wantedStatuses = findProperty('filterWantedStatuses') +wantedStatuses = wantedStatuses ? wantedStatuses.split(',').toList() : null requirementTracing { failBuild = findProperty('failBuild') == 'true' inputDirectories = files('custom-dir') @@ -12,4 +14,5 @@ requirementTracing { reportFormat = 'plain' reportVerbosity = 'ALL' filteredArtifactTypes = artifactTypes + filterWantedStatuses = wantedStatuses } diff --git a/example-projects/custom-config/custom-dir/spec.md b/example-projects/custom-config/custom-dir/spec.md index 3fc39c3..71e29da 100644 --- a/example-projects/custom-config/custom-dir/spec.md +++ b/example-projects/custom-config/custom-dir/spec.md @@ -4,3 +4,4 @@ Example requirement Needs: utest, impl +Status: draft diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java index 919f965..5250b4a 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java @@ -1,5 +1,6 @@ package org.itsallcode.openfasttrace.gradle; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; import java.io.File; @@ -12,6 +13,7 @@ import org.gradle.api.logging.Logging; import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.tasks.TaskProvider; +import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.gradle.config.TagPathConfiguration; import org.itsallcode.openfasttrace.gradle.config.TracingConfig; import org.itsallcode.openfasttrace.gradle.task.CollectTask; @@ -95,9 +97,32 @@ private static void configureTask(final Project rootProject, task.getFilteredArtifactTypes().set(config.getFilteredArtifactTypes()); task.getFilteredTags().set(config.getFilteredTags()); task.getFilterAcceptsItemsWithoutTag().set(config.getFilterAcceptsItemsWithoutTag()); + task.getFilterWantedStatuses().set(getWantedStatuses(config)); task.getDetailsSectionDisplay().set(config.getDetailsSectionDisplay()); } + private static Set getWantedStatuses(final TracingConfig config) + { + return config.getFilterWantedStatuses().getOrElse(Collections.emptySet()).stream() + .map(OpenFastTracePlugin::convertStatus) + .collect(toSet()); + } + + private static ItemStatus convertStatus(final String value) + { + try + { + return ItemStatus.valueOf(value.toUpperCase(Locale.ROOT)); + } + catch (final IllegalArgumentException e) + { + final String validStatuses = Arrays.stream(ItemStatus.values()).map(ItemStatus::name) + .collect(joining(", ")); + throw new IllegalArgumentException( + "Invalid status '" + value + "'. Valid statuses are: " + validStatuses, e); + } + } + private static Set getAllInputDirectories(final Set allProjects) { return allProjects.stream() // diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java b/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java index 035f030..74af7ce 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.gradle.config; import java.util.List; +import java.util.Set; import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; @@ -22,6 +23,7 @@ public class TracingConfig private final ListProperty importedRequirements; private final SetProperty filteredTags; private final SetProperty filteredArtifactTypes; + private final SetProperty filterWantedStatuses; private final Property filterAcceptsItemsWithoutTag; private final Property detailsSectionDisplay; private final Property failBuild; @@ -38,6 +40,7 @@ public TracingConfig(final Project project) this.filteredTags = project.getObjects().setProperty(String.class); this.filteredArtifactTypes = project.getObjects().setProperty(String.class); this.filterAcceptsItemsWithoutTag = project.getObjects().property(Boolean.class); + this.filterWantedStatuses = project.getObjects().setProperty(String.class); this.filterAcceptsItemsWithoutTag.set(true); this.detailsSectionDisplay = project.getObjects().property(DetailsSectionDisplay.class); this.detailsSectionDisplay.set(DetailsSectionDisplay.COLLAPSE); @@ -90,6 +93,11 @@ public Property getDetailsSectionDisplay() return detailsSectionDisplay; } + public SetProperty getFilterWantedStatuses() + { + return filterWantedStatuses; + } + public void setReportVerbosity(final String reportVerbosity) { setReportVerbosity(ReportVerbosity.valueOf(reportVerbosity)); @@ -140,6 +148,11 @@ public void setDetailsSectionDisplay(final String detailsSectionDisplay) this.detailsSectionDisplay.set(DetailsSectionDisplay.valueOf(detailsSectionDisplay)); } + public void setFilterWantedStatuses(final Set statuses) + { + this.filterWantedStatuses.set(statuses); + } + public TagPathConfiguration getTagPathConfig() { return ((ExtensionAware) this).getExtensions().getByType(TagPathConfiguration.class); @@ -161,6 +174,6 @@ public String toString() return "TracingConfig [reportVerbosity=" + reportVerbosity + ", inputDirectories=" + inputDirectories + ", reportFile=" + reportFile + ", pathConfig=" + getTagPathConfig() + ", failBuild=" + failBuild + ", filteredArtifactTypes=" - + filteredArtifactTypes + "]"; + + filteredArtifactTypes + ", filterWantedStatuses=" + filterWantedStatuses + "]"; } } diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java index f48c9f5..23e5664 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java @@ -39,6 +39,8 @@ public class TraceTask extends DefaultTask private final Property filterAcceptsItemsWithoutTag = getProject().getObjects() .property(Boolean.class); private final Property failBuild = getProject().getObjects().property(Boolean.class); + private final SetProperty filterWantedStatuses = getProject().getObjects() + .setProperty(ItemStatus.class); @InputFile @PathSensitive(PathSensitivity.ABSOLUTE) @@ -102,6 +104,13 @@ public Property getFailBuild() return failBuild; } + @Input + @Optional + public SetProperty getFilterWantedStatuses() + { + return filterWantedStatuses; + } + private boolean shouldFailBuild() { return failBuild.getOrElse(true); @@ -161,10 +170,12 @@ private ImportSettings getImportSettings() private FilterSettings getFilterSettings() { - final FilterSettings settings = FilterSettings.builder() // - .artifactTypes(filteredArtifactTypes.getOrElse(emptySet())) // - .tags(filteredTags.get()) // - .withoutTags(filterAcceptsItemsWithoutTag.get()).build(); + final FilterSettings settings = FilterSettings.builder() + .artifactTypes(filteredArtifactTypes.getOrElse(emptySet())) + .tags(filteredTags.get()) + .withoutTags(filterAcceptsItemsWithoutTag.get()) + .wantedStatuses(filterWantedStatuses.get()) + .build(); getLogger().info("Filter settings: artifactTypes={}, tags={}, acceptItemsWithoutTag={}", settings.getArtifactTypes(), settings.getTags(), settings.isArtifactTypeCriteriaSet()); diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java index 633ed7b..34d9f15 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java @@ -120,7 +120,7 @@ void testCollectExampleProjectWithCustomConfig() exampleB Tracing Example - approved + draft 1 """, @@ -185,7 +185,7 @@ void testTraceExampleProjectWithCustomConfig() assertThat(buildResult.task(":traceRequirements").getOutcome(), either(is(TaskOutcome.SUCCESS)).or(is(TaskOutcome.FROM_CACHE))); TestUtil.assertFileContent(PROJECT_CUSTOM_CONFIG_DIR.resolve("build/custom-report.txt"), - "not ok [ in: 1 / 1 ✔ | out: 0 / 0 ] dsn~exampleB~1 (impl, -utest)", + "not ok [ in: 1 / 1 ✔ | out: 0 / 0 ] dsn~exampleB~1 [draft] (impl, -utest)", "not ok - 2 total, 1 direct, 0 transitive defects"); } @@ -197,7 +197,7 @@ void testTraceExampleProjectWithCustomConfigFailBuild() assertEquals(TaskOutcome.FAILED, buildResult.task(":traceRequirements").getOutcome()); TestUtil.assertFileContent(PROJECT_CUSTOM_CONFIG_DIR.resolve("build/custom-report.txt"), - "not ok [ in: 1 / 1 ✔ | out: 0 / 0 ] dsn~exampleB~1 (impl, -utest)", + "not ok [ in: 1 / 1 ✔ | out: 0 / 0 ] dsn~exampleB~1 [draft] (impl, -utest)", "not ok - 2 total, 1 direct, 0 transitive defects"); } @@ -210,6 +210,44 @@ void filteredArtifactTypes() either(is(TaskOutcome.SUCCESS)).or(is(TaskOutcome.FROM_CACHE))); } + @Test + void filteredWantedStatuses() + { + final BuildResult buildResult = runBuild(PROJECT_CUSTOM_CONFIG_DIR, "clean", + "traceRequirements", + "-PfilterWantedStatuses=draft,approved"); + assertThat(buildResult.task(":traceRequirements").getOutcome(), + either(is(TaskOutcome.SUCCESS)).or(is(TaskOutcome.FROM_CACHE))); + TestUtil.assertFileContent(PROJECT_CUSTOM_CONFIG_DIR.resolve("build/custom-report.txt"), + "not ok [ in: 1 / 1 ✔ | out: 0 / 0 ] dsn~exampleB~1 [draft] (impl, -utest)", + "not ok - 2 total, 1 direct, 0 transitive defects"); + } + + @Test + void filteredWantedStatusesNoMatch() + { + final BuildResult buildResult = runBuild(PROJECT_CUSTOM_CONFIG_DIR, "clean", + "traceRequirements", + "-PfilterWantedStatuses=approved"); + assertThat(buildResult.task(":traceRequirements").getOutcome(), + either(is(TaskOutcome.SUCCESS)).or(is(TaskOutcome.FROM_CACHE))); + TestUtil.assertFileContent( + PROJECT_CUSTOM_CONFIG_DIR.resolve("build/custom-report.txt"), + // Generated ID depends on JVM + "not ok [ in: 0 / 0 | out: 0 / 1 ✘ ] impl~exampleB-", + "not ok - 1 total, 1 direct, 0 transitive defects"); + } + + @Test + void filteredWantedStatusesInvalidStatus() + { + final BuildResult buildResult = runBuildExpectFailure(PROJECT_CUSTOM_CONFIG_DIR, "clean", + "traceRequirements", + "-PfilterWantedStatuses=invalid"); + assertThat(buildResult.getOutput(), containsString( + "Invalid status 'invalid'. Valid statuses are: APPROVED, PROPOSED, DRAFT, REJECTED")); + } + @Test void testTraceExampleProjectWithCustomConfigFailBuildErrorMessage() {