diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 954f95ce6..3a9b7f762 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,17 @@ version: 2 -updates: -# - package-ecosystem: "maven" -# directory: "/" -# schedule: -# interval: "monthly" - - package-ecosystem: "github-actions" - directory: "/" +multi-ecosystem-groups: + dependencies: schedule: interval: "monthly" + +updates: + - package-ecosystem: "github-actions" + directory: "/" + patterns: ["*"] + multi-ecosystem-group: "dependencies" + + - package-ecosystem: "maven" + directory: "/" + patterns: ["*"] + multi-ecosystem-group: "dependencies" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f8cc3558..3d3e1147e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,8 @@ jobs: include: - os: ubuntu-latest java: 21 + - os: ubuntu-latest + java: 25 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-os-${{ matrix.os }}-java-${{ matrix.java }} @@ -43,6 +45,9 @@ jobs: distribution: "temurin" java-version: ${{ matrix.java }} cache: "maven" + server-id: ossindex + server-username: OSSINDEX_USERNAME + server-password: OSSINDEX_TOKEN - name: Cache SonarQube packages if: ${{ env.DEFAULT_OS == matrix.os && env.DEFAULT_JAVA == matrix.java }} @@ -56,6 +61,9 @@ jobs: run: | mvn --batch-mode -T 1C clean org.jacoco:jacoco-maven-plugin:prepare-agent install \ -Djava.version=${{ matrix.java }} + env: + OSSINDEX_USERNAME: ${{ secrets.OSSINDEX_USERNAME }} + OSSINDEX_TOKEN: ${{ secrets.OSSINDEX_TOKEN }} - name: Sonar analysis if: ${{ env.DEFAULT_OS == matrix.os && env.DEFAULT_JAVA == matrix.java && env.SONAR_TOKEN != null }} diff --git a/.vscode/settings.json b/.vscode/settings.json index d878e3547..af5d05721 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,4 +24,5 @@ "connectionId": "itsallcode", "projectKey": "org.itsallcode.openfasttrace:openfasttrace-root" }, + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable", } diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliException.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliException.java index c8676ebce..69d3fd0fa 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliException.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliException.java @@ -30,4 +30,4 @@ public CliException(final String message) { super(message); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliStarter.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliStarter.java index 843897b1d..a94a433df 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliStarter.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliStarter.java @@ -38,7 +38,7 @@ public static void main(final String[] args) /** * Auxiliary entry point to the command line application that allows - * injection of a + * injection of a {@link DirectoryService}. * * @param args * command line arguments. @@ -63,6 +63,7 @@ public static void main(final String[] args, final DirectoryService directorySer } } + @SuppressWarnings("java:S1166") // Exceptions are reported to the user private static CliArguments parseCommandLineArguments(final String[] args, final DirectoryService directoryService) { @@ -126,6 +127,7 @@ public void run() } // [impl->dsn~cli.tracing.exit-status~1] + @SuppressWarnings("java:S1147") // Calling System.exit() intentionally private static void exit(final ExitStatus exitStatus) { System.exit(exitStatus.getCode()); diff --git a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliStarter.java b/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliStarter.java deleted file mode 100644 index 1782e6204..000000000 --- a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliStarter.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.itsallcode.openfasttrace.core.cli; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.itsallcode.junit.sysextensions.AssertExit; -import org.itsallcode.junit.sysextensions.ExitGuard; -import org.itsallcode.openfasttrace.api.exporter.ExporterException; -import org.itsallcode.openfasttrace.testutil.TestAssumptions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@SuppressWarnings("removal") // https://github.com/itsallcode/openfasttrace/issues/436 -@ExtendWith(ExitGuard.class) -class TestCliStarter -{ - @BeforeAll - static void assumeSecurityManagerSupported() - { - TestAssumptions.assumeSecurityManagerSupported(); - } - - @Test - void testRunWithoutArguments() - { - AssertExit.assertExitWithStatus(2, () -> run()); - } - - @Test - void testRunThrowingException() - { - final ExporterException exception = assertThrows(ExporterException.class, - () -> run("trace")); - assertThat(exception.getMessage(), - equalTo("Found no matching reporter for output format 'plain'")); - } - - @Test - void testRunWithUnknownCommand() - { - AssertExit.assertExitWithStatus(2, () -> run("unknownCommand")); - } - - @Test - void testRunWithHelpCommand() - { - AssertExit.assertExitWithStatus(0, () -> run("help")); - } - - private void run(final String... args) - { - CliStarter.main(args); - } -} diff --git a/doc/developer_guide.md b/doc/developer_guide.md index b2c75c125..54f4a043e 100644 --- a/doc/developer_guide.md +++ b/doc/developer_guide.md @@ -121,13 +121,20 @@ By default, OFT is built with Java 17. To build and test with a later version, add argument `-Djava.version=17` to the Maven command. - #### Speedup Build By default, Maven builds the OFT modules sequentially. To speed up the build and build modules in parallel, add argument `-T 1C` to the Maven command. +#### Run Single Integration Test + +Specify test class via system property `it.test` and module via command line option `-projects`: + +```sh +mvn -Dit.test=CliStarterIT failsafe:integration-test -projects product +``` + ### Run Requirements Tracing ```sh diff --git a/parent/pom.xml b/parent/pom.xml index b969f4939..69bdf1943 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -12,8 +12,8 @@ 4.2.1 17 - 5.12.2 - 3.5.3 + 6.0.1 + 3.5.4 UTF-8 UTF-8 ${git.commit.time} @@ -209,7 +209,7 @@ org.mockito mockito-junit-jupiter - 5.18.0 + 5.20.0 test @@ -227,7 +227,7 @@ nl.jqno.equalsverifier equalsverifier - 4.0 + 4.2.2 test @@ -236,34 +236,20 @@ 1.4.8 test + + com.exasol + maven-project-version-getter + 1.2.2 + test + + + org.itsallcode + simple-process + 0.3.1 + test + - - - java21 - - 21 - - - java.version - 21 - - - - - - -Djava.security.manager=allow -XX:+EnableDynamicAgentLoading - - - @@ -309,7 +295,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 ${java.version} ${java.version} @@ -391,6 +376,19 @@ + + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + + properties + + + + org.apache.maven.plugins maven-surefire-plugin @@ -402,7 +400,7 @@ true - ${test.args} + ${test.args} -javaagent:${org.mockito:mockito-core:jar} @@ -416,7 +414,7 @@ true true - ${test.args} + ${test.args} -javaagent:${org.mockito:mockito-core:jar} @@ -431,7 +429,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.0 + 1.7.3 oss @@ -439,7 +437,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.2 enforce-maven @@ -485,6 +483,9 @@ org.sonatype.ossindex.maven ossindex-maven-plugin 3.2.0 + + ossindex + audit @@ -498,7 +499,7 @@ org.apache.maven.plugins maven-artifact-plugin - 3.6.0 + 3.6.1 verify-reproducible-build @@ -515,7 +516,7 @@ org.codehaus.mojo versions-maven-plugin - 2.18.0 + 2.19.1 @@ -532,7 +533,12 @@ org.apache.maven.plugins maven-clean-plugin - 3.4.1 + 3.5.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 org.apache.maven.plugins @@ -547,12 +553,12 @@ org.apache.maven.plugins maven-site-plugin - 4.0.0-M16 + 3.21.0 org.apache.maven.plugins maven-shade-plugin - 3.6.0 + 3.6.1 org.apache.maven.plugins @@ -567,7 +573,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.12.0 org.codehaus.mojo @@ -638,7 +644,7 @@ net.sourceforge.plantuml plantuml - 1.2025.0 + 1.2025.10 diff --git a/product/pom.xml b/product/pom.xml index 18457c001..58214bd1f 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -62,6 +62,16 @@ openfasttrace-testutil test + + com.exasol + maven-project-version-getter + test + + + org.itsallcode + simple-process + test + openfasttrace-${revision} @@ -117,7 +127,7 @@ -Xlint:all - + -Xlint:-path -Werror diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java new file mode 100644 index 000000000..953242a63 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java @@ -0,0 +1,112 @@ +package org.itsallcode.openfasttrace.cli; + +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.*; + +import java.nio.file.Path; +import java.util.*; + +import org.itsallcode.openfasttrace.core.cli.ExitStatus; +import org.itsallcode.openfasttrace.core.cli.commands.TraceCommand; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class CliExitIT +{ + private static final String TEST_RESOURCES_MARKDOWN = "../core/src/test/resources/markdown"; + private static final String SAMPLE_DESIGN = TEST_RESOURCES_MARKDOWN + "/sample_design.md"; + private static final String SAMPLE_SYSTEM_REQUIREMENTS = TEST_RESOURCES_MARKDOWN + + "/sample_system_requirements.md"; + + @Test + void testRunWithoutArguments() + { + jarLauncher() + .args(emptyList()) + .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) + .expectStdOut(emptyString()) + .expectStdErr(equalTo("oft: Missing command\nAdd one of 'help','convert','trace'\n\n")) + .verify(); + } + + @Test + void testRunWithUnsupportedCommand() + { + jarLauncher() + .args(List.of("unsupported")) + .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) + .expectStdOut(emptyString()) + .expectStdErr(equalTo( + "oft: 'unsupported' is not an OFT command.\nChoose one of 'help','convert','trace'.\n\n")) + .verify(); + } + + @Test + void testRunWithHelpCommand() + { + jarLauncher() + .args(List.of("help")) + .expectedExitCode(ExitStatus.OK.getCode()) + .expectStdOut(startsWith(""" + OpenFastTrace + + Usage: + oft command""")) + .expectStdErr(emptyString()) + .verify(); + } + + @Test + void testRunWithUnsupportedReporter(@TempDir final Path emptyDir) + { + jarLauncher() + .args(List.of("trace", "-o", "unknown", emptyDir.toString())) + .expectedExitCode(ExitStatus.FAILURE.getCode()) + .expectStdOut(emptyString()) + .expectStdErr(startsWith( + "Exception in thread \"main\" org.itsallcode.openfasttrace.api.exporter.ExporterException: Found no matching reporter for output format 'unknown'")) + .verify(); + } + + @Test + void testCliExitCode_Ok() + { + assertExitStatusForTracedFiles(ExitStatus.OK, SAMPLE_SYSTEM_REQUIREMENTS, SAMPLE_DESIGN); + } + + private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, final String... files) + { + final List args = new ArrayList<>(); + args.add(TraceCommand.COMMAND_NAME); + args.addAll(Arrays.asList(files)); + jarLauncher() + .args(args) + .expectedExitCode(expectedStatus.getCode()) + .expectStdErr(emptyString()) + .expectStdOut(not(emptyString())) + .verify(); + } + + private JarLauncher.Builder jarLauncher() + { + return JarLauncher.builder() + .currentWorkingDir(); + } + + @Test + void testCliExitCode_Failure() + { + assertExitStatusForTracedFiles(ExitStatus.FAILURE, SAMPLE_SYSTEM_REQUIREMENTS); + } + + @Test + void testCliExitCode_CliError() + { + jarLauncher() + .args(List.of("--zzzz")) + .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) + .expectStdOut(emptyString()) + .expectStdErr(equalTo("oft: Unexpected parameter '--zzzz' is not allowed\n")) + .verify(); + } +} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java new file mode 100644 index 000000000..b2f7f84f9 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -0,0 +1,377 @@ +package org.itsallcode.openfasttrace.cli; + +import static java.util.stream.Collectors.joining; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.*; +import java.time.Duration; +import java.util.List; + +import org.itsallcode.junit.sysextensions.SystemErrGuard; +import org.itsallcode.junit.sysextensions.SystemOutGuard; +import org.itsallcode.openfasttrace.cli.JarLauncher.Builder; +import org.itsallcode.openfasttrace.core.cli.ExitStatus; +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.opentest4j.MultipleFailuresError; + +@ExtendWith(SystemOutGuard.class) +@ExtendWith(SystemErrGuard.class) +// [itest->dsn~cli.tracing.exit-status~1] +class CliStarterIT +{ + private static final String SPECOBJECT_PREAMBLE = "\n"; + private static final String ILLEGAL_COMMAND = "illegal"; + private static final String NEWLINE_PARAMETER = "--newline"; + private static final String HELP_COMMAND = "help"; + private static final String CONVERT_COMMAND = "convert"; + private static final String TRACE_COMMAND = "trace"; + private static final String OUTPUT_FILE_PARAMETER = "--output-file"; + private static final String REPORT_VERBOSITY_PARAMETER = "--report-verbosity"; + private static final String OUTPUT_FORMAT_PARAMETER = "--output-format"; + private static final String WANTED_ARTIFACT_TYPES_PARAMETER = "--wanted-artifact-types"; + private static final String COLOR_SCHEME_PARAMETER = "--color-scheme"; + private static final String CARRIAGE_RETURN = "\r"; + private static final String NEWLINE = "\n"; + + private static final Path DOC_DIR = Paths.get("../core/src/test/resources/markdown").toAbsolutePath(); + + private Path outputFile; + + @BeforeEach + void beforeEach(@TempDir final Path tempDir) + { + this.outputFile = tempDir.resolve("stream.txt"); + } + + @Test + void testNoArguments() + { + assertExitWithError(jarLauncher(), ExitStatus.CLI_ERROR, "oft: Missing command"); + } + + private void assertExitWithError(final JarLauncher.Builder jarLauncherBuilder, final ExitStatus status, + final String message) throws MultipleFailuresError + { + jarLauncherBuilder + .expectStdErr(startsWith(message)) + .expectedExitCode(status.getCode()) + .verify(); + } + + // [itest->dsn~cli.command-selection~1] + @Test + void testIllegalCommand() + { + assertExitWithError(jarLauncher(ILLEGAL_COMMAND), ExitStatus.CLI_ERROR, + "oft: '" + ILLEGAL_COMMAND + "' is not an OFT command."); + } + + @Test + void testHelpPrintsUsage() + { + final String nl = "\n"; + assertExitOkWithStdOutStart(jarLauncher(HELP_COMMAND), "OpenFastTrace" + nl + nl + "Usage:"); + } + + // [itest->dsn~cli.command-selection~1] + @Test + void testConvertWithoutExplicitInputs() + { + assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND), SPECOBJECT_PREAMBLE); + } + + private void assertExitOkWithStdOutStart(final JarLauncher.Builder jarLauncherBuilder, final String outputStart) + throws MultipleFailuresError + { + jarLauncherBuilder.expectStdOut(startsWith(outputStart)) + .expectedExitCode(0) + .verify(); + assertOutputFileExists(false); + } + + @Test + void testConvertUnknownExporter() + { + final Builder jarLauncherBuilder = jarLauncher( + CONVERT_COMMAND, DOC_DIR.toString(), + OUTPUT_FORMAT_PARAMETER, "illegal", + OUTPUT_FILE_PARAMETER, this.outputFile.toString()); + assertExitWithError(jarLauncherBuilder, ExitStatus.CLI_ERROR, + "oft: export format 'illegal' is not supported."); + } + + // [itest->dsn~cli.conversion.output-format~1] + @Test + void testConvertToSpecobjectFile() + { + final Builder jarLauncherBuilder = jarLauncher( // + CONVERT_COMMAND, DOC_DIR.toString(), // + OUTPUT_FORMAT_PARAMETER, "specobject", // + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // + COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE"); + assertExitOkWithOutputFileStart(jarLauncherBuilder, + SPECOBJECT_PREAMBLE + "\n assertOutputFileExists(true), + () -> assertOutputFileContentStartsWith(fileStart)); + } + + // [itest->dsn~cli.conversion.default-output-format~1] + @Test + void testConvertDefaultOutputFormat() + { + assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString()), SPECOBJECT_PREAMBLE); + } + + // [itest->dsn~cli.input-file-selection~1] + @Test + void testConvertDefaultOutputFormatIntoFile() + { + assertExitOkWithOutputFileStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString(), + OUTPUT_FILE_PARAMETER, this.outputFile.toString()), SPECOBJECT_PREAMBLE); + } + + // [itest->dsn~cli.default-input~1] + @Test + void testConvertDefaultInputDir() + { + assertExitOkWithOutputFileOfLength(jarLauncher( + CONVERT_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString()), 2000); + } + + @Test + void testTraceNoArguments() + { + jarLauncher(TRACE_COMMAND) + .currentWorkingDir() + .expectedExitCode(1) + .expectStdOut(containsString("not ok - 43 total, 43 defect")) + .verify(); + } + + // [itest->dsn~cli.command-selection~1] + @Test + void testTrace() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + DOC_DIR.toString()), "ok - 5 total"); + } + + @Test + void testTraceWithReportVerbosityMinimal() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, DOC_DIR.toString(), + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + REPORT_VERBOSITY_PARAMETER, "MINIMAL"), "ok"); + } + + @Test + void testTraceWithReportVerbosityQuietToStdOut() + { + jarLauncher( + TRACE_COMMAND, DOC_DIR.toString(), + REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString()) + .expectedExitCode(ExitStatus.OK.getCode()) + .verify(); + assertOutputFileExists(false); + } + + @Test + void testTraceWithReportVerbosityQuietToFileMustBeRejected() + { + jarLauncher( + TRACE_COMMAND, DOC_DIR.toString(), + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + REPORT_VERBOSITY_PARAMETER, "QUIET").expectedExitCode(ExitStatus.CLI_ERROR.getCode()) + .expectStdErr(equalTo("oft: combining stream")); + } + + @Test + // [itest->dsn~cli.default-input~1] + void testTraceDefaultInputDir() + { + jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()) + .expectStdOut(emptyString()) + .expectedExitCode(1) + .timeout(Duration.ofSeconds(10)) + .verify(); + assertOutputFileExists(true); + } + + @Test + void testBasicHtmlTrace() + { + assertExitOkWithStdOutStart(jarLauncher( + TRACE_COMMAND, DOC_DIR.toString(), + OUTPUT_FORMAT_PARAMETER, "html"), ""); + } + + private void assertExitOkWithOutputFileOfLength(final JarLauncher.Builder jarLauncherBuilder, final int length) + { + assertAll( + () -> assertExitOkWithOutputFileStart(jarLauncherBuilder, SPECOBJECT_PREAMBLE), + () -> assertOutputFileLength(length)); + } + + private void assertOutputFileLength(final int length) + { + assertThat(getOutputFileContent().length(), greaterThan(length)); + } + + // [itest->dsn~cli.tracing.output-format~1]] + void testTraceOutputFormatPlain() + { + assertExitOkWithOutputFileOfLength(jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, + this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"), 1000); + } + + @Test + void testTraceMacNewlines() + { + assertAll( // + () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + NEWLINE_PARAMETER, "OLDMAC", + DOC_DIR.toString()), + () -> assertOutputFileExists(true), + this::assertOutputFileContainsOldMacNewlines, + this::assertOutputFileContainsNoUnixNewlines); + } + + private void assertOutputFileContainsOldMacNewlines() + { + assertThat("Has old Mac newlines", getOutputFileContent().contains(CARRIAGE_RETURN), + equalTo(true)); + } + + private void assertOutputFileContainsNoUnixNewlines() + { + assertThat("Has no Unix newlines", getOutputFileContent().contains(NEWLINE), + equalTo(false)); + } + + @Test + // [itest->dsn~cli.default-newline-format~1] + void testTraceDefaultNewlines() + { + assertAll( + () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + DOC_DIR.toString()), + () -> assertOutputFileExists(true), + this::assertPlatformNewlines, + this::assertNoOffendingNewlines); + } + + private void assertExitWithStatus(final int code, final String... args) + { + jarLauncher() + .args(List.of(args)) + .expectedExitCode(code) + .verify(); + } + + private void assertPlatformNewlines() + { + assertThat("Has native platform line separator", + getOutputFileContent().contains(System.lineSeparator()), equalTo(true)); + } + + private void assertNoOffendingNewlines() + { + final String systemLineSeparator = System.lineSeparator(); + switch (systemLineSeparator) + { + case NEWLINE: + assertThat("Has no carriage returns", getOutputFileContent().contains(CARRIAGE_RETURN), + equalTo(false)); + break; + case CARRIAGE_RETURN: + assertThat("Has no newlines", getOutputFileContent().contains(NEWLINE), equalTo(false)); + break; + case NEWLINE + CARRIAGE_RETURN: + assertThat("Has no newline without carriage return and vice-versa", + getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false)); + break; + case CARRIAGE_RETURN + NEWLINE: + assertThat("Has no carriage return without newline and vice-versa", + getOutputFileContent().matches("\r[^\n]|[^\r]\n"), equalTo(false)); + break; + default: + final String hexCode = systemLineSeparator.chars() + .mapToObj(c -> String.format("\\u%04x", c)) + .collect(joining()); + fail("Unsupported line separator '%s' (%s)".formatted(systemLineSeparator, hexCode)); + } + } + + @Test + void testTraceWithFilteredArtifactType() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, DOC_DIR.toString(), + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req"), "ok - 3 total"); + } + + private void assertOutputFileContentStartsWith(final String content) + { + assertThat(getOutputFileContent(), startsWith(content)); + } + + private void assertOutputFileExists(final boolean fileExists) + { + assertThat("Output file %s exists".formatted(this.outputFile), Files.exists(this.outputFile), + equalTo(fileExists)); + } + + private String getOutputFileContent() + { + final Path file = this.outputFile; + if (!Files.exists(file)) + { + throw new AssertionError("Expected output file %s does not exist".formatted(file)); + } + try + { + return Files.readString(file); + } + catch (final IOException exception) + { + // Need to convert this to an unchecked exception. Otherwise, we get + // stuck with the checked exceptions in the assertion lambdas. + throw new UncheckedIOException("Failed to read file %s".formatted(file), exception); + } + } + + private Builder jarLauncher(final String... arguments) + { + return jarLauncher().workingDir(Path.of("..").toAbsolutePath()).args(List.of(arguments)); + } + + private JarLauncher.Builder jarLauncher() + { + return JarLauncher.builder() + .currentWorkingDir(); + } +} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java index 130b73992..653fffdd5 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java @@ -1,26 +1,21 @@ package org.itsallcode.openfasttrace.cli; import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.core.StringContains.containsString; -import static org.itsallcode.junit.sysextensions.AssertExit.assertExitWithStatus; import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.List; -import org.itsallcode.io.Capturable; -import org.itsallcode.junit.sysextensions.ExitGuard; import org.itsallcode.junit.sysextensions.SystemOutGuard; -import org.itsallcode.junit.sysextensions.SystemOutGuard.SysOut; import org.itsallcode.openfasttrace.testutil.AbstractFileBasedTest; -import org.itsallcode.openfasttrace.testutil.TestAssumptions; -import org.junit.jupiter.api.*; +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; -@SuppressWarnings("removal") // https://github.com/itsallcode/openfasttrace/issues/436 -@ExtendWith(ExitGuard.class) @ExtendWith(SystemOutGuard.class) class ITestCliWithFilter extends AbstractFileBasedTest { @@ -37,62 +32,44 @@ class ITestCliWithFilter extends AbstractFileBasedTest private File specFile; - @BeforeAll - static void assumeSecurityManagerSupported() - { - TestAssumptions.assumeSecurityManagerSupported(); - } - @BeforeEach - void beforeEach(@TempDir final Path tempDir, @SysOut final Capturable out) throws IOException + void beforeEach(@TempDir final Path tempDir) throws IOException { this.specFile = tempDir.resolve("spec.md").toFile(); writeTextFile(this.specFile, SPECIFICATION); - out.capture(); } // [itest->dsn~filtering-by-tags-during-import~1] @Test - void testWithoutFilter(@SysOut final Capturable out) + void testWithoutFilter() { - assertExitWithStatus(0, () -> runWithArguments("convert", this.specFile.toString())); - final String stdOut = out.getCapturedData(); - assertThat(stdOut, containsString("a<")); - assertThat(stdOut, containsString("b<")); - assertThat(stdOut, containsString("c<")); + assertStdOut(List.of("convert", this.specFile.toString()), + allOf(containsString("a<"), + containsString("b<"), + containsString("c<"))); } // [itest->dsn~filtering-by-tags-during-import~1] @Test - void testFilterWithAtLeastOneMatchingTag(@SysOut final Capturable out) + void testFilterWithAtLeastOneMatchingTag() { - assertExitWithStatus(0, - () -> runWithArguments("convert", "-t", "tag1", this.specFile.toString())); - final String stdOut = out.getCapturedData(); - assertThat(stdOut, not(containsString("a<"))); - assertThat(stdOut, containsString("b<")); - assertThat(stdOut, not(containsString("c<"))); + assertStdOut(List.of("convert", "-t", "tag1", this.specFile.toString()), + allOf(not(containsString("a<")), containsString("b<"), not(containsString("c<")))); } // [itest->dsn~filtering-by-tags-during-import~1] @Test - void testFilterWithEmptyTagListFiltersOutEverything(final Capturable stream) + void testFilterWithEmptyTagListFiltersOutEverything() { - assertExitWithStatus(0, - () -> runWithArguments("convert", "-t", "", this.specFile.toString())); - final String stdOut = stream.getCapturedData(); - assertThat(stdOut, not(containsString(""))); + assertStdOut(List.of("convert", "-t", "", this.specFile.toString()), not(containsString(""))); } // [itest->dsn~filtering-by-tags-or-no-tags-during-import~1] @Test - void testFilterWithAtLeastOneMatchingTagOrNoTags(final Capturable stream) + void testFilterWithAtLeastOneMatchingTagOrNoTags() { - assertExitWithStatus(0, - () -> runWithArguments("convert", "-t", "_,tag1", this.specFile.toString())); - final String stdOut = stream.getCapturedData(); - assertThat(stdOut, containsString("a<")); - assertThat(stdOut, containsString("b<")); - assertThat(stdOut, not(containsString("c<"))); + assertStdOut(List.of( + "convert", "-t", "_,tag1", this.specFile.toString()), + allOf(containsString("a<"), containsString("b<"), not(containsString("c<")))); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java new file mode 100644 index 000000000..bf7b2cb14 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -0,0 +1,242 @@ +package org.itsallcode.openfasttrace.cli; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.hamcrest.Matcher; +import org.itsallcode.process.SimpleProcess; +import org.itsallcode.process.SimpleProcessBuilder; + +import com.exasol.mavenprojectversiongetter.MavenProjectVersionGetter; + +/** + * This simplifies launching the OFT executable JAR file for integration tests. + */ +public final class JarLauncher +{ + private static final Logger LOG = Logger.getLogger(JarLauncher.class.getName()); + private final SimpleProcess process; + private final Builder builder; + + private JarLauncher(final SimpleProcess process, final Builder builder) + { + this.process = process; + this.builder = builder; + } + + private static JarLauncher start(final Builder builder) + { + final Path jarPath = getExecutableJarPath(); + if (!Files.exists(jarPath)) + { + throw new IllegalStateException( + "Executable JAR not found at %s. Run 'mvn -T1C package -DskipTests' to build it." + .formatted(jarPath)); + } + final List command = new ArrayList<>(); + command.addAll(createJavaLaunchArgs(jarPath)); + if (builder.args != null) + { + command.addAll(builder.args); + } + + LOG.info("Starting command %s in working dir %s...".formatted(command, builder.workingDir)); + final SimpleProcess process = SimpleProcessBuilder.create() + .command(command) + .workingDir(builder.workingDir) + .redirectErrorStream(false) + .streamLogLevel(Level.FINE) + .start(); + return new JarLauncher(process, builder); + } + + private static List createJavaLaunchArgs(final Path jarPath) + { + final String javaExecutable = getJavaExecutable().toString(); + return List.of(javaExecutable, "-jar", jarPath.toString()); + } + + private static Path getExecutableJarPath() + { + final String jarFileName = "openfasttrace-%s.jar".formatted(getCurrentProjectVersion()); + return Path.of("target").resolve(jarFileName).toAbsolutePath(); + } + + private static String getCurrentProjectVersion() + { + return MavenProjectVersionGetter.getProjectRevision(Path.of("../parent/pom.xml").toAbsolutePath()); + } + + private static Path getJavaExecutable() + { + return ProcessHandle.current().info().command() + .map(Path::of) + .orElseThrow(() -> new IllegalStateException("Java executable not found")); + } + + private void assertExpectationsAfterTerminated(final Duration timeout) + { + LOG.fine("Waiting %s for process %d to terminate...".formatted(timeout, process.pid())); + process.waitForTermination(timeout); + final int exitValue = process.exitValue(); + LOG.fine("Process %d terminated with exit code %d".formatted(process.pid(), exitValue)); + assertAll( + () -> assertThat( + "exit code (std out: %s, std err: %s)".formatted(process.getStdOut(), process.getStdErr()), + exitValue, equalTo(builder.expectedExitCode)), + () -> { + if (builder.expectedStdOut != null) + { + assertThat("std out", process.getStdOut(), builder.expectedStdOut); + } + }, () -> { + if (builder.expectedStdErr != null) + { + assertThat("std err", process.getStdErr(), builder.expectedStdErr); + } + }); + } + + /** + * Create a new {@link Builder} for launching the OFT JAR. + * + * @return builder for launching the OFT JAR + */ + public static Builder builder() + { + return new Builder(); + } + + /** + * Builder for launching the OFT JAR. + */ + public static final class Builder + { + private Path workingDir; + private List args; + private int expectedExitCode = 0; + private Matcher expectedStdOut; + private Matcher expectedStdErr; + private Duration timeout = Duration.ofSeconds(3); + + private Builder() + { + } + + /** + * Set the working directory of the new process to the current working + * directory. + * + * @return {@code this} for method chaining + */ + public Builder currentWorkingDir() + { + return this.workingDir(Path.of(System.getProperty("user.dir"))); + } + + /** + * Set the working directory of the new process. + * + * @param workingDir + * the working directory + * @return {@code this} for method chaining + */ + public Builder workingDir(final Path workingDir) + { + this.workingDir = workingDir; + return this; + } + + /** + * Set the arguments for the new process. + * + * @param args + * the arguments + * @return {@code this} for method chaining + */ + public Builder args(final List args) + { + this.args = args; + return this; + } + + /** + * Set the timeout for waiting for process termination. + * + * @param timeout + * the timeout + * @return {@code this} for method chaining + */ + public Builder timeout(final Duration timeout) + { + this.timeout = timeout; + return this; + } + + /** + * Expect a successful exit code (0). + * + * @return {@code this} for method chaining + */ + public Builder successExitCode() + { + return this.expectedExitCode(0); + } + + /** + * Set the expected exit code of the new process. + * + * @param expectedExitCode + * the expected exit code + * @return {@code this} for method chaining + */ + public Builder expectedExitCode(final int expectedExitCode) + { + this.expectedExitCode = expectedExitCode; + return this; + } + + /** + * Set the matcher for the expected standard output. + * + * @param expectedStdOut + * the matcher for standard output + * @return {@code this} for method chaining + */ + public Builder expectStdOut(final Matcher expectedStdOut) + { + this.expectedStdOut = expectedStdOut; + return this; + } + + /** + * Set the matcher for the expected standard error. + * + * @param expectedStdErr + * the matcher for standard error + * @return {@code this} for method chaining + */ + public Builder expectStdErr(final Matcher expectedStdErr) + { + this.expectedStdErr = expectedStdErr; + return this; + } + + /** + * Launch the JAR and verify the expectations. + */ + public void verify() + { + JarLauncher.start(this).assertExpectationsAfterTerminated(this.timeout); + } + } +} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java deleted file mode 100644 index 9cd7badc2..000000000 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.itsallcode.openfasttrace.cli; - -import static org.itsallcode.junit.sysextensions.AssertExit.assertExitWithStatus; - -import java.util.*; - -import org.itsallcode.junit.sysextensions.ExitGuard; -import org.itsallcode.openfasttrace.core.cli.*; -import org.itsallcode.openfasttrace.core.cli.commands.TraceCommand; -import org.itsallcode.openfasttrace.testutil.TestAssumptions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@SuppressWarnings("removal") // https://github.com/itsallcode/openfasttrace/issues/436 -@ExtendWith(ExitGuard.class) -class TestCliExit -{ - private static final String TEST_RESOURCES_MARKDOWN = "../core/src/test/resources/markdown"; - private static final String SAMPLE_DESIGN = TEST_RESOURCES_MARKDOWN + "/sample_design.md"; - private static final String SAMPLE_SYSTEM_REQUIREMENTS = TEST_RESOURCES_MARKDOWN - + "/sample_system_requirements.md"; - - @BeforeAll - static void assumeSecurityManagerSupported() - { - TestAssumptions.assumeSecurityManagerSupported(); - } - - @Test - void testCliExitCode_Ok() - { - assertExitStatusForTracedFiles(ExitStatus.OK, SAMPLE_SYSTEM_REQUIREMENTS, SAMPLE_DESIGN); - } - - private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, - final String... files) - { - assertExitStatusForCommandWithFiles(expectedStatus, TraceCommand.COMMAND_NAME, files); - } - - private void assertExitStatusForCommandWithFiles(final ExitStatus expectedStatus, - final String command, final String... files) - { - final CliArguments arguments = new CliArguments(new StandardDirectoryService()); - final List values = new ArrayList<>(); - values.add(command); - values.addAll(Arrays.asList(files)); - arguments.setUnnamedValues(values); - assertExitWithStatus(expectedStatus.getCode(), () -> new CliStarter(arguments).run()); - } - - @Test - void testCliExitCode_Failure() - { - assertExitStatusForTracedFiles(ExitStatus.FAILURE, SAMPLE_SYSTEM_REQUIREMENTS); - } - - @Test - void testCliExitCode_CliError() - { - final String[] arguments = { "--zzzzz" }; - assertExitWithStatus(ExitStatus.CLI_ERROR.getCode(), () -> CliStarter.main(arguments)); - } -} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java deleted file mode 100644 index f238bed13..000000000 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java +++ /dev/null @@ -1,403 +0,0 @@ -package org.itsallcode.openfasttrace.cli; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.itsallcode.junit.sysextensions.AssertExit.assertExitWithStatus; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.io.IOException; -import java.nio.file.*; - -import org.itsallcode.io.Capturable; -import org.itsallcode.junit.sysextensions.*; -import org.itsallcode.junit.sysextensions.SystemErrGuard.SysErr; -import org.itsallcode.junit.sysextensions.SystemOutGuard.SysOut; -import org.itsallcode.junit.sysextensions.security.ExitTrapException; -import org.itsallcode.openfasttrace.core.cli.CliStarter; -import org.itsallcode.openfasttrace.core.cli.ExitStatus; -import org.itsallcode.openfasttrace.testutil.TestAssumptions; -import org.itsallcode.openfasttrace.testutil.cli.FakeDirectoryService; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; -import org.opentest4j.MultipleFailuresError; - -@SuppressWarnings("removal") // https://github.com/itsallcode/openfasttrace/issues/436 -@ExtendWith(ExitGuard.class) -@ExtendWith(SystemOutGuard.class) -@ExtendWith(SystemErrGuard.class) -// [itest->dsn~cli.tracing.exit-status~1] -class TestCliStarter -{ - private static final String SPECOBJECT_PREAMBLE = "\n"; - private static final String ILLEGAL_COMMAND = "illegal"; - private static final String NEWLINE_PARAMETER = "--newline"; - private static final String HELP_COMMAND = "help"; - private static final String CONVERT_COMMAND = "convert"; - private static final String TRACE_COMMAND = "trace"; - private static final String OUTPUT_FILE_PARAMETER = "--output-file"; - private static final String REPORT_VERBOSITY_PARAMETER = "--report-verbosity"; - private static final String OUTPUT_FORMAT_PARAMETER = "--output-format"; - private static final String WANTED_ARTIFACT_TYPES_PARAMETER = "--wanted-artifact-types"; - private static final String COLOR_SCHEME_PARAMETER = "--color-scheme"; - private static final String CARRIAGE_RETURN = "\r"; - private static final String NEWLINE = "\n"; - - private final Path DOC_DIR = Paths.get("../core/src/test/resources/markdown").toAbsolutePath(); - - private Path outputFile; - - @BeforeAll - static void assumeSecurityManagerSupported() - { - TestAssumptions.assumeSecurityManagerSupported(); - } - - @BeforeEach - void beforeEach(@TempDir final Path tempDir) - { - this.outputFile = tempDir.resolve("stream.txt"); - } - - @Test - void testNoArguments(@SysErr final Capturable err) - { - assertExitWithError(this::runCliStarter, ExitStatus.CLI_ERROR, "oft: Missing command", - err); - } - - private void assertExitWithError(final Runnable runnable, final ExitStatus status, - final String message, final Capturable stream) throws MultipleFailuresError - { - stream.captureMuted(); - assertAll( // - () -> assertExitWithStatus(status.getCode(), runnable), - () -> assertThat(stream.getCapturedData(), startsWith(message)) // - ); - } - - // [itest->dsn~cli.command-selection~1] - @Test - void testIllegalCommand(@SysErr final Capturable err) - { - assertExitWithError(() -> runCliStarter(ILLEGAL_COMMAND), ExitStatus.CLI_ERROR, - "oft: '" + ILLEGAL_COMMAND + "' is not an OFT command.", err); - } - - @Test - void testHelpPrintsUsage(@SysOut final Capturable out) - { - final String nl = System.lineSeparator(); - assertExitOkWithStdOutStart(() -> runCliStarter(HELP_COMMAND), "OpenFastTrace" + nl + nl + "Usage:", out); - } - - // [itest->dsn~cli.command-selection~1] - @Test - void testConvertWithoutExplicitInputs(@SysOut final Capturable out) - { - assertExitOkWithStdOutStart(() -> runCliStarter(CONVERT_COMMAND), SPECOBJECT_PREAMBLE, out); - } - - private void assertExitOkWithStdOutStart(final Runnable runnable, final String outputStart, - final Capturable out) throws MultipleFailuresError - { - out.captureMuted(); - assertAll(() -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), () -> assertOutputFileExists(false), // - () -> assertThat(out.getCapturedData(), startsWith(outputStart))); - } - - @Test - void testConvertUnknownExporter(@SysErr final Capturable err) - { - final Runnable runnable = () -> runCliStarter( // - CONVERT_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FORMAT_PARAMETER, "illegal", // - OUTPUT_FILE_PARAMETER, this.outputFile.toString()); - assertExitWithError(runnable, ExitStatus.CLI_ERROR, - "oft: export format 'illegal' is not supported.", err); - } - - // [itest->dsn~cli.conversion.output-format~1] - @Test - void testConvertToSpecobjectFile() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - CONVERT_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FORMAT_PARAMETER, "specobject", // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE"); - assertExitOkWithOutputFileStart(runnable, - SPECOBJECT_PREAMBLE + "\n assertExitWithStatus(ExitStatus.OK.getCode(), runnable), // - () -> assertOutputFileExists(true), // - () -> assertOutputFileContentStartsWith(fileStart) // - ); - } - - // [itest->dsn~cli.conversion.default-output-format~1] - @Test - void testConvertDefaultOutputFormat(@SysOut final Capturable out) throws IOException - { - final Runnable runnable = () -> runCliStarter(CONVERT_COMMAND, this.DOC_DIR.toString()); - assertExitOkWithStdOutStart(runnable, SPECOBJECT_PREAMBLE, out); - } - - // [itest->dsn~cli.input-file-selection~1] - @Test - void testConvertDefaultOutputFormatIntoFile() throws IOException - { - final Runnable runnable = () -> runCliStarter(CONVERT_COMMAND, this.DOC_DIR.toString(), - OUTPUT_FILE_PARAMETER, this.outputFile.toString()); - assertExitOkWithOutputFileStart(runnable, SPECOBJECT_PREAMBLE); - } - - // [itest->dsn~cli.default-input~1] - @Test - void testConvertDefaultInputDir() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - CONVERT_COMMAND, // - OUTPUT_FILE_PARAMETER, this.outputFile.toString() // - ); - assertExitOkWithOutputFileOfLength(runnable, 2000); - } - - @Test - void testTraceNoArguments(@SysErr final Capturable err) - { - // This test is fragile, since we can't influence the current working - // directory which is automatically used if no directory is specified. - // All we know is that no CLI error should be returned. - try - { - runCliStarter(TRACE_COMMAND); - } - catch (final ExitTrapException e) - { - assertThat(e.getExitStatus(), - anyOf(equalTo(ExitStatus.OK.getCode()), equalTo(ExitStatus.FAILURE.getCode()))); - assertThat(err.getCapturedData(), is(emptyOrNullString())); - } - } - - // [itest->dsn~cli.command-selection~1] - @Test - void testTrace() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - this.DOC_DIR.toString() // - ); - assertExitOkWithOutputFileStart(runnable, "ok - 5 total"); - } - - @Test - void testTraceWithReportVerbosityMinimal() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - REPORT_VERBOSITY_PARAMETER, "MINIMAL" // - ); - assertExitOkWithOutputFileStart(runnable, "ok"); - } - - @Test - void testTraceWithReportVerbosityQuietToStdOut(@SysOut final Capturable out) throws IOException - { - final Runnable runnable = () -> runCliStarter(// - TRACE_COMMAND, this.DOC_DIR.toString(), // - REPORT_VERBOSITY_PARAMETER, "QUIET" // - ); - out.captureMuted(); - assertAll( // - () -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), // - () -> assertOutputFileExists(false), - () -> assertThat(out.getCapturedData(), is(emptyOrNullString())) // - ); - } - - @Test - void testTraceWithReportVerbosityQuietToFileMustBeRejected(@SysErr final Capturable err) - throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - REPORT_VERBOSITY_PARAMETER, "QUIET" // - ); - assertExitWithError(runnable, ExitStatus.CLI_ERROR, "oft: combining stream", err); - } - - @Test - // [itest->dsn~cli.default-input~1] - void testTraceDefaultInputDir(@SysErr final Capturable err) throws IOException - { - // This test is fragile, since we can't influence the current working - // directory which is automatically used if no directory is specified. - // All we know is that no CLI error should be returned and an output - // file must exist. - try - { - runCliStarter(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()); - } - catch (final ExitTrapException e) - { - assertAll( // - () -> assertThat(e.getExitStatus(), - anyOf(equalTo(ExitStatus.OK.getCode()), - equalTo(ExitStatus.FAILURE.getCode()))), - () -> assertThat(err.getCapturedData(), is(emptyOrNullString())), - () -> assertOutputFileExists(true)); - } - } - - @Test - void testBasicHtmlTrace(@SysOut final Capturable out) - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FORMAT_PARAMETER, "html"); - assertExitOkWithStdOutStart(runnable, "", out); - } - - private void assertExitOkWithOutputFileOfLength(final Runnable runnable, final int length) - throws MultipleFailuresError - { - assertAll( // - () -> assertExitOkWithOutputFileStart(runnable, SPECOBJECT_PREAMBLE), // - () -> assertOutputFileLength(length) // - ); - } - - private void assertOutputFileLength(final int length) - { - assertThat(getOutputFileContent().length(), greaterThan(length)); - } - - // [itest->dsn~cli.tracing.output-format~1]] - void testTraceOutputFormatPlain() throws IOException - { - final Runnable runnable = () -> runCliStarter(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, - this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"); - assertExitOkWithOutputFileOfLength(runnable, 1000); - } - - @Test - void testTraceMacNewlines() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - NEWLINE_PARAMETER, "OLDMAC", // - this.DOC_DIR.toString() // - ); - assertAll( // - () -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), // - () -> assertOutputFileExists(true), // - this::assertOutputFileContainsOldMacNewlines, // - this::assertOutputFileContainsNoUnixNewlines // - ); - } - - private void assertOutputFileContainsOldMacNewlines() - { - assertThat("Has old Mac newlines", getOutputFileContent().contains(CARRIAGE_RETURN), - equalTo(true)); - } - - private void assertOutputFileContainsNoUnixNewlines() - { - assertThat("Has no Unix newlines", getOutputFileContent().contains(NEWLINE), - equalTo(false)); - } - - @Test - // [itest->dsn~cli.default-newline-format~1] - void testTraceDefaultNewlines() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - this.DOC_DIR.toString() // - ); - assertAll( // - () -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), // - () -> assertOutputFileExists(true), // - this::assertPlatformNewlines, // - this::assertNoOffendingNewlines // - ); - } - - private void assertPlatformNewlines() - { - assertThat("Has native platform line separator", - getOutputFileContent().contains(System.lineSeparator()), equalTo(true)); - } - - private void assertNoOffendingNewlines() - { - switch (System.lineSeparator()) - { - case NEWLINE: - assertThat("Has no carriage returns", getOutputFileContent().contains(CARRIAGE_RETURN), - equalTo(false)); - break; - case CARRIAGE_RETURN: - assertThat("Has no newlines", getOutputFileContent().contains(NEWLINE), equalTo(false)); - break; - case NEWLINE + CARRIAGE_RETURN: - assertThat("Has no newline without carriage return and vice-versa", - getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false)); - break; - } - } - - @Test - void testTraceWithFilteredArtifactType() throws IOException - { - final Runnable runnable = () -> runCliStarter( // - TRACE_COMMAND, this.DOC_DIR.toString(), // - OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req" // - ); - assertExitOkWithOutputFileStart(runnable, "ok - 3 total"); - } - - private void assertOutputFileContentStartsWith(final String content) - { - assertThat(getOutputFileContent(), startsWith(content)); - } - - private void assertOutputFileExists(final boolean fileExists) - { - assertThat("Output file exists", Files.exists(this.outputFile), equalTo(fileExists)); - } - - private String getOutputFileContent() - { - final Path file = this.outputFile; - try - { - return Files.readString(file); - } - catch (final IOException exception) - { - // Need to convert this to an unchecked exception. Otherwise, we get - // stuck with the checked exceptions in the assertion lambdas. - throw new RuntimeException(exception); - } - } - - private void runCliStarter(final String... arguments) - { - CliStarter.main(arguments, new FakeDirectoryService(this.DOC_DIR.toString())); - } -} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java b/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java index 7ce4c9022..58baee52d 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java @@ -1,11 +1,11 @@ package org.itsallcode.openfasttrace.testutil; -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; +import java.io.*; import java.nio.charset.StandardCharsets; +import java.util.List; -import org.itsallcode.openfasttrace.core.cli.CliStarter; +import org.hamcrest.Matcher; +import org.itsallcode.openfasttrace.cli.JarLauncher; /** * This class is the base class for integration tests that require input files. @@ -23,8 +23,13 @@ protected void writeTextFile(final File file, final String content) throws IOExc } @SuppressWarnings("javadoc") - protected void runWithArguments(final String... args) + protected void assertStdOut(final List args, final Matcher stdOutMatcher) { - CliStarter.main(args); + JarLauncher.builder() + .args(args) + .currentWorkingDir() + .expectStdOut(stdOutMatcher) + .expectedExitCode(0) + .verify(); } -} \ No newline at end of file +} diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/TestAssumptions.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/TestAssumptions.java deleted file mode 100644 index b6ca95cf2..000000000 --- a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/TestAssumptions.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.itsallcode.openfasttrace.testutil; - -import java.lang.Runtime.Version; - -import org.junit.jupiter.api.Assumptions; -import org.opentest4j.TestAbortedException; - -/** - * Assumptions for unit and integration tests. - */ -public class TestAssumptions -{ - private TestAssumptions() - { - // Not instantiable - } - - /** - * This ensures that the current JDK supports using Java's security manager. - * Starting with Java 19, the security manager is not supported anymore. - * - * @throws TestAbortedException - * if the JVM does not support Java's security manager. - */ - public static void assumeSecurityManagerSupported() throws TestAbortedException - { - final Version version = Runtime.version(); - Assumptions.assumeTrue(version.feature() <= 18); - } -}