From 7f7546f29e86037942f4df49b2245e722b269da9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 19 Jan 2025 17:17:12 +0100 Subject: [PATCH 01/25] #345: Add Jar Launcher --- .../openfasttrace/core/cli/CliStarter.java | 2 +- parent/pom.xml | 8 +- product/pom.xml | 7 +- .../openfasttrace/cli/JarLauncher.java | 106 +++++++++++++ .../cli/ProcessOutputConsumer.java | 147 ++++++++++++++++++ .../openfasttrace/cli/TestCliExit.java | 7 + 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java create mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java 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 843897b1..fea9ccde 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. diff --git a/parent/pom.xml b/parent/pom.xml index d9c8a184..af333572 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -246,6 +246,12 @@ 1.4.8 test + + com.exasol + maven-project-version-getter + 1.2.1 + test + @@ -702,4 +708,4 @@ - \ No newline at end of file + diff --git a/product/pom.xml b/product/pom.xml index ced14315..ad988371 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -62,6 +62,11 @@ openfasttrace-testutil test + + com.exasol + maven-project-version-getter + test + openfasttrace-${revision} @@ -125,4 +130,4 @@ - \ No newline at end of file + 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 00000000..b39a5f18 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -0,0 +1,106 @@ +package org.itsallcode.openfasttrace.cli; + +import static java.util.Arrays.asList; + +import java.io.IOException; +import java.io.UncheckedIOException; +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.concurrent.TimeUnit; +import java.util.logging.Logger; + +import com.exasol.mavenprojectversiongetter.MavenProjectVersionGetter; + +class JarLauncher +{ + private static final Logger LOG = Logger.getLogger(ProcessOutputConsumer.class.getName()); + + private final Process process; + private final ProcessOutputConsumer consumer; + + private JarLauncher(final Process process, final ProcessOutputConsumer consumer) + { + this.process = process; + this.consumer = consumer; + } + + public static JarLauncher start(final Path workingDir, final List args) + { + final Path jarPath = getExecutableJar(); + if (!Files.exists(jarPath)) + { + throw new IllegalStateException( + "Executable JAR not found at %s. Run 'mvn -T1C package -DskipTests' to build it."); + } + final List command = new ArrayList<>(); + command.addAll(asList(getJavaExecutable().toString(), "-jar", getExecutableJar().toString())); + command.addAll(args); + final ProcessBuilder processBuilder = new ProcessBuilder(command.toArray(String[]::new)) + .redirectErrorStream(false); + if (workingDir != null) + { + processBuilder.directory(workingDir.toFile()); + } + try + { + final Process process = processBuilder.start(); + final ProcessOutputConsumer consumer = new ProcessOutputConsumer(process); + return new JarLauncher(process, consumer); + } + catch (final IOException exception) + { + throw new UncheckedIOException("Failed to start process %s in working dir %s: %s".formatted(command, + workingDir, exception.getMessage()), exception); + } + } + + private static Path getExecutableJar() + { + return Path.of("target") + .resolve("openfasttrace-%s.jar".formatted(getCurrentProjectVersion())) + .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")); + } + + void waitUntilTerminated(final Duration timeout) + { + waitForProcessTerminated(timeout); + LOG.fine("Process terminated with exit code %d".formatted(exitValue())); + consumer.waitForStreamsClosed(timeout); + } + + private void waitForProcessTerminated(final Duration timeout) + { + try + { + if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) + { + throw new IllegalStateException( + "Timeout while waiting %s for process %d".formatted(timeout, process.pid())); + } + } + catch (final InterruptedException exception) + { + Thread.currentThread().interrupt(); + } + } + + int exitValue() + { + return process.exitValue(); + } +} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java new file mode 100644 index 00000000..5f3886b6 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java @@ -0,0 +1,147 @@ +package org.itsallcode.openfasttrace.cli; + +import java.io.*; +import java.lang.Thread.UncaughtExceptionHandler; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +class ProcessOutputConsumer +{ + private static final Logger LOG = Logger.getLogger(ProcessOutputConsumer.class.getName()); + private final Executor executor; + private final Process process; + private final ProcessStreamConsumer stdOutConsumer; + private final ProcessStreamConsumer stdErrConsumer; + + ProcessOutputConsumer(final Process process) + { + this(createThreadExecutor(), process, new ProcessStreamConsumer("stdout"), new ProcessStreamConsumer("stderr")); + } + + ProcessOutputConsumer(final Executor executor, final Process process, + final ProcessStreamConsumer stdOutConsumer, final ProcessStreamConsumer stdErrConsumer) + { + this.executor = executor; + this.process = process; + this.stdOutConsumer = stdOutConsumer; + this.stdErrConsumer = stdErrConsumer; + } + + private static Executor createThreadExecutor() + { + return runnable -> { + final Thread thread = new Thread(runnable); + thread.setName(ProcessOutputConsumer.class.getSimpleName()); + thread.setUncaughtExceptionHandler(new LoggingExceptionHandler()); + thread.start(); + }; + } + + void start() + { + executor.execute(() -> { + readStream(process.getInputStream(), stdOutConsumer); + }); + executor.execute(() -> { + readStream(process.getErrorStream(), stdErrConsumer); + }); + } + + private void readStream(final InputStream stream, final ProcessStreamConsumer consumer) + { + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) + { + String line = null; + while ((line = reader.readLine()) != null) + { + consumer.accept(line); + } + consumer.streamFinished(); + } + catch (final IOException exception) + { + LOG.log(Level.WARNING, "Reading input stream failed: %s".formatted(exception.getMessage()), exception); + consumer.streamFinished(); + } + } + + String getStdOut() + { + return stdOutConsumer.getContent(); + } + + String getStdErr() + { + return stdErrConsumer.getContent(); + } + + void waitForStreamsClosed(final Duration timeout) + { + stdOutConsumer.waitUntilStreamClosed(timeout); + stdErrConsumer.waitUntilStreamClosed(timeout); + } + + private static class ProcessStreamConsumer + { + private final CountDownLatch streamFinished = new CountDownLatch(1); + private final StringBuilder builder = new StringBuilder(); + private final String name; + + ProcessStreamConsumer(final String name) + { + this.name = name; + } + + String getContent() + { + return builder.toString(); + } + + void streamFinished() + { + streamFinished.countDown(); + } + + void accept(final String line) + { + LOG.fine("%s > %s".formatted(name, line)); + builder.append(line).append("\n"); + } + + void waitUntilStreamClosed(final Duration timeout) + { + if (!await(timeout)) + { + throw new IllegalStateException( + "Stream '%s' not closed within timeout of %s".formatted(name, timeout)); + } + } + + private boolean await(final Duration timeout) + { + try + { + return streamFinished.await(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + catch (final InterruptedException exception) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for stream to be closed", exception); + } + } + } + + private static class LoggingExceptionHandler implements UncaughtExceptionHandler + { + @Override + public void uncaughtException(final Thread thread, final Throwable exception) + { + LOG.log(Level.WARNING, + "Exception occurred in thread '%s': %s".formatted(thread.getName(), exception.toString()), + exception); + } + } +} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java index 8e80e9dc..9296bb31 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java @@ -2,6 +2,7 @@ import static org.itsallcode.junit.sysextensions.AssertExit.assertExitWithStatus; +import java.time.Duration; import java.util.*; import org.itsallcode.junit.sysextensions.ExitGuard; @@ -61,4 +62,10 @@ void testCliExitCode_CliError() final String[] arguments = { "--zzzzz" }; assertExitWithStatus(ExitStatus.CLI_ERROR.getCode(), () -> CliStarter.main(arguments)); } + + @Test + void testExecutableJarLauncher() + { + JarLauncher.start(null, List.of()).waitUntilTerminated(Duration.ofSeconds(10)); + } } From a6a114dc8d7975b06eca84126b2a91ace0e8c115 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 26 Jan 2025 20:45:37 +0100 Subject: [PATCH 02/25] Fix starting stream consumer --- .../openfasttrace/cli/JarLauncher.java | 10 ++++-- .../cli/ProcessOutputConsumer.java | 32 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index b39a5f18..8d8c3293 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -47,7 +47,8 @@ public static JarLauncher start(final Path workingDir, final List args) try { final Process process = processBuilder.start(); - final ProcessOutputConsumer consumer = new ProcessOutputConsumer(process); + final ProcessOutputConsumer consumer = new ProcessOutputConsumer(process, Duration.ofSeconds(1)); + consumer.start(); return new JarLauncher(process, consumer); } catch (final IOException exception) @@ -79,14 +80,15 @@ private static Path getJavaExecutable() void waitUntilTerminated(final Duration timeout) { waitForProcessTerminated(timeout); - LOG.fine("Process terminated with exit code %d".formatted(exitValue())); - consumer.waitForStreamsClosed(timeout); + LOG.fine("Process %d terminated with exit code %d".formatted(process.pid(), exitValue())); + consumer.waitForStreamsClosed(); } private void waitForProcessTerminated(final Duration timeout) { try { + LOG.finest("Waiting %s for process %d to terminate...".formatted(timeout, process.pid())); if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) { throw new IllegalStateException( @@ -96,6 +98,8 @@ private void waitForProcessTerminated(final Duration timeout) catch (final InterruptedException exception) { Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting %s for process to finish".formatted(timeout), + exception); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java index 5f3886b6..7e004f46 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java @@ -16,9 +16,10 @@ class ProcessOutputConsumer private final ProcessStreamConsumer stdOutConsumer; private final ProcessStreamConsumer stdErrConsumer; - ProcessOutputConsumer(final Process process) + ProcessOutputConsumer(final Process process, final Duration streamCloseTimeout) { - this(createThreadExecutor(), process, new ProcessStreamConsumer("stdout"), new ProcessStreamConsumer("stderr")); + this(createThreadExecutor(), process, new ProcessStreamConsumer("stdout", streamCloseTimeout), + new ProcessStreamConsumer("stderr", streamCloseTimeout)); } ProcessOutputConsumer(final Executor executor, final Process process, @@ -42,6 +43,7 @@ private static Executor createThreadExecutor() void start() { + LOG.finest("Start reading stdout and stderr streams in background..."); executor.execute(() -> { readStream(process.getInputStream(), stdOutConsumer); }); @@ -55,10 +57,12 @@ private void readStream(final InputStream stream, final ProcessStreamConsumer co try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { String line = null; + LOG.finest("Start reading from '%s' stream...".formatted(consumer.name)); while ((line = reader.readLine()) != null) { consumer.accept(line); } + LOG.finest("Stream '%s' finished".formatted(consumer.name)); consumer.streamFinished(); } catch (final IOException exception) @@ -78,10 +82,10 @@ String getStdErr() return stdErrConsumer.getContent(); } - void waitForStreamsClosed(final Duration timeout) + void waitForStreamsClosed() { - stdOutConsumer.waitUntilStreamClosed(timeout); - stdErrConsumer.waitUntilStreamClosed(timeout); + stdOutConsumer.waitUntilStreamClosed(); + stdErrConsumer.waitUntilStreamClosed(); } private static class ProcessStreamConsumer @@ -89,10 +93,12 @@ private static class ProcessStreamConsumer private final CountDownLatch streamFinished = new CountDownLatch(1); private final StringBuilder builder = new StringBuilder(); private final String name; + private final Duration streamCloseTimeout; - ProcessStreamConsumer(final String name) + ProcessStreamConsumer(final String name, final Duration streamCloseTimeout) { this.name = name; + this.streamCloseTimeout = streamCloseTimeout; } String getContent() @@ -111,12 +117,17 @@ void accept(final String line) builder.append(line).append("\n"); } - void waitUntilStreamClosed(final Duration timeout) + void waitUntilStreamClosed() { - if (!await(timeout)) + LOG.finest("Waiting %s for stream '%s' to close".formatted(streamCloseTimeout, name)); + if (!await(streamCloseTimeout)) { throw new IllegalStateException( - "Stream '%s' not closed within timeout of %s".formatted(name, timeout)); + "Stream '%s' not closed within timeout of %s".formatted(name, streamCloseTimeout)); + } + else + { + LOG.finest("Stream '%s' closed".formatted(name)); } } @@ -129,7 +140,8 @@ private boolean await(final Duration timeout) catch (final InterruptedException exception) { Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for stream to be closed", exception); + throw new IllegalStateException("Interrupted while waiting for stream '%s' to be closed: %s" + .formatted(name, exception.getMessage()), exception); } } } From bf774193983c7419ea91d764bb06a8b2c16ef7fa Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 3 Jul 2025 15:08:31 +0200 Subject: [PATCH 03/25] Add builder for JarLauncher --- .../core/cli/TestCliStarter.java | 56 ------ parent/pom.xml | 6 + product/pom.xml | 5 + .../openfasttrace/cli/ITestCliWithFilter.java | 10 +- .../openfasttrace/cli/JarLauncher.java | 177 ++++++++++++------ .../cli/ProcessOutputConsumer.java | 159 ---------------- .../openfasttrace/cli/TestCliExit.java | 109 ++++++++--- .../openfasttrace/cli/TestCliStarter.java | 10 +- .../testutil/TestAssumptions.java | 30 --- 9 files changed, 218 insertions(+), 344 deletions(-) delete mode 100644 core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliStarter.java delete mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java delete mode 100644 testutil/src/main/java/org/itsallcode/openfasttrace/testutil/TestAssumptions.java 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 1782e620..00000000 --- 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/parent/pom.xml b/parent/pom.xml index 1eb0ad1b..2c37ccc9 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -242,6 +242,12 @@ 1.2.1 test + + org.itsallcode + simple-process + 0.2.0 + test + diff --git a/product/pom.xml b/product/pom.xml index d1a45a60..4a325a05 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -67,6 +67,11 @@ maven-project-version-getter test + + org.itsallcode + simple-process + test + openfasttrace-${revision} 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 130b7399..4f4202fd 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java @@ -14,8 +14,8 @@ 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; @@ -37,12 +37,6 @@ 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 { diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index 8d8c3293..b0244f39 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -1,67 +1,72 @@ package org.itsallcode.openfasttrace.cli; -import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertAll; -import java.io.IOException; -import java.io.UncheckedIOException; 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.concurrent.TimeUnit; +import java.util.*; +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; -class JarLauncher +final class JarLauncher { - private static final Logger LOG = Logger.getLogger(ProcessOutputConsumer.class.getName()); - - private final Process process; - private final ProcessOutputConsumer consumer; + private static final Logger LOG = Logger.getLogger(JarLauncher.class.getName()); + private final SimpleProcess process; + private final Builder builder; - private JarLauncher(final Process process, final ProcessOutputConsumer consumer) + private JarLauncher(final SimpleProcess process, final Builder builder) { this.process = process; - this.consumer = consumer; + this.builder = builder; } - public static JarLauncher start(final Path workingDir, final List args) + private static JarLauncher start(final Builder builder) { - final Path jarPath = getExecutableJar(); + final Path jarPath = getExecutableJar(builder.jarNameTemplate); if (!Files.exists(jarPath)) { throw new IllegalStateException( "Executable JAR not found at %s. Run 'mvn -T1C package -DskipTests' to build it."); } final List command = new ArrayList<>(); - command.addAll(asList(getJavaExecutable().toString(), "-jar", getExecutableJar().toString())); - command.addAll(args); - final ProcessBuilder processBuilder = new ProcessBuilder(command.toArray(String[]::new)) - .redirectErrorStream(false); - if (workingDir != null) - { - processBuilder.directory(workingDir.toFile()); - } - try + command.addAll(javaLaunchArgs(jarPath, builder.mainClass)); + if (builder.args != null) { - final Process process = processBuilder.start(); - final ProcessOutputConsumer consumer = new ProcessOutputConsumer(process, Duration.ofSeconds(1)); - consumer.start(); - return new JarLauncher(process, consumer); + command.addAll(builder.args); } - catch (final IOException exception) + + final SimpleProcess process = SimpleProcessBuilder.create().command(command) + .workingDir(builder.workingDir) + .redirectErrorStream(false) + .streamLogLevel(Level.INFO) + .start(); + return new JarLauncher(process, builder); + } + + private static List javaLaunchArgs(final Path jarPath, final Class mainClass) + { + final String javaExecutable = getJavaExecutable().toString(); + if (mainClass == null) { - throw new UncheckedIOException("Failed to start process %s in working dir %s: %s".formatted(command, - workingDir, exception.getMessage()), exception); + return List.of(javaExecutable, "-jar", jarPath.toString()); } + return List.of(javaExecutable, "-classpath", jarPath.toString(), mainClass.getName()); } - private static Path getExecutableJar() + private static Path getExecutableJar(final String jarNameTemplate) { return Path.of("target") - .resolve("openfasttrace-%s.jar".formatted(getCurrentProjectVersion())) + .resolve(Objects.requireNonNull(jarNameTemplate, "jarNameTemplate") + .formatted(getCurrentProjectVersion())) .toAbsolutePath(); } @@ -77,34 +82,100 @@ private static Path getJavaExecutable() .orElseThrow(() -> new IllegalStateException("Java executable not found")); } - void waitUntilTerminated(final Duration timeout) + public void waitUntilTerminated(final Duration timeout) + { + 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", 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); + } + }); + } + + public static Builder builder() { - waitForProcessTerminated(timeout); - LOG.fine("Process %d terminated with exit code %d".formatted(process.pid(), exitValue())); - consumer.waitForStreamsClosed(); + return new Builder(); } - private void waitForProcessTerminated(final Duration timeout) + public static final class Builder { - try + private String jarNameTemplate; + private Path workingDir; + private List args; + private int expectedExitCode = 0; + private Matcher expectedStdOut; + private Matcher expectedStdErr; + private Class mainClass; + + private Builder() { - LOG.finest("Waiting %s for process %d to terminate...".formatted(timeout, process.pid())); - if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) - { - throw new IllegalStateException( - "Timeout while waiting %s for process %d".formatted(timeout, process.pid())); - } } - catch (final InterruptedException exception) + + public Builder jarNameTemplate(final String jarNameTemplate) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting %s for process to finish".formatted(timeout), - exception); + this.jarNameTemplate = jarNameTemplate; + return this; } - } - int exitValue() - { - return process.exitValue(); + public Builder mainClass(final Class mainClass) + { + this.mainClass = mainClass; + return this; + } + + public Builder currentWorkingDir() + { + return this.workingDir(Path.of(System.getProperty("user.dir"))); + } + + public Builder workingDir(final Path workingDir) + { + this.workingDir = workingDir; + return this; + } + + public Builder args(final List args) + { + this.args = args; + return this; + } + + public Builder successExitCode() + { + return this.expectedExitCode(0); + } + + public Builder expectedExitCode(final int expectedExitCode) + { + this.expectedExitCode = expectedExitCode; + return this; + } + + public Builder expectStdOut(final Matcher expectedStdOut) + { + this.expectedStdOut = expectedStdOut; + return this; + } + + public Builder expectStdErr(final Matcher expectedStdErr) + { + this.expectedStdErr = expectedStdErr; + return this; + } + + public JarLauncher start() + { + return JarLauncher.start(this); + } } + } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java deleted file mode 100644 index 7e004f46..00000000 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ProcessOutputConsumer.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.itsallcode.openfasttrace.cli; - -import java.io.*; -import java.lang.Thread.UncaughtExceptionHandler; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.concurrent.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -class ProcessOutputConsumer -{ - private static final Logger LOG = Logger.getLogger(ProcessOutputConsumer.class.getName()); - private final Executor executor; - private final Process process; - private final ProcessStreamConsumer stdOutConsumer; - private final ProcessStreamConsumer stdErrConsumer; - - ProcessOutputConsumer(final Process process, final Duration streamCloseTimeout) - { - this(createThreadExecutor(), process, new ProcessStreamConsumer("stdout", streamCloseTimeout), - new ProcessStreamConsumer("stderr", streamCloseTimeout)); - } - - ProcessOutputConsumer(final Executor executor, final Process process, - final ProcessStreamConsumer stdOutConsumer, final ProcessStreamConsumer stdErrConsumer) - { - this.executor = executor; - this.process = process; - this.stdOutConsumer = stdOutConsumer; - this.stdErrConsumer = stdErrConsumer; - } - - private static Executor createThreadExecutor() - { - return runnable -> { - final Thread thread = new Thread(runnable); - thread.setName(ProcessOutputConsumer.class.getSimpleName()); - thread.setUncaughtExceptionHandler(new LoggingExceptionHandler()); - thread.start(); - }; - } - - void start() - { - LOG.finest("Start reading stdout and stderr streams in background..."); - executor.execute(() -> { - readStream(process.getInputStream(), stdOutConsumer); - }); - executor.execute(() -> { - readStream(process.getErrorStream(), stdErrConsumer); - }); - } - - private void readStream(final InputStream stream, final ProcessStreamConsumer consumer) - { - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) - { - String line = null; - LOG.finest("Start reading from '%s' stream...".formatted(consumer.name)); - while ((line = reader.readLine()) != null) - { - consumer.accept(line); - } - LOG.finest("Stream '%s' finished".formatted(consumer.name)); - consumer.streamFinished(); - } - catch (final IOException exception) - { - LOG.log(Level.WARNING, "Reading input stream failed: %s".formatted(exception.getMessage()), exception); - consumer.streamFinished(); - } - } - - String getStdOut() - { - return stdOutConsumer.getContent(); - } - - String getStdErr() - { - return stdErrConsumer.getContent(); - } - - void waitForStreamsClosed() - { - stdOutConsumer.waitUntilStreamClosed(); - stdErrConsumer.waitUntilStreamClosed(); - } - - private static class ProcessStreamConsumer - { - private final CountDownLatch streamFinished = new CountDownLatch(1); - private final StringBuilder builder = new StringBuilder(); - private final String name; - private final Duration streamCloseTimeout; - - ProcessStreamConsumer(final String name, final Duration streamCloseTimeout) - { - this.name = name; - this.streamCloseTimeout = streamCloseTimeout; - } - - String getContent() - { - return builder.toString(); - } - - void streamFinished() - { - streamFinished.countDown(); - } - - void accept(final String line) - { - LOG.fine("%s > %s".formatted(name, line)); - builder.append(line).append("\n"); - } - - void waitUntilStreamClosed() - { - LOG.finest("Waiting %s for stream '%s' to close".formatted(streamCloseTimeout, name)); - if (!await(streamCloseTimeout)) - { - throw new IllegalStateException( - "Stream '%s' not closed within timeout of %s".formatted(name, streamCloseTimeout)); - } - else - { - LOG.finest("Stream '%s' closed".formatted(name)); - } - } - - private boolean await(final Duration timeout) - { - try - { - return streamFinished.await(timeout.toMillis(), TimeUnit.MILLISECONDS); - } - catch (final InterruptedException exception) - { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for stream '%s' to be closed: %s" - .formatted(name, exception.getMessage()), exception); - } - } - } - - private static class LoggingExceptionHandler implements UncaughtExceptionHandler - { - @Override - public void uncaughtException(final Thread thread, final Throwable exception) - { - LOG.log(Level.WARNING, - "Exception occurred in thread '%s': %s".formatted(thread.getName(), exception.toString()), - exception); - } - } -} diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java index 75a9afcf..a88b8def 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java @@ -1,31 +1,77 @@ package org.itsallcode.openfasttrace.cli; -import static org.itsallcode.junit.sysextensions.AssertExit.assertExitWithStatus; +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.*; +import java.nio.file.Path; import java.time.Duration; import java.util.*; -import org.itsallcode.junit.sysextensions.ExitGuard; -import org.itsallcode.openfasttrace.core.cli.*; +import org.itsallcode.openfasttrace.core.cli.ExitStatus; 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; +import org.junit.jupiter.api.io.TempDir; -@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"; + private static final Duration TIMEOUT = Duration.ofSeconds(5); - @BeforeAll - static void assumeSecurityManagerSupported() + @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")) + .start() + .waitUntilTerminated(TIMEOUT); + } + + @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")) + .start() + .waitUntilTerminated(TIMEOUT); + } + + @Test + void testRunWithHelpCommand() { - TestAssumptions.assumeSecurityManagerSupported(); + jarLauncher() + .args(List.of("help")) + .expectedExitCode(ExitStatus.OK.getCode()) + .expectStdOut(startsWith(""" + OpenFastTrace + + Usage: + oft command""")) + .expectStdErr(emptyString()) + .start() + .waitUntilTerminated(TIMEOUT); + } + + @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'")) + .start() + .waitUntilTerminated(TIMEOUT); } @Test @@ -34,21 +80,25 @@ void testCliExitCode_Ok() assertExitStatusForTracedFiles(ExitStatus.OK, SAMPLE_SYSTEM_REQUIREMENTS, SAMPLE_DESIGN); } - private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, - final String... files) + private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, final String... files) { - assertExitStatusForCommandWithFiles(expectedStatus, TraceCommand.COMMAND_NAME, 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())) + .start() + .waitUntilTerminated(TIMEOUT); } - private void assertExitStatusForCommandWithFiles(final ExitStatus expectedStatus, - final String command, final String... files) + private JarLauncher.Builder jarLauncher() { - 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()); + return JarLauncher.builder() + .jarNameTemplate("openfasttrace-%s.jar") + .currentWorkingDir(); } @Test @@ -60,13 +110,12 @@ void testCliExitCode_Failure() @Test void testCliExitCode_CliError() { - final String[] arguments = { "--zzzzz" }; - assertExitWithStatus(ExitStatus.CLI_ERROR.getCode(), () -> CliStarter.main(arguments)); - } - - @Test - void testExecutableJarLauncher() - { - JarLauncher.start(null, List.of()).waitUntilTerminated(Duration.ofSeconds(10)); + jarLauncher() + .args(List.of("--zzzz")) + .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) + .expectStdOut(emptyString()) + .expectStdErr(equalTo("oft: Unexpected parameter '--zzzz' is not allowed\n")) + .start() + .waitUntilTerminated(TIMEOUT); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java index f238bed1..6f698053 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java @@ -15,9 +15,9 @@ 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.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; @@ -47,12 +47,6 @@ class TestCliStarter private Path outputFile; - @BeforeAll - static void assumeSecurityManagerSupported() - { - TestAssumptions.assumeSecurityManagerSupported(); - } - @BeforeEach void beforeEach(@TempDir final Path tempDir) { 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 b6ca95cf..00000000 --- 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); - } -} From 42740d6f32018b69754f1356fc1e6053bb7d73c9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 3 Jul 2025 15:10:18 +0200 Subject: [PATCH 04/25] Convert test to integration test --- .../openfasttrace/cli/{TestCliExit.java => CliExitIT.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename product/src/test/java/org/itsallcode/openfasttrace/cli/{TestCliExit.java => CliExitIT.java} (99%) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java similarity index 99% rename from product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java rename to product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java index a88b8def..ee9111ae 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliExit.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -class TestCliExit +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"; From e79f0f3427e0802c11ea4b5afec8adc70b0cace2 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 3 Jul 2025 15:14:04 +0200 Subject: [PATCH 05/25] Test with latest Java 24 --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65102e1e..6055a941 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: 24 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-os-${{ matrix.os }}-java-${{ matrix.java }} From 1976464b6f5e32882f1cb3d8be8d2477c583a31e Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Thu, 3 Jul 2025 15:14:30 +0200 Subject: [PATCH 06/25] Remove ExitGuard annotations --- .../org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java | 3 --- .../org/itsallcode/openfasttrace/cli/TestCliStarter.java | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) 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 4f4202fd..a495e059 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java @@ -10,7 +10,6 @@ import java.nio.file.Path; 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; @@ -19,8 +18,6 @@ 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 { diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java index 6f698053..8992ab7e 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java @@ -9,8 +9,9 @@ import java.nio.file.*; import org.itsallcode.io.Capturable; -import org.itsallcode.junit.sysextensions.*; +import org.itsallcode.junit.sysextensions.SystemErrGuard; import org.itsallcode.junit.sysextensions.SystemErrGuard.SysErr; +import org.itsallcode.junit.sysextensions.SystemOutGuard; import org.itsallcode.junit.sysextensions.SystemOutGuard.SysOut; import org.itsallcode.junit.sysextensions.security.ExitTrapException; import org.itsallcode.openfasttrace.core.cli.CliStarter; @@ -22,8 +23,6 @@ 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] From ee88f30cff23b8fa4cf565bb5645c00bfc92e8d7 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 5 Jul 2025 18:07:17 +0200 Subject: [PATCH 07/25] Adapt tests to Java > 21 --- .../openfasttrace/cli/CliStarterIT.java | 349 +++++++++++++++ .../openfasttrace/cli/ITestCliWithFilter.java | 48 +-- .../openfasttrace/cli/JarLauncher.java | 5 +- .../openfasttrace/cli/TestCliStarter.java | 396 ------------------ .../testutil/AbstractFileBasedTest.java | 14 +- 5 files changed, 376 insertions(+), 436 deletions(-) create mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java delete mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java 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 00000000..3ce0c4b1 --- /dev/null +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -0,0 +1,349 @@ +package org.itsallcode.openfasttrace.cli; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.IOException; +import java.nio.file.*; +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 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.expectStdOut(startsWith(message)).expectedExitCode(status.getCode()).start(); + } + + // [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 = System.lineSeparator(); + 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).start(); + assertOutputFileExists(false); + } + + @Test + void testConvertUnknownExporter() + { + final Builder jarLauncherBuilder = jarLauncher( + CONVERT_COMMAND, this.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, this.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() throws IOException + { + assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND, this.DOC_DIR.toString()), SPECOBJECT_PREAMBLE); + } + + // [itest->dsn~cli.input-file-selection~1] + @Test + void testConvertDefaultOutputFormatIntoFile() + { + assertExitOkWithOutputFileStart(jarLauncher(CONVERT_COMMAND, this.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() + { + // 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. + jarLauncher(TRACE_COMMAND).start(); + } + + // [itest->dsn~cli.command-selection~1] + @Test + void testTrace() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + this.DOC_DIR.toString()), "ok - 5 total"); + } + + @Test + void testTraceWithReportVerbosityMinimal() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, this.DOC_DIR.toString(), + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), + REPORT_VERBOSITY_PARAMETER, "MINIMAL"), "ok"); + } + + @Test + void testTraceWithReportVerbosityQuietToStdOut() + { + jarLauncher( + TRACE_COMMAND, this.DOC_DIR.toString(), + REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString()) + .expectedExitCode(ExitStatus.OK.getCode()).start(); + assertOutputFileExists(false); + } + + @Test + void testTraceWithReportVerbosityQuietToFileMustBeRejected() + { + jarLauncher( + TRACE_COMMAND, this.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() + { + // 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. + jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()).expectStdErr(emptyString()); + assertOutputFileExists(true); + + } + + @Test + void testBasicHtmlTrace() + { + assertExitOkWithStdOutStart(jarLauncher( + TRACE_COMMAND, this.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", + this.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(), + this.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).start(); + } + + 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() + { + assertExitOkWithOutputFileStart(jarLauncher( + TRACE_COMMAND, this.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 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 Builder jarLauncher(final String... arguments) + { + return jarLauncher().args(List.of(arguments)); + } + + private JarLauncher.Builder jarLauncher() + { + return JarLauncher.builder() + .jarNameTemplate("openfasttrace-%s.jar") + .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 a495e059..653fffdd 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/ITestCliWithFilter.java @@ -1,17 +1,15 @@ 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.SystemOutGuard; -import org.itsallcode.junit.sysextensions.SystemOutGuard.SysOut; import org.itsallcode.openfasttrace.testutil.AbstractFileBasedTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,55 +33,43 @@ class ITestCliWithFilter extends AbstractFileBasedTest private File specFile; @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 index b0244f39..aa7975c2 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -17,7 +17,7 @@ import com.exasol.mavenprojectversiongetter.MavenProjectVersionGetter; -final class JarLauncher +public final class JarLauncher { private static final Logger LOG = Logger.getLogger(JarLauncher.class.getName()); private final SimpleProcess process; @@ -35,7 +35,8 @@ private static JarLauncher start(final Builder builder) if (!Files.exists(jarPath)) { throw new IllegalStateException( - "Executable JAR not found at %s. Run 'mvn -T1C package -DskipTests' to build it."); + "Executable JAR not found at %s. Run 'mvn -T1C package -DskipTests' to build it." + .formatted(jarPath)); } final List command = new ArrayList<>(); command.addAll(javaLaunchArgs(jarPath, builder.mainClass)); 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 8992ab7e..00000000 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java +++ /dev/null @@ -1,396 +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.SystemErrGuard; -import org.itsallcode.junit.sysextensions.SystemErrGuard.SysErr; -import org.itsallcode.junit.sysextensions.SystemOutGuard; -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.cli.FakeDirectoryService; -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 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; - - @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 7ce4c902..f67d058e 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,8 @@ 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).expectStdOut(stdOutMatcher).expectedExitCode(0).start(); } -} \ No newline at end of file +} From ff7b6bd163b7ae45055585a3283ac5cd6bd80985 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 5 Jul 2025 18:11:55 +0200 Subject: [PATCH 08/25] Fix integration tests --- .../test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java | 1 - .../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 1 - .../test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java | 2 +- .../openfasttrace/testutil/AbstractFileBasedTest.java | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java index ee9111ae..7a856a91 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java @@ -97,7 +97,6 @@ private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, fin private JarLauncher.Builder jarLauncher() { return JarLauncher.builder() - .jarNameTemplate("openfasttrace-%s.jar") .currentWorkingDir(); } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index 3ce0c4b1..05f69228 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -343,7 +343,6 @@ private Builder jarLauncher(final String... arguments) private JarLauncher.Builder jarLauncher() { return JarLauncher.builder() - .jarNameTemplate("openfasttrace-%s.jar") .currentWorkingDir(); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index aa7975c2..ed9ab8d3 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -109,7 +109,7 @@ public static Builder builder() public static final class Builder { - private String jarNameTemplate; + private String jarNameTemplate = "openfasttrace-%s.jar"; private Path workingDir; private List args; private int expectedExitCode = 0; 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 f67d058e..568e9928 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java @@ -25,6 +25,6 @@ protected void writeTextFile(final File file, final String content) throws IOExc @SuppressWarnings("javadoc") protected void assertStdOut(final List args, final Matcher stdOutMatcher) { - JarLauncher.builder().args(args).expectStdOut(stdOutMatcher).expectedExitCode(0).start(); + JarLauncher.builder().args(args).currentWorkingDir().expectStdOut(stdOutMatcher).expectedExitCode(0).start(); } } From 436824464459140d2081fcff713846400693529d Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 6 Jul 2025 17:25:27 +0200 Subject: [PATCH 09/25] Fix integration tests --- .../openfasttrace/cli/CliExitIT.java | 14 ++--- .../openfasttrace/cli/CliStarterIT.java | 60 ++++++++++--------- .../openfasttrace/cli/JarLauncher.java | 10 +++- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java index 7a856a91..e6160213 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java @@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.*; import java.nio.file.Path; -import java.time.Duration; import java.util.*; import org.itsallcode.openfasttrace.core.cli.ExitStatus; @@ -18,7 +17,6 @@ class CliExitIT 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"; - private static final Duration TIMEOUT = Duration.ofSeconds(5); @Test void testRunWithoutArguments() @@ -29,7 +27,7 @@ void testRunWithoutArguments() .expectStdOut(emptyString()) .expectStdErr(equalTo("oft: Missing command\nAdd one of 'help','convert','trace'\n\n")) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } @Test @@ -42,7 +40,7 @@ void testRunWithUnsupportedCommand() .expectStdErr(equalTo( "oft: 'unsupported' is not an OFT command.\nChoose one of 'help','convert','trace'.\n\n")) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } @Test @@ -58,7 +56,7 @@ void testRunWithHelpCommand() oft command""")) .expectStdErr(emptyString()) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } @Test @@ -71,7 +69,7 @@ void testRunWithUnsupportedReporter(@TempDir final Path emptyDir) .expectStdErr(startsWith( "Exception in thread \"main\" org.itsallcode.openfasttrace.api.exporter.ExporterException: Found no matching reporter for output format 'unknown'")) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } @Test @@ -91,7 +89,7 @@ private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, fin .expectStdErr(emptyString()) .expectStdOut(not(emptyString())) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } private JarLauncher.Builder jarLauncher() @@ -115,6 +113,6 @@ void testCliExitCode_CliError() .expectStdOut(emptyString()) .expectStdErr(equalTo("oft: Unexpected parameter '--zzzz' is not allowed\n")) .start() - .waitUntilTerminated(TIMEOUT); + .waitUntilTerminated(); } } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index 05f69228..f62093b9 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -3,9 +3,11 @@ 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.nio.file.*; +import java.time.Duration; import java.util.List; import org.itsallcode.junit.sysextensions.SystemErrGuard; @@ -37,7 +39,7 @@ class CliStarterIT 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 static final Path DOC_DIR = Paths.get("../core/src/test/resources/markdown").toAbsolutePath(); private Path outputFile; @@ -92,7 +94,7 @@ private void assertExitOkWithStdOutStart(final JarLauncher.Builder jarLauncherBu void testConvertUnknownExporter() { final Builder jarLauncherBuilder = jarLauncher( - CONVERT_COMMAND, this.DOC_DIR.toString(), + CONVERT_COMMAND, DOC_DIR.toString(), OUTPUT_FORMAT_PARAMETER, "illegal", OUTPUT_FILE_PARAMETER, this.outputFile.toString()); assertExitWithError(jarLauncherBuilder, ExitStatus.CLI_ERROR, @@ -104,7 +106,7 @@ void testConvertUnknownExporter() void testConvertToSpecobjectFile() { final Builder jarLauncherBuilder = jarLauncher( // - CONVERT_COMMAND, this.DOC_DIR.toString(), // + CONVERT_COMMAND, DOC_DIR.toString(), // OUTPUT_FORMAT_PARAMETER, "specobject", // OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE"); @@ -115,7 +117,8 @@ void testConvertToSpecobjectFile() private void assertExitOkWithOutputFileStart(final JarLauncher.Builder jarLauncherBuilder, final String fileStart) throws MultipleFailuresError { - jarLauncherBuilder.expectedExitCode(ExitStatus.OK.getCode()).start(); + jarLauncherBuilder.expectedExitCode(ExitStatus.OK.getCode()).start() + .waitUntilTerminated(Duration.ofSeconds(10)); assertAll( () -> assertOutputFileExists(true), () -> assertOutputFileContentStartsWith(fileStart)); @@ -123,16 +126,16 @@ private void assertExitOkWithOutputFileStart(final JarLauncher.Builder jarLaunch // [itest->dsn~cli.conversion.default-output-format~1] @Test - void testConvertDefaultOutputFormat() throws IOException + void testConvertDefaultOutputFormat() { - assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND, this.DOC_DIR.toString()), SPECOBJECT_PREAMBLE); + assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString()), SPECOBJECT_PREAMBLE); } // [itest->dsn~cli.input-file-selection~1] @Test void testConvertDefaultOutputFormatIntoFile() { - assertExitOkWithOutputFileStart(jarLauncher(CONVERT_COMMAND, this.DOC_DIR.toString(), + assertExitOkWithOutputFileStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER, this.outputFile.toString()), SPECOBJECT_PREAMBLE); } @@ -140,10 +143,9 @@ void testConvertDefaultOutputFormatIntoFile() @Test void testConvertDefaultInputDir() { - assertExitOkWithOutputFileOfLength(jarLauncher( // - CONVERT_COMMAND, // - OUTPUT_FILE_PARAMETER, this.outputFile.toString() // - ), 2000); + assertExitOkWithOutputFileOfLength(jarLauncher( + CONVERT_COMMAND, + OUTPUT_FILE_PARAMETER, this.outputFile.toString()), 2000); } @Test @@ -162,14 +164,14 @@ void testTrace() assertExitOkWithOutputFileStart(jarLauncher( TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString(), - this.DOC_DIR.toString()), "ok - 5 total"); + DOC_DIR.toString()), "ok - 5 total"); } @Test void testTraceWithReportVerbosityMinimal() { assertExitOkWithOutputFileStart(jarLauncher( - TRACE_COMMAND, this.DOC_DIR.toString(), + TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER, this.outputFile.toString(), REPORT_VERBOSITY_PARAMETER, "MINIMAL"), "ok"); } @@ -178,7 +180,7 @@ void testTraceWithReportVerbosityMinimal() void testTraceWithReportVerbosityQuietToStdOut() { jarLauncher( - TRACE_COMMAND, this.DOC_DIR.toString(), + TRACE_COMMAND, DOC_DIR.toString(), REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString()) .expectedExitCode(ExitStatus.OK.getCode()).start(); assertOutputFileExists(false); @@ -188,7 +190,7 @@ void testTraceWithReportVerbosityQuietToStdOut() void testTraceWithReportVerbosityQuietToFileMustBeRejected() { jarLauncher( - TRACE_COMMAND, this.DOC_DIR.toString(), + 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")); @@ -198,20 +200,19 @@ void testTraceWithReportVerbosityQuietToFileMustBeRejected() // [itest->dsn~cli.default-input~1] void testTraceDefaultInputDir() { - // 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. - jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()).expectStdErr(emptyString()); + jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()) + .expectStdOut(emptyString()) + .expectedExitCode(1) + .start() + .waitUntilTerminated(Duration.ofSeconds(10)); assertOutputFileExists(true); - } @Test void testBasicHtmlTrace() { assertExitOkWithStdOutStart(jarLauncher( - TRACE_COMMAND, this.DOC_DIR.toString(), + TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FORMAT_PARAMETER, "html"), ""); } @@ -241,7 +242,7 @@ void testTraceMacNewlines() () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString(), NEWLINE_PARAMETER, "OLDMAC", - this.DOC_DIR.toString()), + DOC_DIR.toString()), () -> assertOutputFileExists(true), this::assertOutputFileContainsOldMacNewlines, this::assertOutputFileContainsNoUnixNewlines); @@ -266,7 +267,7 @@ void testTraceDefaultNewlines() assertAll( () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString(), - this.DOC_DIR.toString()), + DOC_DIR.toString()), () -> assertOutputFileExists(true), this::assertPlatformNewlines, this::assertNoOffendingNewlines); @@ -274,7 +275,7 @@ void testTraceDefaultNewlines() private void assertExitWithStatus(final int code, final String... args) { - jarLauncher().args(List.of(args)).expectedExitCode(code).start(); + jarLauncher().args(List.of(args)).expectedExitCode(code).start().waitUntilTerminated(); } private void assertPlatformNewlines() @@ -298,6 +299,8 @@ private void assertNoOffendingNewlines() assertThat("Has no newline without carriage return and vice-versa", getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false)); break; + default: + fail("Unsupported line separator"); } } @@ -305,7 +308,7 @@ private void assertNoOffendingNewlines() void testTraceWithFilteredArtifactType() { assertExitOkWithOutputFileStart(jarLauncher( - TRACE_COMMAND, this.DOC_DIR.toString(), + TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER, this.outputFile.toString(), WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req"), "ok - 3 total"); } @@ -317,7 +320,8 @@ private void assertOutputFileContentStartsWith(final String content) private void assertOutputFileExists(final boolean fileExists) { - assertThat("Output file exists", Files.exists(this.outputFile), equalTo(fileExists)); + assertThat("Output file %s exists".formatted(this.outputFile), Files.exists(this.outputFile), + equalTo(fileExists)); } private String getOutputFileContent() @@ -337,7 +341,7 @@ private String getOutputFileContent() private Builder jarLauncher(final String... arguments) { - return jarLauncher().args(List.of(arguments)); + return jarLauncher().workingDir(Path.of("..").toAbsolutePath()).args(List.of(arguments)); } private JarLauncher.Builder jarLauncher() diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index ed9ab8d3..c3d23b91 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -45,7 +45,9 @@ private static JarLauncher start(final Builder builder) command.addAll(builder.args); } - final SimpleProcess process = SimpleProcessBuilder.create().command(command) + 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.INFO) @@ -83,8 +85,14 @@ private static Path getJavaExecutable() .orElseThrow(() -> new IllegalStateException("Java executable not found")); } + public void waitUntilTerminated() + { + waitUntilTerminated(Duration.ofSeconds(3)); + } + public void waitUntilTerminated(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)); From b45e0343ff78247f13dd62a018bc7db482539111 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 7 Jul 2025 11:59:41 +0200 Subject: [PATCH 10/25] Improve test assertions --- .../itsallcode/openfasttrace/cli/CliStarterIT.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index f62093b9..b710c97a 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -6,6 +6,7 @@ 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; @@ -154,7 +155,12 @@ void testTraceNoArguments() // 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. - jarLauncher(TRACE_COMMAND).start(); + jarLauncher(TRACE_COMMAND) + .currentWorkingDir() + .expectedExitCode(1) + .expectStdOut(containsString("not ok - 43 total, 43 defect")) + .start() + .waitUntilTerminated(); } // [itest->dsn~cli.command-selection~1] @@ -327,6 +333,10 @@ private void assertOutputFileExists(final boolean 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); @@ -335,7 +345,7 @@ private String getOutputFileContent() { // Need to convert this to an unchecked exception. Otherwise, we get // stuck with the checked exceptions in the assertion lambdas. - throw new RuntimeException(exception); + throw new UncheckedIOException("Failed to read file %s".formatted(file), exception); } } From 03a2870964b703432d9059bba6363c32a5fc153a Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 7 Jul 2025 16:43:15 +0200 Subject: [PATCH 11/25] Enable broken tests --- doc/developer_guide.md | 9 ++++++++- product/pom.xml | 2 +- .../itsallcode/openfasttrace/cli/CliStarterIT.java | 13 +++++++------ .../itsallcode/openfasttrace/cli/JarLauncher.java | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/doc/developer_guide.md b/doc/developer_guide.md index b2c75c12..54f4a043 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/product/pom.xml b/product/pom.xml index 4a325a05..58214bd1 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -127,7 +127,7 @@ -Xlint:all - + -Xlint:-path -Werror diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index b710c97a..ad36fdba 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -59,7 +59,10 @@ void testNoArguments() private void assertExitWithError(final JarLauncher.Builder jarLauncherBuilder, final ExitStatus status, final String message) throws MultipleFailuresError { - jarLauncherBuilder.expectStdOut(startsWith(message)).expectedExitCode(status.getCode()).start(); + jarLauncherBuilder + .expectStdErr(startsWith(message)) + .expectedExitCode(status.getCode()).start() + .waitUntilTerminated(); } // [itest->dsn~cli.command-selection~1] @@ -87,7 +90,7 @@ void testConvertWithoutExplicitInputs() private void assertExitOkWithStdOutStart(final JarLauncher.Builder jarLauncherBuilder, final String outputStart) throws MultipleFailuresError { - jarLauncherBuilder.expectStdOut(startsWith(outputStart)).expectedExitCode(0).start(); + jarLauncherBuilder.expectStdOut(startsWith(outputStart)).expectedExitCode(0).start().waitUntilTerminated(); assertOutputFileExists(false); } @@ -152,9 +155,6 @@ void testConvertDefaultInputDir() @Test void testTraceNoArguments() { - // 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. jarLauncher(TRACE_COMMAND) .currentWorkingDir() .expectedExitCode(1) @@ -188,7 +188,8 @@ void testTraceWithReportVerbosityQuietToStdOut() jarLauncher( TRACE_COMMAND, DOC_DIR.toString(), REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString()) - .expectedExitCode(ExitStatus.OK.getCode()).start(); + .expectedExitCode(ExitStatus.OK.getCode()).start() + .waitUntilTerminated(); assertOutputFileExists(false); } diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index c3d23b91..698b7ac5 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -50,7 +50,7 @@ private static JarLauncher start(final Builder builder) .command(command) .workingDir(builder.workingDir) .redirectErrorStream(false) - .streamLogLevel(Level.INFO) + .streamLogLevel(Level.FINE) .start(); return new JarLauncher(process, builder); } From 887aacacc22a0554048f88823c197784f693b666 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 3 Aug 2025 09:51:04 +0200 Subject: [PATCH 12/25] Simplify JarLauncher API --- .../openfasttrace/cli/CliExitIT.java | 18 ++++-------- .../openfasttrace/cli/CliStarterIT.java | 29 +++++++++++-------- .../openfasttrace/cli/JarLauncher.java | 22 +++++++------- .../testutil/AbstractFileBasedTest.java | 7 ++++- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java index e6160213..953242a6 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliExitIT.java @@ -26,8 +26,7 @@ void testRunWithoutArguments() .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) .expectStdOut(emptyString()) .expectStdErr(equalTo("oft: Missing command\nAdd one of 'help','convert','trace'\n\n")) - .start() - .waitUntilTerminated(); + .verify(); } @Test @@ -39,8 +38,7 @@ void testRunWithUnsupportedCommand() .expectStdOut(emptyString()) .expectStdErr(equalTo( "oft: 'unsupported' is not an OFT command.\nChoose one of 'help','convert','trace'.\n\n")) - .start() - .waitUntilTerminated(); + .verify(); } @Test @@ -55,8 +53,7 @@ void testRunWithHelpCommand() Usage: oft command""")) .expectStdErr(emptyString()) - .start() - .waitUntilTerminated(); + .verify(); } @Test @@ -68,8 +65,7 @@ void testRunWithUnsupportedReporter(@TempDir final Path emptyDir) .expectStdOut(emptyString()) .expectStdErr(startsWith( "Exception in thread \"main\" org.itsallcode.openfasttrace.api.exporter.ExporterException: Found no matching reporter for output format 'unknown'")) - .start() - .waitUntilTerminated(); + .verify(); } @Test @@ -88,8 +84,7 @@ private void assertExitStatusForTracedFiles(final ExitStatus expectedStatus, fin .expectedExitCode(expectedStatus.getCode()) .expectStdErr(emptyString()) .expectStdOut(not(emptyString())) - .start() - .waitUntilTerminated(); + .verify(); } private JarLauncher.Builder jarLauncher() @@ -112,7 +107,6 @@ void testCliExitCode_CliError() .expectedExitCode(ExitStatus.CLI_ERROR.getCode()) .expectStdOut(emptyString()) .expectStdErr(equalTo("oft: Unexpected parameter '--zzzz' is not allowed\n")) - .start() - .waitUntilTerminated(); + .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 index ad36fdba..e4a45d70 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -61,8 +61,8 @@ private void assertExitWithError(final JarLauncher.Builder jarLauncherBuilder, f { jarLauncherBuilder .expectStdErr(startsWith(message)) - .expectedExitCode(status.getCode()).start() - .waitUntilTerminated(); + .expectedExitCode(status.getCode()) + .verify(); } // [itest->dsn~cli.command-selection~1] @@ -90,7 +90,9 @@ void testConvertWithoutExplicitInputs() private void assertExitOkWithStdOutStart(final JarLauncher.Builder jarLauncherBuilder, final String outputStart) throws MultipleFailuresError { - jarLauncherBuilder.expectStdOut(startsWith(outputStart)).expectedExitCode(0).start().waitUntilTerminated(); + jarLauncherBuilder.expectStdOut(startsWith(outputStart)) + .expectedExitCode(0) + .verify(); assertOutputFileExists(false); } @@ -121,8 +123,9 @@ void testConvertToSpecobjectFile() private void assertExitOkWithOutputFileStart(final JarLauncher.Builder jarLauncherBuilder, final String fileStart) throws MultipleFailuresError { - jarLauncherBuilder.expectedExitCode(ExitStatus.OK.getCode()).start() - .waitUntilTerminated(Duration.ofSeconds(10)); + jarLauncherBuilder.expectedExitCode(ExitStatus.OK.getCode()) + .timeout(Duration.ofSeconds(10)) + .verify(); assertAll( () -> assertOutputFileExists(true), () -> assertOutputFileContentStartsWith(fileStart)); @@ -159,8 +162,7 @@ void testTraceNoArguments() .currentWorkingDir() .expectedExitCode(1) .expectStdOut(containsString("not ok - 43 total, 43 defect")) - .start() - .waitUntilTerminated(); + .verify(); } // [itest->dsn~cli.command-selection~1] @@ -188,8 +190,8 @@ void testTraceWithReportVerbosityQuietToStdOut() jarLauncher( TRACE_COMMAND, DOC_DIR.toString(), REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString()) - .expectedExitCode(ExitStatus.OK.getCode()).start() - .waitUntilTerminated(); + .expectedExitCode(ExitStatus.OK.getCode()) + .verify(); assertOutputFileExists(false); } @@ -210,8 +212,8 @@ void testTraceDefaultInputDir() jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString()) .expectStdOut(emptyString()) .expectedExitCode(1) - .start() - .waitUntilTerminated(Duration.ofSeconds(10)); + .timeout(Duration.ofSeconds(10)) + .verify(); assertOutputFileExists(true); } @@ -282,7 +284,10 @@ void testTraceDefaultNewlines() private void assertExitWithStatus(final int code, final String... args) { - jarLauncher().args(List.of(args)).expectedExitCode(code).start().waitUntilTerminated(); + jarLauncher() + .args(List.of(args)) + .expectedExitCode(code) + .verify(); } private void assertPlatformNewlines() diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index 698b7ac5..23e94ad4 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -39,7 +39,7 @@ private static JarLauncher start(final Builder builder) .formatted(jarPath)); } final List command = new ArrayList<>(); - command.addAll(javaLaunchArgs(jarPath, builder.mainClass)); + command.addAll(createJavaLaunchArgs(jarPath, builder.mainClass)); if (builder.args != null) { command.addAll(builder.args); @@ -55,7 +55,7 @@ private static JarLauncher start(final Builder builder) return new JarLauncher(process, builder); } - private static List javaLaunchArgs(final Path jarPath, final Class mainClass) + private static List createJavaLaunchArgs(final Path jarPath, final Class mainClass) { final String javaExecutable = getJavaExecutable().toString(); if (mainClass == null) @@ -85,12 +85,7 @@ private static Path getJavaExecutable() .orElseThrow(() -> new IllegalStateException("Java executable not found")); } - public void waitUntilTerminated() - { - waitUntilTerminated(Duration.ofSeconds(3)); - } - - public void waitUntilTerminated(final Duration timeout) + private void waitUntilTerminated(final Duration timeout) { LOG.fine("Waiting %s for process %d to terminate...".formatted(timeout, process.pid())); process.waitForTermination(timeout); @@ -123,6 +118,7 @@ public static final class Builder private int expectedExitCode = 0; private Matcher expectedStdOut; private Matcher expectedStdErr; + private Duration timeout = Duration.ofSeconds(3); private Class mainClass; private Builder() @@ -158,6 +154,12 @@ public Builder args(final List args) return this; } + public Builder timeout(final Duration timeout) + { + this.timeout = timeout; + return this; + } + public Builder successExitCode() { return this.expectedExitCode(0); @@ -181,9 +183,9 @@ public Builder expectStdErr(final Matcher expectedStdErr) return this; } - public JarLauncher start() + public void verify() { - return JarLauncher.start(this); + JarLauncher.start(this).waitUntilTerminated(this.timeout); } } 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 568e9928..58baee52 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/testutil/AbstractFileBasedTest.java @@ -25,6 +25,11 @@ protected void writeTextFile(final File file, final String content) throws IOExc @SuppressWarnings("javadoc") protected void assertStdOut(final List args, final Matcher stdOutMatcher) { - JarLauncher.builder().args(args).currentWorkingDir().expectStdOut(stdOutMatcher).expectedExitCode(0).start(); + JarLauncher.builder() + .args(args) + .currentWorkingDir() + .expectStdOut(stdOutMatcher) + .expectedExitCode(0) + .verify(); } } From b8ddf25b71a89654aa76968b3549f3a55852f4d2 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 3 Aug 2025 09:57:35 +0200 Subject: [PATCH 13/25] Fix sonar warnings --- .../org/itsallcode/openfasttrace/core/cli/CliException.java | 2 +- .../java/org/itsallcode/openfasttrace/core/cli/CliStarter.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 c8676ebc..69d3fd0f 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 fea9ccde..ef2202ba 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 @@ -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) { From 6bf24e9a74f867acac2795a6cc40914919f526c4 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:29:05 +0100 Subject: [PATCH 14/25] Adapt integration test under windows --- .github/workflows/build.yml | 2 +- .../org/itsallcode/openfasttrace/core/cli/CliStarter.java | 1 + parent/pom.xml | 5 +++-- .../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a451b2c..3d3422e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - os: ubuntu-latest java: 21 - os: ubuntu-latest - java: 24 + java: 25 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-os-${{ matrix.os }}-java-${{ matrix.java }} 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 ef2202ba..a94a433d 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 @@ -127,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/parent/pom.xml b/parent/pom.xml index e17feecb..7a530c6f 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -245,20 +245,21 @@ org.itsallcode simple-process - 0.2.0 + 0.3.1 test + java21 21 java.version 21 diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index e4a45d70..91d27cbc 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -76,7 +76,7 @@ void testIllegalCommand() @Test void testHelpPrintsUsage() { - final String nl = System.lineSeparator(); + final String nl = "\n"; assertExitOkWithStdOutStart(jarLauncher(HELP_COMMAND), "OpenFastTrace" + nl + nl + "Usage:"); } From 515b3811016baefd4f98f7e0765ba0189afa2405 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:49:20 +0100 Subject: [PATCH 15/25] Fix mockito agent warning --- parent/pom.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index 7a530c6f..17646b6d 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -404,6 +404,19 @@ + + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + + properties + + + + org.apache.maven.plugins maven-surefire-plugin @@ -415,7 +428,7 @@ true - ${test.args} + ${test.args} -javaagent:${org.mockito:mockito-core:jar} @@ -429,7 +442,7 @@ true true - ${test.args} + ${test.args} -javaagent:${org.mockito:mockito-core:jar} From a739ccc3db8fdcc4755b0fd09838c65d641c7036 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:49:40 +0100 Subject: [PATCH 16/25] Define ossindex credentials --- .github/workflows/build.yml | 5 +++++ parent/pom.xml | 3 +++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d3422e1..25cb4644 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,6 +45,8 @@ jobs: distribution: "temurin" java-version: ${{ matrix.java }} cache: "maven" + server-username: OSSINDEX_USERNAME + server-password: OSSINDEX_TOKEN - name: Cache SonarQube packages if: ${{ env.DEFAULT_OS == matrix.os && env.DEFAULT_JAVA == matrix.java }} @@ -58,6 +60,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/parent/pom.xml b/parent/pom.xml index 17646b6d..ca82b1fe 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -511,6 +511,9 @@ org.sonatype.ossindex.maven ossindex-maven-plugin 3.2.0 + + ossindex + audit From 87a916f3ee5b27a660afa6fd00de8112709f39cd Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:52:49 +0100 Subject: [PATCH 17/25] Upgrade dependencies --- parent/pom.xml | 55 +++++++++++++++----------------------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index ca82b1fe..b81e5735 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} @@ -250,33 +250,6 @@ - - - - java21 - - 21 - - - java.version - 21 - - - - - - -Djava.security.manager=allow -XX:+EnableDynamicAgentLoading - - - @@ -322,7 +295,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 ${java.version} ${java.version} @@ -457,7 +429,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.0 + 1.7.3 oss @@ -465,7 +437,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.2 enforce-maven @@ -527,7 +499,7 @@ org.apache.maven.plugins maven-artifact-plugin - 3.6.0 + 3.6.1 verify-reproducible-build @@ -544,7 +516,7 @@ org.codehaus.mojo versions-maven-plugin - 2.18.0 + 2.19.1 @@ -561,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 @@ -576,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 @@ -596,7 +573,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.12.0 org.codehaus.mojo @@ -667,7 +644,7 @@ net.sourceforge.plantuml plantuml - 1.2025.0 + 1.2025.10 From 4fdeeaefae8701658eddaa18bbcdbad9c7fc3928 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:54:23 +0100 Subject: [PATCH 18/25] Upgrade test dependencies --- parent/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index b81e5735..69bdf194 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -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 @@ -239,7 +239,7 @@ com.exasol maven-project-version-getter - 1.2.1 + 1.2.2 test From dfc1e173f15641fe9d7c855d9c4b5b9df1663830 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 20:56:07 +0100 Subject: [PATCH 19/25] Fix ossindex credentials --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25cb4644..3d3e1147 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,6 +45,7 @@ jobs: distribution: "temurin" java-version: ${{ matrix.java }} cache: "maven" + server-id: ossindex server-username: OSSINDEX_USERNAME server-password: OSSINDEX_TOKEN From b4054f10dd8ff5a13326dead0c96b5d9d7e2e540 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 21:10:23 +0100 Subject: [PATCH 20/25] Add debug output for failing test --- .../org/itsallcode/openfasttrace/cli/CliStarterIT.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index 91d27cbc..b91460db 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -1,5 +1,6 @@ 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; @@ -298,7 +299,8 @@ private void assertPlatformNewlines() private void assertNoOffendingNewlines() { - switch (System.lineSeparator()) + final String systemLineSeparator = System.lineSeparator(); + switch (systemLineSeparator) { case NEWLINE: assertThat("Has no carriage returns", getOutputFileContent().contains(CARRIAGE_RETURN), @@ -312,7 +314,10 @@ private void assertNoOffendingNewlines() getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false)); break; default: - fail("Unsupported line separator"); + final String hexCode = systemLineSeparator.chars() + .mapToObj(c -> String.format("\\u%04x", c)) + .collect(joining()); + fail("Unsupported line separator '%s' (hex: %s)".formatted(systemLineSeparator, hexCode)); } } From 0eb1cc7ce6f260f8c150d830afee5c7e02e6d202 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 21:13:07 +0100 Subject: [PATCH 21/25] Add helpful output to assertion message --- .../java/org/itsallcode/openfasttrace/cli/JarLauncher.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index 23e94ad4..a996a9fd 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -91,7 +91,10 @@ private void waitUntilTerminated(final Duration timeout) 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", exitValue, equalTo(builder.expectedExitCode)), + assertAll( + () -> assertThat( + "exit code (std out: %s, std err: %s)".formatted(process.getStdOut(), process.getStdErr()), + exitValue, equalTo(builder.expectedExitCode)), () -> { if (builder.expectedStdOut != null) { From 1cc04a985e6bc04ba6daa5a95f447f18555f98a7 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 9 Nov 2025 21:17:00 +0100 Subject: [PATCH 22/25] Fix integration test for windows --- .../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java index b91460db..b2f7f84f 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -313,11 +313,15 @@ private void assertNoOffendingNewlines() 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' (hex: %s)".formatted(systemLineSeparator, hexCode)); + fail("Unsupported line separator '%s' (%s)".formatted(systemLineSeparator, hexCode)); } } From 545e206d677a2a3caa623a1981af6a2653e44c0e Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 16 Nov 2025 10:37:05 +0100 Subject: [PATCH 23/25] Implement review findings --- .../openfasttrace/cli/JarLauncher.java | 109 +++++++++++++----- 1 file changed, 78 insertions(+), 31 deletions(-) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java index a996a9fd..bf7b2cb1 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/JarLauncher.java @@ -7,7 +7,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -17,6 +18,9 @@ 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()); @@ -31,7 +35,7 @@ private JarLauncher(final SimpleProcess process, final Builder builder) private static JarLauncher start(final Builder builder) { - final Path jarPath = getExecutableJar(builder.jarNameTemplate); + final Path jarPath = getExecutableJarPath(); if (!Files.exists(jarPath)) { throw new IllegalStateException( @@ -39,7 +43,7 @@ private static JarLauncher start(final Builder builder) .formatted(jarPath)); } final List command = new ArrayList<>(); - command.addAll(createJavaLaunchArgs(jarPath, builder.mainClass)); + command.addAll(createJavaLaunchArgs(jarPath)); if (builder.args != null) { command.addAll(builder.args); @@ -55,22 +59,16 @@ private static JarLauncher start(final Builder builder) return new JarLauncher(process, builder); } - private static List createJavaLaunchArgs(final Path jarPath, final Class mainClass) + private static List createJavaLaunchArgs(final Path jarPath) { final String javaExecutable = getJavaExecutable().toString(); - if (mainClass == null) - { - return List.of(javaExecutable, "-jar", jarPath.toString()); - } - return List.of(javaExecutable, "-classpath", jarPath.toString(), mainClass.getName()); + return List.of(javaExecutable, "-jar", jarPath.toString()); } - private static Path getExecutableJar(final String jarNameTemplate) + private static Path getExecutableJarPath() { - return Path.of("target") - .resolve(Objects.requireNonNull(jarNameTemplate, "jarNameTemplate") - .formatted(getCurrentProjectVersion())) - .toAbsolutePath(); + final String jarFileName = "openfasttrace-%s.jar".formatted(getCurrentProjectVersion()); + return Path.of("target").resolve(jarFileName).toAbsolutePath(); } private static String getCurrentProjectVersion() @@ -85,7 +83,7 @@ private static Path getJavaExecutable() .orElseThrow(() -> new IllegalStateException("Java executable not found")); } - private void waitUntilTerminated(final Duration timeout) + private void assertExpectationsAfterTerminated(final Duration timeout) { LOG.fine("Waiting %s for process %d to terminate...".formatted(timeout, process.pid())); process.waitForTermination(timeout); @@ -108,88 +106,137 @@ exitValue, equalTo(builder.expectedExitCode)), }); } + /** + * 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 String jarNameTemplate = "openfasttrace-%s.jar"; private Path workingDir; private List args; private int expectedExitCode = 0; private Matcher expectedStdOut; private Matcher expectedStdErr; private Duration timeout = Duration.ofSeconds(3); - private Class mainClass; private Builder() { } - public Builder jarNameTemplate(final String jarNameTemplate) - { - this.jarNameTemplate = jarNameTemplate; - return this; - } - - public Builder mainClass(final Class mainClass) - { - this.mainClass = mainClass; - return this; - } - + /** + * 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).waitUntilTerminated(this.timeout); + JarLauncher.start(this).assertExpectationsAfterTerminated(this.timeout); } } - } From b94462274c071e817139f90031516407eb61e992 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 16 Nov 2025 10:37:16 +0100 Subject: [PATCH 24/25] Fix vscode setup --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index d878e354..af5d0572 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", } From 40efb83c80cadfab3c3b71fb734a569b899a0524 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 16 Nov 2025 10:41:58 +0100 Subject: [PATCH 25/25] Get aggregated dependency update PRs --- .github/dependabot.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 954f95ce..3a9b7f76 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"