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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ requirementTracing {
reportVerbosity = 'failure_details'
detailsSectionDisplay = 'collapse'
filteredArtifactTypes = ["req", "dsn"]
filterWantedStatuses = ["draft", "approved"]
}
```

Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions example-projects/custom-config/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ 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')
reportFile = file('build/custom-report.txt')
reportFormat = 'plain'
reportVerbosity = 'ALL'
filteredArtifactTypes = artifactTypes
filterWantedStatuses = wantedStatuses
}
1 change: 1 addition & 0 deletions example-projects/custom-config/custom-dir/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
Example requirement

Needs: utest, impl
Status: draft
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -19,7 +21,7 @@
import org.itsallcode.openfasttrace.gradle.task.config.SerializableTagPathConfig;
import org.slf4j.Logger;

public class OpenFastTracePlugin implements Plugin<Project>

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 21

no comment

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 21

no comment

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 25

no comment

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 25

no comment

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 17

no comment

Check warning on line 24 in src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java

View workflow job for this annotation

GitHub Actions / Build with Java 17

no comment
{
private static final Logger LOG = Logging.getLogger(OpenFastTracePlugin.class);
private static final String TASK_GROUP_NAME = "trace";
Expand Down Expand Up @@ -95,9 +97,32 @@
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<ItemStatus> 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<File> getAllInputDirectories(final Set<Project> allProjects)
{
return allProjects.stream() //
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,6 +23,7 @@ public class TracingConfig
private final ListProperty<Object> importedRequirements;
private final SetProperty<String> filteredTags;
private final SetProperty<String> filteredArtifactTypes;
private final SetProperty<String> filterWantedStatuses;
private final Property<Boolean> filterAcceptsItemsWithoutTag;
private final Property<DetailsSectionDisplay> detailsSectionDisplay;
private final Property<Boolean> failBuild;
Expand All @@ -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);
Expand Down Expand Up @@ -90,6 +93,11 @@ public Property<DetailsSectionDisplay> getDetailsSectionDisplay()
return detailsSectionDisplay;
}

public SetProperty<String> getFilterWantedStatuses()
{
return filterWantedStatuses;
}

public void setReportVerbosity(final String reportVerbosity)
{
setReportVerbosity(ReportVerbosity.valueOf(reportVerbosity));
Expand Down Expand Up @@ -140,6 +148,11 @@ public void setDetailsSectionDisplay(final String detailsSectionDisplay)
this.detailsSectionDisplay.set(DetailsSectionDisplay.valueOf(detailsSectionDisplay));
}

public void setFilterWantedStatuses(final Set<String> statuses)
{
this.filterWantedStatuses.set(statuses);
}

public TagPathConfiguration getTagPathConfig()
{
return ((ExtensionAware) this).getExtensions().getByType(TagPathConfiguration.class);
Expand All @@ -161,6 +174,6 @@ public String toString()
return "TracingConfig [reportVerbosity=" + reportVerbosity + ", inputDirectories="
+ inputDirectories + ", reportFile=" + reportFile + ", pathConfig="
+ getTagPathConfig() + ", failBuild=" + failBuild + ", filteredArtifactTypes="
+ filteredArtifactTypes + "]";
+ filteredArtifactTypes + ", filterWantedStatuses=" + filterWantedStatuses + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class TraceTask extends DefaultTask
private final Property<Boolean> filterAcceptsItemsWithoutTag = getProject().getObjects()
.property(Boolean.class);
private final Property<Boolean> failBuild = getProject().getObjects().property(Boolean.class);
private final SetProperty<ItemStatus> filterWantedStatuses = getProject().getObjects()
.setProperty(ItemStatus.class);

@InputFile
@PathSensitive(PathSensitivity.ABSOLUTE)
Expand Down Expand Up @@ -102,6 +104,13 @@ public Property<Boolean> getFailBuild()
return failBuild;
}

@Input
@Optional
public SetProperty<ItemStatus> getFilterWantedStatuses()
{
return filterWantedStatuses;
}

private boolean shouldFailBuild()
{
return failBuild.getOrElse(true);
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ void testCollectExampleProjectWithCustomConfig()
<specobject>
<id>exampleB</id>
<shortdesc>Tracing Example</shortdesc>
<status>approved</status>
<status>draft</status>
<version>1</version>
""",

Expand Down Expand Up @@ -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");
}

Expand All @@ -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");
}

Expand All @@ -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()
{
Expand Down
Loading