diff --git a/.vscode/settings.json b/.vscode/settings.json index 13e6f9b..06577a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,12 @@ "java.sources.organizeImports.staticStarThreshold": 3, "java.configuration.updateBuildConfiguration": "automatic", "java.compile.nullAnalysis.mode": "automatic", + "java.test.config": { + "vmArgs": [ + "--add-opens", + "java.base/java.lang=ALL-UNNAMED" + ] + }, "sonarlint.connectedMode.project": { "connectionId": "itsallcode", "projectKey": "org.itsallcode:openfasttrace-gradle" diff --git a/CHANGELOG.md b/CHANGELOG.md index eadae15..99729c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#81](https://github.com/itsallcode/openfasttrace-gradle/issues/81): - Ensure compatibility with Gradle configuration cache - Mark plugin as compatible with configuration cache +- [PR #85](https://github.com/itsallcode/openfasttrace-gradle/pull/85): + - Allow configuring color schema + - Configuration values for details section display and report verbosity are now case insensitive + - Improve error messages for invalid enum values ## [3.2.0] - 2026-08-18 diff --git a/README.md b/README.md index 3f5c92e..720744e 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ You can configure the following properties: * `failures` - list of defect specification items * `failure_summaries` - list of summaries for defect specification items * `failure_details` - summaries and details for defect specification items (default) + * `direct_failure_details` - details of non-transitive unclean items (added in OFT 4.6.0) * `all` - summaries and details for all specification items +* `reportColorScheme`: Color scheme of plain text report + * `black_and_white` - Black and white (default) + * `monochrome` - Monochrome (e.g for printers) + * `color` - Color * `detailsSectionDisplay`: Initial display status of the details section in the HTML report * `collapse` - hide details (default) * `expand` - show details diff --git a/example-projects/custom-config/build.gradle b/example-projects/custom-config/build.gradle index 5a8895d..ca86276 100644 --- a/example-projects/custom-config/build.gradle +++ b/example-projects/custom-config/build.gradle @@ -12,7 +12,9 @@ requirementTracing { inputDirectories = files('custom-dir') reportFile = file('build/custom-report.txt') reportFormat = 'plain' - reportVerbosity = 'ALL' + reportVerbosity = 'all' + reportColorScheme = findProperty('reportColorScheme') + detailsSectionDisplay = 'collapse' filteredArtifactTypes = artifactTypes filterWantedStatuses = wantedStatuses } diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java index 44a07a7..f3656aa 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java @@ -104,6 +104,7 @@ private static void configureTask(final Project rootProject, .get())); } task.getReportVerbosity().set(config.getReportVerbosity()); + task.getReportColorScheme().set(config.getReportColorScheme()); task.getReportFormat().set(config.getReportFormat()); task.getImportedRequirements() .from(getImportedRequirements(rootProject, rootProject.getAllprojects())); 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 93cd06d..859ef34 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/config/TracingConfig.java @@ -1,13 +1,16 @@ package org.itsallcode.openfasttrace.gradle.config; -import java.util.List; -import java.util.Set; +import static java.util.stream.Collectors.joining; + +import java.util.*; +import java.util.stream.Collectors; import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.provider.*; +import org.itsallcode.openfasttrace.api.ColorScheme; import org.itsallcode.openfasttrace.api.DetailsSectionDisplay; import org.itsallcode.openfasttrace.api.report.ReportVerbosity; @@ -18,6 +21,7 @@ public class TracingConfig private static final String DEFAULT_REPORT_FORMAT = "plain"; private final Property reportVerbosity; + private final Property reportColorScheme; private final Property reportFormat; private final ConfigurableFileCollection inputDirectories; private final RegularFileProperty reportFile; @@ -41,6 +45,7 @@ public TracingConfig(final Project project) this.reportFile = project.getObjects().fileProperty(); this.reportVerbosity = project.getObjects().property(ReportVerbosity.class); this.reportVerbosity.set(DEFAULT_REPORT_VERBOSITY); + this.reportColorScheme = project.getObjects().property(ColorScheme.class); this.reportFormat = project.getObjects().property(String.class); this.reportFormat.set(DEFAULT_REPORT_FORMAT); this.importedRequirements = project.getObjects().listProperty(Object.class); @@ -65,6 +70,16 @@ public Property getReportVerbosity() return reportVerbosity; } + /** + * Returns the report color scheme. + * + * @return the color scheme + */ + public Property getReportColorScheme() + { + return reportColorScheme; + } + /** * Returns the report format property. * @@ -163,7 +178,28 @@ public SetProperty getFilterWantedStatuses() */ public void setReportVerbosity(final String reportVerbosity) { - setReportVerbosity(ReportVerbosity.valueOf(reportVerbosity)); + this.setReportVerbosity(convertVerbosity(reportVerbosity)); + } + + private static ReportVerbosity convertVerbosity(final String reportVerbosity) + { + if (reportVerbosity == null) + { + return null; + } + try + { + return ReportVerbosity.valueOf(reportVerbosity.toUpperCase(Locale.ROOT)); + } + catch (final IllegalArgumentException e) + { + final String validVerbosities = Arrays.stream(ReportVerbosity.values()).map(ReportVerbosity::name) + .collect(joining(", ")); + throw new IllegalArgumentException( + "Invalid verbosity '" + reportVerbosity + "'. Valid verbosities are: " + + validVerbosities, + e); + } } /** @@ -177,6 +213,49 @@ public void setReportVerbosity(final ReportVerbosity reportVerbosity) this.reportVerbosity.set(reportVerbosity); } + /** + * Sets the report color scheme. + * + * @param reportColorScheme + * color scheme to use + */ + public void setReportColorScheme(final String reportColorScheme) + { + this.setReportColorScheme(convertColorScheme(reportColorScheme)); + } + + private static ColorScheme convertColorScheme(final String reportColorScheme) + { + if (reportColorScheme == null) + { + return null; + } + try + { + return ColorScheme.valueOf(reportColorScheme.toUpperCase(Locale.ROOT)); + } + catch (final IllegalArgumentException e) + { + final String validColorSchemes = Arrays.stream(ColorScheme.values()).map(ColorScheme::name) + .collect(joining(", ")); + throw new IllegalArgumentException( + "Invalid color scheme '" + reportColorScheme + "'. Valid color schemes are: " + + validColorSchemes, + e); + } + } + + /** + * Sets the report color scheme. + * + * @param reportColorScheme + * color scheme to use + */ + public void setReportColorScheme(final ColorScheme reportColorScheme) + { + this.reportColorScheme.set(reportColorScheme); + } + /** * Sets the report format. * @@ -254,6 +333,17 @@ public void setFilterAcceptsItemsWithoutTag(final boolean filterAcceptsItemsWith this.filterAcceptsItemsWithoutTag.set(filterAcceptsItemsWithoutTag); } + /** + * Sets the report details section display setting by name. + * + * @param detailsSectionDisplay + * display setting name + */ + public void setDetailsSectionDisplay(final DetailsSectionDisplay detailsSectionDisplay) + { + this.detailsSectionDisplay.set(detailsSectionDisplay); + } + /** * Sets the report details section display setting by name. * @@ -262,7 +352,28 @@ public void setFilterAcceptsItemsWithoutTag(final boolean filterAcceptsItemsWith */ public void setDetailsSectionDisplay(final String detailsSectionDisplay) { - this.detailsSectionDisplay.set(DetailsSectionDisplay.valueOf(detailsSectionDisplay)); + this.setDetailsSectionDisplay(convertDetailsSelectionDisplay(detailsSectionDisplay)); + } + + private static DetailsSectionDisplay convertDetailsSelectionDisplay(final String detailsSectionDisplay) + { + if (detailsSectionDisplay == null) + { + return null; + } + try + { + return DetailsSectionDisplay.valueOf(detailsSectionDisplay.toUpperCase(Locale.ROOT)); + } + catch (final IllegalArgumentException e) + { + final String validValues = Arrays.stream(DetailsSectionDisplay.values()).map(Enum::name) + .collect(Collectors.joining(", ")); + throw new IllegalArgumentException( + "Invalid details section display '" + detailsSectionDisplay + "'. Valid values are: " + + validValues, + e); + } } /** @@ -310,8 +421,8 @@ public void setFailBuild(final boolean failBuild) @Override public String toString() { - return "TracingConfig [reportVerbosity=" + reportVerbosity + ", inputDirectories=" - + inputDirectories + ", reportFile=" + reportFile + ", pathConfig=" + return "TracingConfig [reportVerbosity=" + reportVerbosity + ", reportColorScheme=" + reportColorScheme + + ", inputDirectories=" + inputDirectories + ", reportFile=" + reportFile + ", pathConfig=" + getTagPathConfig() + ", failBuild=" + failBuild + ", 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 a68e798..4821745 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java @@ -14,6 +14,7 @@ import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.*; import org.itsallcode.openfasttrace.api.*; +import org.itsallcode.openfasttrace.api.ReportSettings.Builder; import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.ImportSettings; import org.itsallcode.openfasttrace.api.report.ReportVerbosity; @@ -25,10 +26,14 @@ @CacheableTask public class TraceTask extends DefaultTask { + private static final ColorScheme DEFAULT_COLOR_SCHEME = ColorScheme.BLACK_AND_WHITE; + private final RegularFileProperty requirementsFile = getProject().getObjects().fileProperty(); private final RegularFileProperty outputFile = getProject().getObjects().fileProperty(); private final Property reportVerbosity = getProject().getObjects() .property(ReportVerbosity.class); + private final Property reportColorScheme = getProject().getObjects() + .property(ColorScheme.class); private final Property reportFormat = getProject().getObjects().property(String.class); private final Property detailsSectionDisplay = getProject().getObjects() .property(DetailsSectionDisplay.class); @@ -83,6 +88,18 @@ public Property getReportVerbosity() return reportVerbosity; } + /** + * Get the report color scheme property. + * + * @return the color scheme property + */ + @Input + @Optional + public Property getReportColorScheme() + { + return reportColorScheme; + } + /** * Returns the report format property. * @@ -213,22 +230,23 @@ public void trace() private ReportSettings getReportSettings() { - getLogger().info("Report settings: verbosity={}, format={}, detailsSectionDisplay={}", - reportVerbosity.get(), reportFormat.get(), detailsSectionDisplay.get()); - return ReportSettings.builder() // - .verbosity(reportVerbosity.get()) // - .outputFormat(reportFormat.get()) // - .showOrigin(true) // - .newline(Newline.UNIX) // - .detailsSectionDisplay(detailsSectionDisplay.get()) // - .build(); + getLogger().info("Report settings: verbosity={}, format={}, detailsSectionDisplay={}, colorScheme={}", + reportVerbosity.get(), reportFormat.get(), detailsSectionDisplay.get(), reportColorScheme.getOrNull()); + final Builder builder = ReportSettings.builder() + .verbosity(reportVerbosity.get()) + .outputFormat(reportFormat.get()) + .showOrigin(true) + .newline(Newline.UNIX) + .detailsSectionDisplay(detailsSectionDisplay.get()) + .colorScheme(reportColorScheme.getOrElse(DEFAULT_COLOR_SCHEME)); + return builder.build(); } private ImportSettings getImportSettings() { - return ImportSettings.builder() // - .addInputs(getAllImportFiles()) // - .filter(getFilterSettings()) // + return ImportSettings.builder() + .addInputs(getAllImportFiles()) + .filter(getFilterSettings()) .build(); } diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java index 536e53d..27380c4 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java @@ -275,6 +275,21 @@ void filteredWantedStatusesInvalidStatus() "Invalid status 'invalid'. Valid statuses are: APPROVED, PROPOSED, DRAFT, REJECTED")); } + @Test + void reportColorSchemeColor() + { + fixture(PROJECT_CUSTOM_CONFIG_DIR) + .withoutBuildCache() + .withArgs("clean", "traceRequirements", "-PreportColorScheme=color") + .withReportFile(Path.of("build/custom-report.txt")) + .run() + .assertTraceOutcomeSuccess() + .assertOutput(containsString( + "Report settings: verbosity=ALL, format=plain, detailsSectionDisplay=COLLAPSE, colorScheme=COLOR")) + .assertReportFileLines( + "\u001B[91mnot ok\u001B[0m - 2 total, 1 direct, 0 transitive defects"); + } + @Test void testTraceExampleProjectWithCustomConfigFailBuildErrorMessage() { diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java b/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java index 90869b6..8951308 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/PluginTestFixture.java @@ -20,6 +20,7 @@ class PluginTestFixture private final Path projectDir; private String[] arguments; private Path relativeReportPath; + private boolean buildCache = true; private PluginTestFixture(final GradleTestConfig config, final Path projectDir) { @@ -38,6 +39,12 @@ PluginTestFixture withArgs(final String... args) return this; } + PluginTestFixture withoutBuildCache() + { + this.buildCache = false; + return this; + } + public PluginTestFixture withReportFile(final Path relativeReportPath) { this.relativeReportPath = relativeReportPath; @@ -64,7 +71,11 @@ private GradleRunner createGradleRunner() configureJacoco(projectDir); final List allArgs = new ArrayList<>(); allArgs.addAll(List.of(arguments)); - allArgs.addAll(List.of("--info", "--stacktrace", "--build-cache")); + allArgs.addAll(List.of("--info", "--stacktrace")); + if (buildCache) + { + allArgs.add("--build-cache"); + } allArgs.addAll(List.of("--configuration-cache", "--configuration-cache-problems=fail")); allArgs.addAll(List.of("--warning-mode", "fail")); final GradleRunner runner = GradleRunner.create() @@ -104,9 +115,10 @@ private Result(final BuildResult buildResult) this.buildResult = buildResult; } - void assertOutput(final Matcher matcher) + Result assertOutput(final Matcher matcher) { assertThat(buildResult.getOutput(), matcher); + return this; } Result assertTraceOutcomeSuccessOrFromCache() @@ -122,6 +134,11 @@ Result assertTraceOutcomeSuccessFromCacheOrUpToDate() .or(is(TaskOutcome.UP_TO_DATE))); } + Result assertTraceOutcomeSuccess() + { + return assertOutcome(":traceRequirements", is(TaskOutcome.SUCCESS)); + } + Result assertCollectOutcomeUpToDate() { return this.assertOutcome(":collectRequirements", TaskOutcome.UP_TO_DATE); diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/config/TracingConfigTest.java b/src/test/java/org/itsallcode/openfasttrace/gradle/config/TracingConfigTest.java new file mode 100644 index 0000000..3afd69b --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/config/TracingConfigTest.java @@ -0,0 +1,95 @@ +package org.itsallcode.openfasttrace.gradle.config; + +import static org.junit.jupiter.api.Assertions.*; + +import org.gradle.testfixtures.ProjectBuilder; +import org.itsallcode.openfasttrace.api.ColorScheme; +import org.itsallcode.openfasttrace.api.DetailsSectionDisplay; +import org.itsallcode.openfasttrace.api.report.ReportVerbosity; +import org.junit.jupiter.api.Test; + +class TracingConfigTest +{ + private final TracingConfig tracingConfig = new TracingConfig(ProjectBuilder.builder().build()); + + @Test + void acceptsColorSchemeNameIgnoringCase() + { + tracingConfig.setReportColorScheme("color"); + + assertEquals(ColorScheme.COLOR, tracingConfig.getReportColorScheme().get()); + } + + @Test + void acceptsNullColorScheme() + { + tracingConfig.setReportColorScheme((String) null); + + assertFalse(tracingConfig.getReportColorScheme().isPresent()); + } + + @Test + void rejectsInvalidColorSchemeWithValueAndValidValues() + { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tracingConfig.setReportColorScheme("rainbow")); + + assertEquals( + "Invalid color scheme 'rainbow'. Valid color schemes are: BLACK_AND_WHITE, MONOCHROME, COLOR", + exception.getMessage()); + } + + @Test + void acceptsVerbosityNameIgnoringCase() + { + tracingConfig.setReportVerbosity("summary"); + + assertEquals(ReportVerbosity.SUMMARY, tracingConfig.getReportVerbosity().get()); + } + + @Test + void acceptsNullVerbosity() + { + tracingConfig.setReportVerbosity((String) null); + + assertFalse(tracingConfig.getReportVerbosity().isPresent()); + } + + @Test + void rejectsInvalidVerbosityWithValueAndValidValues() + { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tracingConfig.setReportVerbosity("extreme")); + + assertEquals( + "Invalid verbosity 'extreme'. Valid verbosities are: QUIET, MINIMAL, SUMMARY, FAILURES, DIRECT_FAILURES, FAILURE_SUMMARIES, DIRECT_FAILURE_SUMMARIES, FAILURE_DETAILS, DIRECT_FAILURE_DETAILS, ALL", + exception.getMessage()); + } + + @Test + void acceptsDetailsSectionDisplayNameIgnoringCase() + { + tracingConfig.setDetailsSectionDisplay("collapse"); + + assertEquals(DetailsSectionDisplay.COLLAPSE, tracingConfig.getDetailsSectionDisplay().get()); + } + + @Test + void acceptsNullDetailsSectionDisplay() + { + tracingConfig.setDetailsSectionDisplay((String) null); + + assertFalse(tracingConfig.getDetailsSectionDisplay().isPresent()); + } + + @Test + void rejectsInvalidDetailsSectionDisplayWithValueAndValidValues() + { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tracingConfig.setDetailsSectionDisplay("invalid")); + + assertEquals( + "Invalid details section display 'invalid'. Valid values are: COLLAPSE, EXPAND", + exception.getMessage()); + } +} \ No newline at end of file