diff --git a/README.md b/README.md index 27009bf..2752bae 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Gradle plugin for the requirement tracing suite [OpenFastTrace](https://github.c ```groovy plugins { - id "org.itsallcode.openfasttrace" version "3.0.1" + id "org.itsallcode.openfasttrace" version "3.2.0" } ``` @@ -50,10 +50,12 @@ You can configure the following properties: * `failBuild`: Fail build when tracing finds any issues (default: `true`) * `inputDirectories`: Files or directories to import -* `reportFile`: Path to the report file +* `reportFile`: Path to the report file (do not specify when using the `ux` `reportFormat`) * `reportFormat`: Format of the report * `plain` - Plain Text (default) * `html` - HTML + * `aspec` - Extended requirement.xml format + * `ux` - HTML based requirement browser * `reportVerbosity`: Report verbosity * `quiet` - no output (in case only the return code is used) * `minimal` - display ok or not ok diff --git a/build.gradle b/build.gradle index bd1a741..13634ef 100644 --- a/build.gradle +++ b/build.gradle @@ -10,17 +10,21 @@ plugins { } repositories { + if (!gradle.startParameter.taskNames.contains("publish")) { + mavenLocal() + } mavenCentral() } apply from: 'gradle/workAroundJacocoGradleTestKitIssueOnWindows.gradle' -version = '3.0.1' +version = '3.2.0' group = 'org.itsallcode' ext { gradlePluginId = 'org.itsallcode.openfasttrace' - oftVersion = '4.1.0' + oftVersion = '4.2.0' + uxVersion = '0.1.0' junitVersion = '5.11.0' if (project.hasProperty('oftSourceDir')) { oftSourceDir = file(project.oftSourceDir) @@ -93,6 +97,45 @@ task compileOft(type: Exec) { '-Djava.version=' + getJavaVersion() } +// >> openfasttrace-ux + + +ext { + if (project.hasProperty('uxSourceDir')) { + uxSourceDir = file(project.uxSourceDir) + } else { + uxSourceDir = 'oft-dir-not-found' + } +} + +tasks.register("compileUx", Exec) { + workingDir uxSourceDir + + commandLine 'npm', 'run', 'deploy' +} +compileUx.onlyIf { project.file(uxSourceDir).isDirectory() } + +configurations { + create("openfasttraceux") +} +dependencies { + openfasttraceux "org.itsallcode:openfasttrace-ux:$uxVersion" +} + +task addOftUxAsResource(type: Copy) { + description "Adds the OpenFastTrace-UX ZIP artifact as file resources to the gradle plugin" + dependsOn(compileUx) + + from configurations.getByName("openfasttraceux").getSingleFile() + into layout.buildDirectory.dir("resources/main") + rename { "openfasttrace-ux.zip" } +} +tasks.named("processResources") { + dependsOn("addOftUxAsResource") +} + +// << openfasttrace-ux + clean { def exampleProjects = rootProject.file('example-projects').listFiles() def propertyFiles = exampleProjects.collect { new File(it, 'gradle.properties') } @@ -118,6 +161,9 @@ signing { def signingPassword = findProperty("signingPassword") useInMemoryPgpKeys(signingKey, signingPassword) } +tasks.withType(Sign) { + onlyIf { !gradle.startParameter.taskNames.contains("publishToMavenLocal") } +} testing { suites { diff --git a/example-projects/ux-report/build.gradle b/example-projects/ux-report/build.gradle new file mode 100644 index 0000000..3188224 --- /dev/null +++ b/example-projects/ux-report/build.gradle @@ -0,0 +1,28 @@ +plugins { + id "base" + id 'org.itsallcode.openfasttrace' version '3.2.0' +} + + +requirementTracing { + failBuild = false + if (project.hasProperty('oftSourceDir')) { + oftSourceDir = file(project.oftSourceDir) + if (!oftSourceDir.exists()) { + logger.warn "OFT source directory $oftSourceDir does not exist" + } + inputDirectories = files(project.fileTree(dir:oftSourceDir).matching { + include "doc/spec/**" + include "api/src/main/**" + include "api/src/test/**" + include "core/src/main/**" + include "core/src/test/java/**" + include "exporter/**/src/main/**" + include "importer/**/src/main/**" + include "reporter/**/src/main/**" + }) + } else { + inputDirectories = files('custom-dir') + } + reportFormat = 'ux' +} diff --git a/example-projects/ux-report/custom-dir/source.java b/example-projects/ux-report/custom-dir/source.java new file mode 100644 index 0000000..2e39f55 --- /dev/null +++ b/example-projects/ux-report/custom-dir/source.java @@ -0,0 +1 @@ +// [impl->dsn~exampleB~1] diff --git a/example-projects/ux-report/custom-dir/spec.md b/example-projects/ux-report/custom-dir/spec.md new file mode 100644 index 0000000..a938392 --- /dev/null +++ b/example-projects/ux-report/custom-dir/spec.md @@ -0,0 +1,16 @@ +# Tracing Example + +`dsn~exampleB~1` + +Example requirement + +Needs: utest, impl + +# Rejected Example + +`dsn~rejected1~1` +Status: rejected + +Rejected requirement + +Needs: utest, impl diff --git a/example-projects/ux-report/settings.gradle b/example-projects/ux-report/settings.gradle new file mode 100644 index 0000000..8403701 --- /dev/null +++ b/example-projects/ux-report/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} +rootProject.name = 'UX Reporter Example' diff --git a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java index 5a00da0..b3162d6 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePlugin.java @@ -1,12 +1,5 @@ package org.itsallcode.openfasttrace.gradle; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; - -import java.io.File; -import java.util.*; -import java.util.stream.Stream; - import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -20,6 +13,16 @@ import org.itsallcode.openfasttrace.gradle.task.config.SerializableTagPathConfig; import org.slf4j.Logger; +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + public class OpenFastTracePlugin implements Plugin { private static final Logger LOG = Logging.getLogger(OpenFastTracePlugin.class); @@ -30,6 +33,7 @@ public void apply(final Project rootProject) { LOG.info("Initializing OpenFastTrack plugin for project '{}'", rootProject); rootProject.allprojects(OpenFastTracePlugin::createConfigDsl); + System.setProperty( "oftProjectName", rootProject.getName() ); createTasks(rootProject); } @@ -79,17 +83,7 @@ private static void configureTask(final Project rootProject, final TracingConfig config = getConfig(rootProject); task.getFailBuild().set(config.getFailBuild()); task.getRequirementsFile().set(collectTask.get().getOutputFile()); - if (config.getReportFile().isPresent()) - { - task.getOutputFile().set(config.getReportFile()); - } - else - { - final String extension = "html".equals(config.getReportFormat().get()) ? "html" : "txt"; - task.getOutputFile() - .set(new File(rootProject.getLayout().getBuildDirectory().getAsFile().get(), - "reports/tracing." + extension)); - } + configureOutputs(rootProject, task, config); task.getReportVerbosity().set(config.getReportVerbosity()); task.getReportFormat().set(config.getReportFormat()); task.getImportedRequirements().set(getImportedRequirements(rootProject.getAllprojects())); @@ -99,6 +93,39 @@ private static void configureTask(final Project rootProject, task.getDetailsSectionDisplay().set(config.getDetailsSectionDisplay()); } + private static void configureOutputs( final Project rootProject, + final TraceTask task, + final TracingConfig config ) + { + if( config.getReportFile().isPresent() ) + { + task.getOutputFile().set( config.getReportFile() ); + } + else + { + final String reporterFormat = config.getReportFormat().get(); + task.getOutputFile() + .set( new File( rootProject.getLayout().getBuildDirectory().getAsFile().get(), + toReporterFile( reporterFormat ) ) ); + final String resourceName = "openfasttrace-" + reporterFormat + ".zip"; + final URL resource = task.getClass().getClassLoader().getResource( resourceName ); + if( "ux".equals( reporterFormat ) ) + { + task.getReportFile().set( "build/reports/openfasttrace/openfasttrace.html" ); + } + if( resource != null ) + task.getAdditionalResources().add( resourceName ); + } + } + + private static String toReporterFile( final String reporterFormat ) + { + return "ux".equals( reporterFormat ) ? "reports/openfasttrace/resources/js/specitem_data.js" + : "html".equals( reporterFormat ) ? "reports/tracing.html" + : "reports/tracing.txt"; + + } + private static Set getAllInputDirectories(final Set allProjects) { return allProjects.stream() // 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 a1af6fd..279445b 100644 --- a/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java +++ b/src/main/java/org/itsallcode/openfasttrace/gradle/task/TraceTask.java @@ -4,9 +4,15 @@ import static java.util.stream.Collectors.toList; import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.List; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.gradle.api.DefaultTask; import org.gradle.api.file.RegularFileProperty; @@ -25,6 +31,7 @@ public class TraceTask extends DefaultTask { private final RegularFileProperty requirementsFile = getProject().getObjects().fileProperty(); private final RegularFileProperty outputFile = getProject().getObjects().fileProperty(); + private final Property reportFile = getProject().getObjects().property(String.class); private final Property reportVerbosity = getProject().getObjects() .property(ReportVerbosity.class); private final Property reportFormat = getProject().getObjects().property(String.class); @@ -39,6 +46,7 @@ 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 additionalResources = getProject().getObjects().setProperty(String.class); @InputFile public RegularFileProperty getRequirementsFile() @@ -52,6 +60,12 @@ public RegularFileProperty getOutputFile() return outputFile; } + @Input + @Optional + public Property getReportFile() { + return reportFile; + } + @Input public Property getReportVerbosity() { @@ -101,15 +115,34 @@ public Property getFailBuild() return failBuild; } + @Input + public SetProperty getAdditionalResources() + { + return additionalResources; + } + private boolean shouldFailBuild() { return failBuild.getOrElse(true); } + private String reportFile() { + return reportFile.getOrElse(getOutputFileInternal().toPath().toString()); + } + @TaskAction public void trace() { createReportOutputDir(); + + final Path reportPath = getOutputFileInternal().toPath(); + for( final String resourceName : additionalResources.get() ) { + final URL resourceURL = this.getClass().getClassLoader().getResource(resourceName); + if( resourceURL == null ) + throw new IllegalStateException("Resource " + resourceName + " does not exist"); + extractZipResource(resourceURL, reportPath.getParent().getParent().getParent().toFile()); + } + final Oft oft = new OftRunner(); final ImportSettings importSettings = getImportSettings(); final List importedItems = oft.importItems(importSettings); @@ -117,14 +150,13 @@ public void trace() importSettings.getInputs()); final List linkedItems = oft.link(importedItems); final Trace trace = oft.trace(linkedItems); - final Path reportPath = getOutputFileInternal().toPath(); getLogger().info("Tracing result: {} total items, {} defects. Writing report to {}", trace.count(), trace.countDefects(), reportPath); oft.reportToPath(trace, reportPath, getReportSettings()); if (trace.countDefects() > 0) { final String message = "Requirement tracing found " + trace.countDefects() - + " defects. See report at " + reportPath + " for details."; + + " defects. See report at " + reportFile() + " for details."; if (shouldFailBuild()) { throw new IllegalStateException(message); @@ -195,4 +227,36 @@ private File getOutputFileInternal() { return outputFile.getAsFile().get(); } + + private void extractZipResource(final URL resource, final File outputDir) { + getLogger().info("Extracting additional resource {}", resource.getPath()); + try(final ZipInputStream zipStream = new ZipInputStream(resource.openStream())) { + if( !outputDir.isDirectory() && !outputDir.mkdirs() ) { + throw new IllegalStateException("Error creating directory " + outputDir); + } + while( true ) { + final ZipEntry entry = zipStream.getNextEntry(); + if( entry == null ) + break; + File outputFile = new File(outputDir, entry.getName()); + + if( entry.isDirectory() ) { + if( !outputFile.isDirectory() && !outputFile.mkdirs() ) { + throw new IllegalStateException("Error creating directory " + outputFile); + } + } + else { + final File path = outputFile.getParentFile(); + if( !path.isDirectory() && !path.mkdirs() ) { + throw new IllegalStateException("Error creating directory " + path); + } + Files.copy(zipStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + zipStream.closeEntry(); + } + } + catch (IOException e) { + throw new IllegalStateException("Failed to extract resource " + resource.getPath(), e); + } + } } diff --git a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java index b562a30..fd4229a 100644 --- a/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/gradle/OpenFastTracePluginTest.java @@ -1,26 +1,34 @@ package org.itsallcode.openfasttrace.gradle; -import static java.util.Arrays.asList; -import static java.util.stream.Collectors.joining; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.ArrayList; -import java.util.List; - import org.gradle.api.logging.Logging; import org.gradle.internal.impldep.org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.gradle.internal.impldep.org.apache.commons.compress.archivers.zip.ZipFile; -import org.gradle.testkit.runner.*; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.Logger; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.joining; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + class OpenFastTracePluginTest { private static final Logger LOG = Logging.getLogger(OpenFastTracePluginTest.class); @@ -33,6 +41,7 @@ class OpenFastTracePluginTest private static final Path DEPENDENCY_CONFIG_DIR = EXAMPLES_DIR.resolve("dependency-config"); private static final Path PUBLISH_CONFIG_DIR = EXAMPLES_DIR.resolve("publish-config"); private static final Path HTML_REPORT_CONFIG_DIR = EXAMPLES_DIR.resolve("html-report"); + private static final Path UX_REPORT_CONFIG_DIR = EXAMPLES_DIR.resolve("ux-report"); @ParameterizedTest(name = "testTracingTaskAddedToProject {0}") @EnumSource @@ -142,6 +151,15 @@ void testTraceTaskUpToDateWhenAlreadyRun(final GradleTestConfig config) assertEquals(TaskOutcome.UP_TO_DATE, buildResult.task(":traceRequirements").getOutcome()); } + @ParameterizedTest(name = "testUxReporter {0}") + @EnumSource + void testUxReporter(final GradleTestConfig config) + { + BuildResult buildResult = runBuild(config , UX_REPORT_CONFIG_DIR, "clean", + "traceRequirements"); + assertEquals(TaskOutcome.SUCCESS, buildResult.task(":traceRequirements").getOutcome()); + } + @ParameterizedTest(name = "testTraceExampleProjectWithCustomConfig {0}") @EnumSource void testTraceExampleProjectWithCustomConfig(final GradleTestConfig config) throws IOException @@ -318,10 +336,10 @@ private static GradleRunner createGradleRunner(final GradleTestConfig config, allArgs.addAll(asList("--warning-mode", "all")); } final GradleRunner runner = GradleRunner.create() // - .withProjectDir(projectDir.toFile()) // - .withPluginClasspath() // - .withArguments(allArgs) // - .forwardOutput(); + .withProjectDir(projectDir.toFile()) // + .withPluginClasspath() // + .withArguments(allArgs) // + .forwardOutput(); if (config.gradleVersion != null) { runner.withGradleVersion(config.gradleVersion);