diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 57aeae1..12f006d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,8 +5,6 @@ on: branches: [ main ] pull_request: branches: [ main ] - schedule: - - cron: '0 4 * * 3' jobs: analyze: diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 6711c38..f6b15fb 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -106,7 +106,7 @@ org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 1984e5b..a084115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2024-11-13 + +- [#70](https://github.com/itsallcode/openfasttrace-maven-plugin/issues/70) Add support for OFT's command line option `--wanted-tags` + ## [2.2.0] - 2024-08-21 - [PR #66](https://github.com/itsallcode/openfasttrace-maven-plugin/issues/66) Add filter for artifact types. diff --git a/README.md b/README.md index 4585bc5..d0862fe 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Add the openfasttrace-maven-plugin to your `pom.xml`: true COLLAPSE feat,req + prototype,mvp ``` @@ -62,7 +63,7 @@ See [src/test/resources/empty-project](src/test/resources/simple-project/) for a ### OpenFastTrace Plugins -You can use OpenFastTrace plugins to import and export requirements in additional formats. Include plugins by adding them as a dependency to the `openfasttrace-maven-plugin`, see [project-with-plugins](src/test/resources/project-with-plugins) as an example. +You can use OpenFastTrace plugins to import and export requirements in additional formats. Include plugins by adding them as a dependency to the `openfasttrace-maven-plugin`, see [project-with-plugins](./src/test/resources/project-with-plugins) as an example. ```xml @@ -153,11 +154,15 @@ You can add additional resource directories using the [Maven Resources Plugin](h ``` -#### Selecting the Imported ArtifactTypes +#### Selecting the Imported Specification Items Sometimes you don't want to trace the whole requirement chain. Instead, you are interested in the consistency of a subset. For instance, if you need to deliver a system requirement specification to another team, your job is to assure that the document is consistent in itself. -For those cases you can add an include list to the configuration that explicitly lists all artifact types to be imported. Note that this also affects which required coverage is imported — which is exactly what you want in this situation. +For those cases you can add an include list to the configuration that explicitly lists all artifact types or tags to be imported. Note that this also affects which required coverage is imported — which is exactly what you want in this situation. + +See the [OFT user guide on import options](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide.md#import-options) for details. + +##### Select Artifact Types The following example configuration limits import to artifact types `feat` and `req`. @@ -167,6 +172,24 @@ The following example configuration limits import to artifact types `feat` and ` ``` +This works similar to OFT's command line argument `--wanted-artifact-types`. + +##### Select Tags + +The following example configuration limits import to tags `prototype` and `mvp`. + +```xml + + prototype,mvp + +``` + +This works similar to OFT's command line argument `--wanted-tags`. + +You can specify the underscore `_` to import specification items without tags. + +You can also specify the tags to import using CLI option `-Dtags=prototype,mvp`. + #### Report ##### Report Format diff --git a/pom.xml b/pom.xml index 136d485..f987cee 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 4.1.0 3.8.7 true - 5.11.0 + 5.11.3 0.8.12 itsallcode https://sonarcloud.io @@ -98,7 +98,7 @@ org.apache.maven.plugin-tools maven-plugin-annotations - 3.13.1 + 3.15.1 provided @@ -118,7 +118,7 @@ commons-io commons-io - 2.16.1 + 2.17.0 test @@ -171,6 +171,18 @@ ${junit.version} test + + org.mockito + mockito-junit-jupiter + 5.14.2 + test + + + org.itsallcode + hamcrest-auto-matcher + 0.8.2 + test + org.jacoco org.jacoco.agent @@ -199,7 +211,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.5 + 3.2.7 sign-artifacts @@ -322,7 +334,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.14.0 + 3.15.1 openfasttrace false @@ -365,7 +377,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.8.0 + 3.11.1 attach-javadocs @@ -379,7 +391,7 @@ true true - + false @@ -433,7 +445,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.7.1 + 3.8.1 copy-jacoco @@ -453,11 +465,8 @@ org.apache.maven.plugins maven-surefire-plugin - 3.4.0 + 3.5.2 - - **VerifierTest.java - src/test/resources/logging.properties @@ -466,11 +475,8 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.4.0 + 3.5.2 - - **VerifierTest.java - true src/test/resources/logging.properties @@ -553,7 +559,7 @@ org.apache.maven.plugins maven-artifact-plugin - 3.5.1 + 3.5.3 verify-reproducible-build diff --git a/src/main/java/org/itsallcode/openfasttrace/maven/TraceMojo.java b/src/main/java/org/itsallcode/openfasttrace/maven/TraceMojo.java index 5db9532..aa7be2d 100644 --- a/src/main/java/org/itsallcode/openfasttrace/maven/TraceMojo.java +++ b/src/main/java/org/itsallcode/openfasttrace/maven/TraceMojo.java @@ -1,24 +1,22 @@ package org.itsallcode.openfasttrace.maven; -import static java.util.stream.Collectors.toList; +import static java.util.Collections.emptySet; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Stream; +import javax.inject.Inject; + import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.*; import org.apache.maven.project.*; -import org.itsallcode.openfasttrace.api.DetailsSectionDisplay; -import org.itsallcode.openfasttrace.api.FilterSettings; -import org.itsallcode.openfasttrace.api.ReportSettings; +import org.itsallcode.openfasttrace.api.*; import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.api.importer.ImportSettings; import org.itsallcode.openfasttrace.api.report.ReportVerbosity; @@ -32,6 +30,8 @@ @Mojo(name = "trace", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true) public class TraceMojo extends AbstractMojo { + private static final String WILDCARD_TAG = "_"; + /** * Location of the directory where the reports are generated. *

@@ -99,10 +99,23 @@ public class TraceMojo extends AbstractMojo * will be applied. *

  • If the artifactTypes set is not null, only artifacts with types that * match the specified types will be imported.
  • + * */ @Parameter(property = "artifactTypes") - private Set artifactTypes; - + Set artifactTypes; + + /** + * Determines which tags should be imported. + *

    + * Import only specification items that have at least one of the listed + * tags. If you add a single underscore {@code _}, specification items that + * have no tags at all are also imported. + *

    + * Default: Import all specification items. + */ + @Parameter(property = "tags") + Set tags; + /** * Skip running OFT. *

    @@ -114,19 +127,35 @@ public class TraceMojo extends AbstractMojo @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; - @Component - private ProjectBuilder mavenProjectBuilder; - @Parameter(defaultValue = "${session}", readonly = true) private MavenSession session; + private final ProjectBuilder mavenProjectBuilder; + + /** + * Create a new instance. + * + * @param mavenProjectBuilder + * maven project builder + */ + @Inject + public TraceMojo(final ProjectBuilder mavenProjectBuilder) + { + this.mavenProjectBuilder = mavenProjectBuilder; + } /** - * Create a new instance + * Constructor used in unit tests. + * + * @param mavenProjectBuilder + * maven project builder + * @param project + * maven project */ - public TraceMojo() + TraceMojo(final ProjectBuilder mavenProjectBuilder, final MavenProject project) { - // Added default constructor to fix javadoc warning + this(mavenProjectBuilder); + this.project = project; } @Override @@ -216,7 +245,7 @@ private static void createDir(final Path path) } } - private ImportSettings createImportSettings() + ImportSettings createImportSettings() { final List sourcePaths = getSourcePaths(); logSourcePaths(sourcePaths); @@ -228,14 +257,38 @@ private ImportSettings createImportSettings() getLog().info("Tracing doc directory " + docPath.get()); settings.addInputs(docPath.get()); } - if (artifactTypes != null) + final FilterSettings filterSettings = FilterSettings.builder() + .artifactTypes(getFilteredArtifactTypes()) + .tags(getFilteredTags()) + .withoutTags(isFilterWithoutTags()) + .build(); + settings.filter(filterSettings); + return settings.build(); + } + + private Set getFilteredArtifactTypes() + { + return artifactTypes == null ? emptySet() : artifactTypes; + } + + private Set getFilteredTags() + { + if (tags == null) { - FilterSettings filterSettings = FilterSettings.builder() - .artifactTypes(artifactTypes) - .build(); - settings.filter(filterSettings); + return emptySet(); } - return settings.build(); + final Set copy = new HashSet<>(tags); + copy.remove(WILDCARD_TAG); + return copy; + } + + private boolean isFilterWithoutTags() + { + if (tags == null || tags.isEmpty()) + { + return true; + } + return tags.contains(WILDCARD_TAG); } private void logSourcePaths(final List sourcePaths) @@ -245,7 +298,7 @@ private void logSourcePaths(final List sourcePaths) return; } final Path baseDir = project.getBasedir().toPath(); - final List relativePaths = sourcePaths.stream().map(baseDir::relativize).collect(toList()); + final List relativePaths = sourcePaths.stream().map(baseDir::relativize).toList(); getLog().info( "Tracing " + sourcePaths.size() + " sub-directories of base dir " + baseDir + ": " + relativePaths); @@ -284,15 +337,15 @@ private List getSourcePathOfProject(final MavenProject mavenProject) final List compileSourceRoots = mavenProject.getCompileSourceRoots(); final List testCompileSourceRoots = mavenProject.getTestCompileSourceRoots(); final List resourceDirs = mavenProject.getResources().stream().map(Resource::getDirectory) - .collect(toList()); + .toList(); final List testResourceDirs = mavenProject.getTestResources().stream().map(Resource::getDirectory) - .collect(toList()); + .toList(); final Stream sourcePaths = Stream .of(compileSourceRoots, resourceDirs, testCompileSourceRoots, testResourceDirs) .flatMap(List::stream) .map(Path::of) .filter(Files::exists); - return Stream.concat(sourcePathsOfSubModules, sourcePaths).collect(toList()); + return Stream.concat(sourcePathsOfSubModules, sourcePaths).toList(); } private Optional getProjectSubPath(final String dir) diff --git a/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoVerifierTest.java b/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoIT.java similarity index 83% rename from src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoVerifierTest.java rename to src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoIT.java index b76f574..5d7719a 100644 --- a/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoVerifierTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoIT.java @@ -16,12 +16,14 @@ import org.apache.maven.it.Verifier; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import com.exasol.mavenpluginintegrationtesting.MavenIntegrationTestEnvironment; -class TraceMojoVerifierTest +class TraceMojoIT { - private static final Logger LOG = Logger.getLogger(TraceMojoVerifierTest.class.getName()); + private static final Logger LOG = Logger.getLogger(TraceMojoIT.class.getName()); private static final Path BASE_TEST_DIR = Paths.get("src/test/resources").toAbsolutePath(); private static final String CURRENT_PLUGIN_VERSION = getCurrentProjectVersion(); private static final String OFT_GOAL = "org.itsallcode:openfasttrace-maven-plugin:" + CURRENT_PLUGIN_VERSION @@ -37,12 +39,14 @@ class TraceMojoVerifierTest private static final Path EMPTY_PROJECT = BASE_TEST_DIR.resolve("empty-project"); private static final Path SIMPLE_PROJECT = BASE_TEST_DIR.resolve("simple-project"); private static final Path PROJECT_WITH_PLUGINS = BASE_TEST_DIR.resolve("project-with-plugins"); + private static final Path PROJECT_WITH_TAGS = BASE_TEST_DIR.resolve("project-with-tags"); private static final Path TRACING_DEFECTS = BASE_TEST_DIR.resolve("project-with-tracing-defects"); private static final Path TRACING_DEFECTS_FAIL_BUILD = BASE_TEST_DIR .resolve("project-with-tracing-defects-fail-build"); private static final Path HTML_REPORT_PROJECT = BASE_TEST_DIR .resolve("html-report"); - public static final Path PARTIAL_ARTIFACT_COVERAGE_PROJECT = BASE_TEST_DIR.resolve("project-with-partial-artifact-coverage"); + public static final Path PARTIAL_ARTIFACT_COVERAGE_PROJECT = BASE_TEST_DIR + .resolve("project-with-partial-artifact-coverage"); private static MavenIntegrationTestEnvironment mvnITEnv; @BeforeAll @@ -65,8 +69,8 @@ void testTracingWithMultipleLanguages() throws Exception verifier.setCliOptions(List.of("-pl .")); verifier.executeGoals(List.of("generate-sources", "generate-test-sources", OFT_GOAL)); verifier.verifyErrorFreeLog(); - assertThat(fileContent(PROJECT_WITH_MULTIPLE_LANGUAGES.resolve("target/tracing-report.txt")) - , equalTo("ok - 8 total\n")); + assertThat(fileContent(PROJECT_WITH_MULTIPLE_LANGUAGES.resolve("target/tracing-report.txt")), + equalTo("ok - 8 total\n")); } @Test @@ -173,7 +177,7 @@ void testTracingFindsDefectsFailBuild() () -> runTracingMojo(TRACING_DEFECTS_FAIL_BUILD)); assertAll(() -> assertThat(exception.getMessage(), containsString("Tracing found 1 defects out of 2 items")), () -> assertThat(fileContent(TRACING_DEFECTS_FAIL_BUILD.resolve("target/tracing-report.txt")), - containsString("not ok - 2 total, 1 defect"))); + containsString("not ok - 2 total, 1 defect"))); } @Test @@ -183,7 +187,7 @@ void testHtmlReport() throws Exception final String content = fileContent(HTML_REPORT_PROJECT.resolve("target/tracing-report.html")); assertAll(() -> assertThat(content, containsString(" 3 total")), - () ->assertThat(content, containsString("

    "))); + () -> assertThat(content, containsString("
    "))); } @Test @@ -209,6 +213,35 @@ void testTracingSelectedArtifactTypes() throws Exception equalTo("ok - 2 total\n")); } + @ParameterizedTest(name = "wanted tags {0} finds {1} items") + @CsvSource(delimiter = ';', nullValues = "NULL", value = + { "NULL; 3", "tagA; 1", "tagB; 1", "tagA,tagB; 2", "tagA,tagB,_; 3", "tagC; 0", "_,tagA; 2", + // This should find 1 item but finds 3 due to a bug in OFT: + // https://github.com/itsallcode/openfasttrace/issues/432 + "_; 3" }) + void testTracingSelectedTags(final String tags, final int expectedItemCount) throws Exception + { + final Verifier verifier = mvnITEnv.getVerifier(PROJECT_WITH_TAGS); + verifier.addCliOption("-DfailBuild=false"); + if (tags != null) + { + verifier.addCliOption("-Dtags=" + tags); + } + verifier.executeGoal(OFT_GOAL); + verifier.verifyErrorFreeLog(); + final String expectedResult; + if (expectedItemCount > 0) + { + expectedResult = "not ok - %1$d total, %1$d defect".formatted(expectedItemCount); + } + else + { + expectedResult = "ok - 0 total"; + } + assertThat(fileContent(PROJECT_WITH_TAGS.resolve("target/tracing-report.txt")), + containsString(expectedResult)); + } + private static void runTracingMojo(final Path projectDir) throws Exception { final Verifier verifier = mvnITEnv.getVerifier(projectDir); diff --git a/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoTest.java b/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoTest.java new file mode 100644 index 0000000..8e006ce --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/maven/TraceMojoTest.java @@ -0,0 +1,161 @@ +package org.itsallcode.openfasttrace.maven; + +import static java.util.Collections.emptySet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import org.apache.maven.model.Resource; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuilder; +import org.itsallcode.matcher.auto.AutoMatcher; +import org.itsallcode.openfasttrace.api.FilterSettings; +import org.itsallcode.openfasttrace.api.importer.ImportSettings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TraceMojoTest +{ + @Mock + ProjectBuilder mavenProjectBuilderMock; + @Mock + MavenProject projectMock; + @TempDir + Path baseDir; + + TraceMojo testee; + + @BeforeEach + void setup() + { + this.testee = createMojo(); + } + + TraceMojo createMojo() + { + when(projectMock.getBasedir()).thenReturn(baseDir.toFile()); + return new TraceMojo(mavenProjectBuilderMock, projectMock); + } + + @Test + void createDefaultImportSettings() + { + testee.artifactTypes = null; + assertThat(testee.createImportSettings(), AutoMatcher.equalTo(ImportSettings.builder().build())); + } + + @Test + void createImportSettingsAddsSourceRootPaths() throws IOException + { + final Path compileSrcPath = baseDir.resolve("src"); + final Path testCompileSrcPath = baseDir.resolve("test"); + Files.createDirectories(compileSrcPath); + Files.createDirectories(testCompileSrcPath); + when(projectMock.getCompileSourceRoots()).thenReturn(List.of(compileSrcPath.toString())); + when(projectMock.getTestCompileSourceRoots()).thenReturn(List.of(testCompileSrcPath.toString())); + + assertImportSettings(ImportSettings.builder().addInputs(compileSrcPath, testCompileSrcPath)); + } + + @Test + void createImportSettingsAddsResourcesDir() throws IOException + { + final Path resourcesPath = baseDir.resolve("res"); + Files.createDirectories(resourcesPath); + when(projectMock.getResources()).thenReturn(List.of(resource(resourcesPath))); + + assertImportSettings(ImportSettings.builder().addInputs(resourcesPath)); + } + + private static Resource resource(final Path directory) + { + final Resource resource = new Resource(); + resource.setDirectory(directory.toString()); + return resource; + } + + @Test + void createImportSettingsAddsDocPath() throws IOException + { + final Path docDir = baseDir.resolve("doc"); + Files.createDirectories(docDir); + + assertImportSettings(ImportSettings.builder().addInputs(docDir)); + } + + @Test + void createImportSettingsWithNullArtifactTypes() + { + testee.artifactTypes = null; + assertFilterSettings(FilterSettings.builder()); + } + + @Test + void createImportSettingsWithEmptyArtifactTypes() + { + testee.artifactTypes = emptySet(); + assertFilterSettings(FilterSettings.builder()); + } + + @Test + void createImportSettingsWithArtifactTypes() + { + testee.artifactTypes = Set.of("feat", "req"); + assertFilterSettings(FilterSettings.builder().artifactTypes(Set.of("feat", "req"))); + } + + @Test + void createImportSettingsWithNullTags() + { + testee.tags = null; + assertFilterSettings(FilterSettings.builder()); + } + + @Test + void createImportSettingsWithEmptyTags() + { + testee.tags = emptySet(); + assertFilterSettings(FilterSettings.builder()); + } + + @Test + void createImportSettingsWithTags() + { + testee.tags = Set.of("tag1", "tag2"); + assertFilterSettings(FilterSettings.builder().tags(Set.of("tag1", "tag2")).withoutTags(false)); + } + + @Test + void createImportSettingsWithWildcardTag() + { + testee.tags = Set.of("_", "tag1", "tag2"); + assertFilterSettings(FilterSettings.builder().tags(Set.of("tag1", "tag2")).withoutTags(true)); + } + + @Test + void createImportSettingsWithOnlyWildcardTag() + { + testee.tags = Set.of("_"); + assertFilterSettings(FilterSettings.builder().tags(emptySet()).withoutTags(true)); + } + + private void assertImportSettings(final ImportSettings.Builder importSettingsBuilder) + { + assertThat(testee.createImportSettings(), AutoMatcher.equalTo(importSettingsBuilder.build())); + } + + private void assertFilterSettings(final FilterSettings.Builder filterSettingsBuilder) + { + assertImportSettings(ImportSettings.builder().filter(filterSettingsBuilder.build())); + } +} diff --git a/src/test/resources/project-with-tags/doc/spec.md b/src/test/resources/project-with-tags/doc/spec.md new file mode 100644 index 0000000..25433a6 --- /dev/null +++ b/src/test/resources/project-with-tags/doc/spec.md @@ -0,0 +1,13 @@ +# Without Tag +`dsn~without-tag~1` +Needs: impl + +# With Tag A +`dsn~with-tag-a~1` +Tags: tagA +Needs: impl + +# With Tag B +`dsn~with-tag-b~1` +Tags: tagB +Needs: impl diff --git a/src/test/resources/project-with-tags/pom.xml b/src/test/resources/project-with-tags/pom.xml new file mode 100644 index 0000000..70e1448 --- /dev/null +++ b/src/test/resources/project-with-tags/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.itsallcode + openfasttrace-maven-plugin-test + 0.0.0 + jar + + + 17 + 17 + UTF-8 + + + + + + org.itsallcode + openfasttrace-maven-plugin + + + trace-requirements + + trace + + + + + plain + + + + +