getWantedTags()
{
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 44bd33311..ecf52325a 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
@@ -1,7 +1,5 @@
package org.itsallcode.openfasttrace.core.cli;
-import java.util.Optional;
-
import org.itsallcode.openfasttrace.api.cli.DirectoryService;
import org.itsallcode.openfasttrace.core.cli.commands.*;
import org.itsallcode.openfasttrace.core.cli.logging.LoggingConfigurator;
@@ -36,7 +34,7 @@ public CliStarter(final CliArguments arguments)
public static void main(final String[] args)
{
final DirectoryService directoryService = new StandardDirectoryService();
- main(args, directoryService);
+ mainDelegate(args, directoryService);
}
/**
@@ -49,7 +47,7 @@ public static void main(final String[] args)
* directory service for getting the current directory. This
* allows injecting a mock in unit tests.
*/
- public static void main(final String[] args, final DirectoryService directoryService)
+ public static void mainDelegate(final String[] args, final DirectoryService directoryService)
{
final CliArguments arguments = parseCommandLineArguments(args, directoryService);
final ArgumentValidator validator = new ArgumentValidator(arguments);
diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
index 997bc8d97..cf1159049 100644
--- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
+++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
@@ -12,7 +12,7 @@
/**
* This class implements an interpreter for command line arguments
- *
+ * dsn~cli.tracing.output-format~1]]
+ @Test
void testTraceOutputFormatPlain()
{
assertExitOkWithOutputFileOfLength(jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER,
From 095033c2d2b1b11682a207a732ed0e7d0c5d9a5e Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 18:11:17 +0200
Subject: [PATCH 3/8] #503: Fixed JavaDoc and comments.
---
.../openfasttrace/core/cli/CliArguments.java | 12 ++++++------
.../core/cli/CommandLineInterpreter.java | 5 +++--
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java
index 464f3a690..3848206e9 100644
--- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java
+++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java
@@ -288,7 +288,7 @@ public void setH(final boolean helpSet)
}
/**
- * Get a list of artifact types to be applied as filter during import
+ * Get a list of artifact types to be applied as a filter during import
*
* @return set of wanted artifact types
*/
@@ -298,7 +298,7 @@ public Set getWantedArtifactTypes()
}
/**
- * Set a list of artifact types to be applied as filter during import
+ * Set a list of artifact types to be applied as a filter during import
*
* @param artifactTypes
* list of wanted artifact types
@@ -314,7 +314,7 @@ private HashSet createSetFromCommaSeparatedString(final String commaSepa
}
/**
- * Set a list of artifact types to be applied as filter during import
+ * Set a list of artifact types to be applied as a filter during import
*
* @param artifactTypes
* list of wanted artifact types
@@ -325,7 +325,7 @@ public void setA(final String artifactTypes)
}
/**
- * Get a list of tags to be applied as filter during import
+ * Get a list of tags to be applied as a filter during import
*
* @return set of wanted tags
*/
@@ -370,7 +370,7 @@ public DetailsSectionDisplay getDetailsSectionDisplay()
}
/**
- * Set a list of tags to be applied as filter during import
+ * Set a list of tags to be applied as a filter during import
*
* @param tags
* list of wanted tags
@@ -381,7 +381,7 @@ public void setWantedTags(final String tags)
}
/**
- * Set a list of tags to be applied as filter during import
+ * Set a list of tags to be applied as a filter during import
*
* @param tags
* list of wanted tags
diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
index cf1159049..87760465a 100644
--- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
+++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CommandLineInterpreter.java
@@ -12,12 +12,13 @@
/**
* This class implements an interpreter for command line arguments
- *
* Users of this class must create a POJO that contains a setter method for each
* command line argument that they want to use.
- *
* Additionally, they can add a setter called setUnnamedValues that
* will receive all argument values that are unnamed.
+ *
*/
public class CommandLineInterpreter
{
From 95df7bf148267f180f1ec1ba7ba2ddfcdf2b55f9 Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 18:25:24 +0200
Subject: [PATCH 4/8] #503: Limited scope of plain output test.
---
.../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 2 +-
1 file changed, 1 insertion(+), 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 854e4a722..9ca5aa8e7 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
@@ -245,7 +245,7 @@ private void assertOutputFileLength(final int length)
@Test
void testTraceOutputFormatPlain()
{
- assertExitOkWithOutputFileOfLength(jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER,
+ assertExitOkWithOutputFileOfLength(jarLauncher(TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER,
this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"), 1000);
}
From e11011f7963ec2a5ccef31de465f0a2d18bfa2f1 Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 18:31:09 +0200
Subject: [PATCH 5/8] #503: Fixed plain assertion.
---
.../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 4 ++--
1 file changed, 2 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 9ca5aa8e7..babdc3198 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
@@ -245,8 +245,8 @@ private void assertOutputFileLength(final int length)
@Test
void testTraceOutputFormatPlain()
{
- assertExitOkWithOutputFileOfLength(jarLauncher(TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER,
- this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"), 1000);
+ assertExitOkWithOutputFileStart(jarLauncher(TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER,
+ this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"), "ok - 5 total");
}
@Test
From 7746794587dcb0eb7ed85f8cda1b38825388d913 Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 21:19:02 +0200
Subject: [PATCH 6/8] #503: Pulled `exit` all the way up to the main method to
enable unit testing.
---
.../openfasttrace/core/cli/CliStarter.java | 27 +++++++++----------
1 file changed, 12 insertions(+), 15 deletions(-)
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 ecf52325a..0c2eca02f 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
@@ -34,7 +34,7 @@ public CliStarter(final CliArguments arguments)
public static void main(final String[] args)
{
final DirectoryService directoryService = new StandardDirectoryService();
- mainDelegate(args, directoryService);
+ exit(mainDelegate(args, directoryService));
}
/**
@@ -46,21 +46,22 @@ public static void main(final String[] args)
* @param directoryService
* directory service for getting the current directory. This
* allows injecting a mock in unit tests.
+ * @return exit status of the command.
*/
- public static void mainDelegate(final String[] args, final DirectoryService directoryService)
+ public static ExitStatus mainDelegate(final String[] args, final DirectoryService directoryService)
{
final CliArguments arguments = parseCommandLineArguments(args, directoryService);
final ArgumentValidator validator = new ArgumentValidator(arguments);
if (validator.isValid())
{
LoggingConfigurator.create(arguments).configureLogging();
- new CliStarter(arguments).run();
+ return new CliStarter(arguments).run();
}
else
{
printToStdError(
"oft: " + validator.getError() + "\n" + validator.getSuggestion() + "\n");
- exit(CLI_ERROR);
+ return CLI_ERROR;
}
}
@@ -90,33 +91,29 @@ private static void printToStdError(final String message)
/**
* Process the command line arguments and execute the commands.
+ *
+ * @return the exit status of the command.
*/
// [impl->dsn~cli.command-selection~1]
- public void run()
+ public ExitStatus run()
{
final String command = this.arguments.isHelpSet()
? HelpCommand.COMMAND_NAME
: this.arguments.getCommand().orElse(MISSING_COMMAND);
- ExitStatus exitStatus;
switch (command)
{
case ConvertCommand.COMMAND_NAME:
- exitStatus = new ConvertCommand(this.arguments).run() ? OK : FAILURE;
- break;
+ return new ConvertCommand(this.arguments).run() ? OK : FAILURE;
case TraceCommand.COMMAND_NAME:
- exitStatus = new TraceCommand(this.arguments).run() ? OK : FAILURE;
- break;
+ return new TraceCommand(this.arguments).run() ? OK : FAILURE;
case HelpCommand.COMMAND_NAME:
- exitStatus = new HelpCommand(true).run() ? OK : FAILURE;
- break;
+ return new HelpCommand(true).run() ? OK : FAILURE;
case MISSING_COMMAND:
default:
new HelpCommand(false).run();
printToStdError("Compand missing trying to execute OFT. Choose one of: trace, convert, help");
- exitStatus = CLI_ERROR;
- break;
+ return CLI_ERROR;
}
- exit(exitStatus);
}
// [impl->dsn~cli.tracing.exit-status~1]
From 9cee6046bc757619f3b4597fde2fd1ac1b1e6a55 Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 21:38:12 +0200
Subject: [PATCH 7/8] #503: Pulled exception handling up.
---
.../openfasttrace/core/cli/CliStarter.java | 37 ++++++++-----------
1 file changed, 15 insertions(+), 22 deletions(-)
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 0c2eca02f..f7215a755 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
@@ -50,35 +50,28 @@ public static void main(final String[] args)
*/
public static ExitStatus mainDelegate(final String[] args, final DirectoryService directoryService)
{
- final CliArguments arguments = parseCommandLineArguments(args, directoryService);
- final ArgumentValidator validator = new ArgumentValidator(arguments);
- if (validator.isValid())
- {
- LoggingConfigurator.create(arguments).configureLogging();
- return new CliStarter(arguments).run();
+ try {
+ final CliArguments arguments = parseCommandLineArguments(args, directoryService);
+ final ArgumentValidator validator = new ArgumentValidator(arguments);
+ if (validator.isValid()) {
+ LoggingConfigurator.create(arguments).configureLogging();
+ return new CliStarter(arguments).run();
+ } else {
+ printToStdError(
+ "oft: " + validator.getError() + "\n" + validator.getSuggestion() + "\n");
+ return CLI_ERROR;
+ }
}
- else
- {
- printToStdError(
- "oft: " + validator.getError() + "\n" + validator.getSuggestion() + "\n");
+ catch (final CliException e) {
+ printToStdError("oft: " + e.getMessage());
return CLI_ERROR;
}
}
- @SuppressWarnings("java:S1166") // Exceptions are reported to the user
private static CliArguments parseCommandLineArguments(final String[] args,
- final DirectoryService directoryService)
- {
+ final DirectoryService directoryService) throws CliException {
final CliArguments arguments = new CliArguments(directoryService);
- try
- {
- new CommandLineInterpreter(args, arguments).parse();
- }
- catch (final CliException e)
- {
- printToStdError("oft: " + e.getMessage());
- exit(CLI_ERROR);
- }
+ new CommandLineInterpreter(args, arguments).parse();
return arguments;
}
From eb7be00f4036d6d9555193c60e652f379c26b8c3 Mon Sep 17 00:00:00 2001
From: redcatbaer
Date: Sat, 23 May 2026 22:08:14 +0200
Subject: [PATCH 8/8] #503: Moved CLIStarter tests to a class that tests the
delegate to be able to record code coverage.
---
doc/changes/changes_4.5.0.md | 1 +
.../openfasttrace/cli/CliStarterIT.java | 340 +--------------
.../cli/CliStarterInternalIT.java | 390 ++++++++++++++++++
3 files changed, 400 insertions(+), 331 deletions(-)
create mode 100644 product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterInternalIT.java
diff --git a/doc/changes/changes_4.5.0.md b/doc/changes/changes_4.5.0.md
index 987323838..2d1eaf204 100644
--- a/doc/changes/changes_4.5.0.md
+++ b/doc/changes/changes_4.5.0.md
@@ -6,6 +6,7 @@ Code name: ???
In this release we added the a `-h` / `--help` to the command line.
+We also refactored the tests around the CLI starter to improve readability and maintainability and made getting the test coverage easier.
## Features
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 babdc3198..c3d71758c 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java
@@ -1,57 +1,25 @@
package org.itsallcode.openfasttrace.cli;
-import static java.util.stream.Collectors.joining;
-import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.fail;
-import java.io.IOException;
-import java.io.UncheckedIOException;
import java.nio.file.*;
-import java.time.Duration;
import java.util.List;
-import org.itsallcode.junit.sysextensions.SystemErrGuard;
-import org.itsallcode.junit.sysextensions.SystemOutGuard;
import org.itsallcode.openfasttrace.cli.JarLauncher.Builder;
import org.itsallcode.openfasttrace.core.cli.ExitStatus;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.io.TempDir;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
import org.opentest4j.MultipleFailuresError;
-@ExtendWith(SystemOutGuard.class)
-@ExtendWith(SystemErrGuard.class)
+/**
+ * This integration test was reduced to a minimal smoke test.
+ * The actual tests are in {@link CliStarterInternalIT}, which makes recording code coverage easier.
+ *
+ * @see CliStarterInternalIT
+ */
// [itest->dsn~cli.tracing.exit-status~1]
class CliStarterIT
{
- private static final String SPECOBJECT_PREAMBLE = "\n";
- private static final String ILLEGAL_COMMAND = "illegal";
- private static final String NEWLINE_PARAMETER = "--newline";
private static final String HELP_COMMAND = "help";
- private static final String CONVERT_COMMAND = "convert";
- private static final String TRACE_COMMAND = "trace";
- private static final String OUTPUT_FILE_PARAMETER = "--output-file";
- private static final String REPORT_VERBOSITY_PARAMETER = "--report-verbosity";
- private static final String OUTPUT_FORMAT_PARAMETER = "--output-format";
- private static final String WANTED_ARTIFACT_TYPES_PARAMETER = "--wanted-artifact-types";
- private static final String COLOR_SCHEME_PARAMETER = "--color-scheme";
- private static final String CARRIAGE_RETURN = "\r";
- private static final String NEWLINE = "\n";
-
- private static final Path DOC_DIR = Paths.get("../core/src/test/resources/markdown").toAbsolutePath();
-
- private Path outputFile;
-
- @BeforeEach
- void beforeEach(@TempDir final Path tempDir)
- {
- this.outputFile = tempDir.resolve("stream.txt");
- }
@Test
void testNoArguments()
@@ -68,304 +36,14 @@ private void assertExitWithError(final JarLauncher.Builder jarLauncherBuilder, f
.verify();
}
- // [itest->dsn~cli.command-selection~1]
@Test
- void testIllegalCommand()
- {
- assertExitWithError(jarLauncher(ILLEGAL_COMMAND), ExitStatus.CLI_ERROR,
- "oft: '" + ILLEGAL_COMMAND + "' is not an OFT command.");
- }
-
- @ValueSource(strings = {HELP_COMMAND, "-h", "--help"})
- @ParameterizedTest
- void testHelpPrintsUsage(final String command)
+ void testHelpPrintsUsage()
{
final String nl = "\n";
- assertExitOkWithStdOutStart(jarLauncher(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))
+ jarLauncher(HELP_COMMAND)
+ .expectStdOut(startsWith("OpenFastTrace" + nl + nl + "Usage:"))
.expectedExitCode(0)
.verify();
- assertOutputFileExists(false);
- }
-
- @Test
- void testConvertUnknownExporter()
- {
- final Builder jarLauncherBuilder = jarLauncher(
- CONVERT_COMMAND, DOC_DIR.toString(),
- OUTPUT_FORMAT_PARAMETER, "illegal",
- OUTPUT_FILE_PARAMETER, this.outputFile.toString());
- assertExitWithError(jarLauncherBuilder, ExitStatus.CLI_ERROR,
- "oft: export format 'illegal' is not supported.");
- }
-
- // [itest->dsn~cli.conversion.output-format~1]
- @Test
- void testConvertToSpecobjectFile()
- {
- final Builder jarLauncherBuilder = jarLauncher( //
- CONVERT_COMMAND, DOC_DIR.toString(), //
- OUTPUT_FORMAT_PARAMETER, "specobject", //
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(), //
- COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE");
- assertExitOkWithOutputFileStart(jarLauncherBuilder,
- SPECOBJECT_PREAMBLE + "\n assertOutputFileExists(true),
- () -> assertOutputFileContentStartsWith(fileStart));
- }
-
- // [itest->dsn~cli.conversion.default-output-format~1]
- @Test
- void testConvertDefaultOutputFormat()
- {
- assertExitOkWithStdOutStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString()), SPECOBJECT_PREAMBLE);
- }
-
- // [itest->dsn~cli.input-file-selection~1]
- @Test
- void testConvertDefaultOutputFormatIntoFile()
- {
- assertExitOkWithOutputFileStart(jarLauncher(CONVERT_COMMAND, DOC_DIR.toString(),
- OUTPUT_FILE_PARAMETER, this.outputFile.toString()), SPECOBJECT_PREAMBLE);
- }
-
- // [itest->dsn~cli.default-input~1]
- @Test
- void testConvertDefaultInputDir()
- {
- assertExitOkWithOutputFileOfLength(jarLauncher(
- CONVERT_COMMAND,
- OUTPUT_FILE_PARAMETER, this.outputFile.toString()), 2000);
- }
-
- @Test
- void testTraceNoArguments()
- {
- jarLauncher(TRACE_COMMAND)
- .currentWorkingDir()
- .expectedExitCode(1)
- .expectStdOut(containsString("not ok[0m - 43 total, 43 defect"))
- .verify();
- }
-
- // [itest->dsn~cli.command-selection~1]
- @Test
- void testTrace()
- {
- assertExitOkWithOutputFileStart(jarLauncher(
- TRACE_COMMAND,
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- DOC_DIR.toString()), "ok - 5 total");
- }
-
- @Test
- void testTraceWithReportVerbosityMinimal()
- {
- assertExitOkWithOutputFileStart(jarLauncher(
- TRACE_COMMAND, DOC_DIR.toString(),
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- REPORT_VERBOSITY_PARAMETER, "MINIMAL"), "ok");
- }
-
- @Test
- void testTraceWithReportVerbosityQuietToStdOut()
- {
- jarLauncher(
- TRACE_COMMAND, DOC_DIR.toString(),
- REPORT_VERBOSITY_PARAMETER, "QUIET").expectStdOut(emptyString())
- .expectedExitCode(ExitStatus.OK.getCode())
- .verify();
- assertOutputFileExists(false);
- }
-
- @Test
- void testTraceWithReportVerbosityQuietToFileMustBeRejected()
- {
- jarLauncher(
- TRACE_COMMAND, DOC_DIR.toString(),
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- REPORT_VERBOSITY_PARAMETER, "QUIET").expectedExitCode(ExitStatus.CLI_ERROR.getCode())
- .expectStdErr(equalTo("oft: combining stream"));
- }
-
- @Test
- // [itest->dsn~cli.default-input~1]
- void testTraceDefaultInputDir()
- {
- jarLauncher(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString())
- .expectStdOut(emptyString())
- .expectedExitCode(1)
- .timeout(Duration.ofSeconds(10))
- .verify();
- assertOutputFileExists(true);
- }
-
- @Test
- void testBasicHtmlTrace()
- {
- assertExitOkWithStdOutStart(jarLauncher(
- TRACE_COMMAND, DOC_DIR.toString(),
- OUTPUT_FORMAT_PARAMETER, "html"), "");
- }
-
- private void assertExitOkWithOutputFileOfLength(final JarLauncher.Builder jarLauncherBuilder, final int length)
- {
- assertAll(
- () -> assertExitOkWithOutputFileStart(jarLauncherBuilder, SPECOBJECT_PREAMBLE),
- () -> assertOutputFileLength(length));
- }
-
- private void assertOutputFileLength(final int length)
- {
- assertThat(getOutputFileContent().length(), greaterThan(length));
- }
-
- // [itest->dsn~cli.tracing.output-format~1]]
- @Test
- void testTraceOutputFormatPlain()
- {
- assertExitOkWithOutputFileStart(jarLauncher(TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER,
- this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain"), "ok - 5 total");
- }
-
- @Test
- void testTraceMacNewlines()
- {
- assertAll( //
- () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND,
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- NEWLINE_PARAMETER, "OLDMAC",
- DOC_DIR.toString()),
- () -> assertOutputFileExists(true),
- this::assertOutputFileContainsOldMacNewlines,
- this::assertOutputFileContainsNoUnixNewlines);
- }
-
- private void assertOutputFileContainsOldMacNewlines()
- {
- assertThat("Has old Mac newlines", getOutputFileContent().contains(CARRIAGE_RETURN),
- equalTo(true));
- }
-
- private void assertOutputFileContainsNoUnixNewlines()
- {
- assertThat("Has no Unix newlines", getOutputFileContent().contains(NEWLINE),
- equalTo(false));
- }
-
- @Test
- // [itest->dsn~cli.default-newline-format~1]
- void testTraceDefaultNewlines()
- {
- assertAll(
- () -> assertExitWithStatus(ExitStatus.OK.getCode(), TRACE_COMMAND,
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- DOC_DIR.toString()),
- () -> assertOutputFileExists(true),
- this::assertPlatformNewlines,
- this::assertNoOffendingNewlines);
- }
-
- private void assertExitWithStatus(final int code, final String... args)
- {
- jarLauncher()
- .args(List.of(args))
- .expectedExitCode(code)
- .verify();
- }
-
- private void assertPlatformNewlines()
- {
- assertThat("Has native platform line separator",
- getOutputFileContent().contains(System.lineSeparator()), equalTo(true));
- }
-
- private void assertNoOffendingNewlines()
- {
- final String systemLineSeparator = System.lineSeparator();
- switch (systemLineSeparator)
- {
- case NEWLINE:
- assertThat("Has no carriage returns", getOutputFileContent().contains(CARRIAGE_RETURN),
- equalTo(false));
- break;
- case CARRIAGE_RETURN:
- assertThat("Has no newlines", getOutputFileContent().contains(NEWLINE), equalTo(false));
- break;
- case NEWLINE + CARRIAGE_RETURN:
- assertThat("Has no newline without carriage return and vice-versa",
- getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false));
- break;
- case CARRIAGE_RETURN + NEWLINE:
- assertThat("Has no carriage return without newline and vice-versa",
- getOutputFileContent().matches("\r[^\n]|[^\r]\n"), equalTo(false));
- break;
- default:
- final String hexCode = systemLineSeparator.chars()
- .mapToObj(c -> String.format("\\u%04x", c))
- .collect(joining());
- fail("Unsupported line separator '%s' (%s)".formatted(systemLineSeparator, hexCode));
- }
- }
-
- @Test
- void testTraceWithFilteredArtifactType()
- {
- assertExitOkWithOutputFileStart(jarLauncher(
- TRACE_COMMAND, DOC_DIR.toString(),
- OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
- WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req"), "ok - 3 total");
- }
-
- private void assertOutputFileContentStartsWith(final String content)
- {
- assertThat(getOutputFileContent(), startsWith(content));
- }
-
- private void assertOutputFileExists(final boolean fileExists)
- {
- assertThat("Output file %s exists".formatted(this.outputFile), Files.exists(this.outputFile),
- equalTo(fileExists));
- }
-
- private String getOutputFileContent()
- {
- final Path file = this.outputFile;
- if (!Files.exists(file))
- {
- throw new AssertionError("Expected output file %s does not exist".formatted(file));
- }
- try
- {
- return Files.readString(file);
- }
- catch (final IOException exception)
- {
- // Need to convert this to an unchecked exception. Otherwise, we get
- // stuck with the checked exceptions in the assertion lambdas.
- throw new UncheckedIOException("Failed to read file %s".formatted(file), exception);
- }
}
private Builder jarLauncher(final String... arguments)
diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterInternalIT.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterInternalIT.java
new file mode 100644
index 000000000..3d4046944
--- /dev/null
+++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterInternalIT.java
@@ -0,0 +1,390 @@
+package org.itsallcode.openfasttrace.cli;
+
+import static java.util.stream.Collectors.joining;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.itsallcode.openfasttrace.core.cli.CliStarter;
+import org.itsallcode.openfasttrace.core.cli.ExitStatus;
+import org.itsallcode.openfasttrace.core.cli.StandardDirectoryService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class CliStarterInternalIT {
+ private static final String SPECOBJECT_PREAMBLE = "\n";
+ private static final String ILLEGAL_COMMAND = "illegal";
+ private static final String NEWLINE_PARAMETER = "--newline";
+ private static final String HELP_COMMAND = "help";
+ private static final String CONVERT_COMMAND = "convert";
+ private static final String TRACE_COMMAND = "trace";
+ private static final String OUTPUT_FILE_PARAMETER = "--output-file";
+ private static final String REPORT_VERBOSITY_PARAMETER = "--report-verbosity";
+ private static final String OUTPUT_FORMAT_PARAMETER = "--output-format";
+ private static final String WANTED_ARTIFACT_TYPES_PARAMETER = "--wanted-artifact-types";
+ private static final String COLOR_SCHEME_PARAMETER = "--color-scheme";
+ private static final String CARRIAGE_RETURN = "\r";
+ private static final String NEWLINE = "\n";
+
+ private static final Path DOC_DIR = Paths.get("../core/src/test/resources/markdown").toAbsolutePath();
+
+ private Path outputFile;
+ private ByteArrayOutputStream outContent;
+ private ByteArrayOutputStream errContent;
+ private PrintStream originalOut;
+ private PrintStream originalErr;
+
+ @BeforeEach
+ void beforeEach(@TempDir final Path tempDir) {
+ this.outputFile = tempDir.resolve("stream.txt");
+ this.outContent = new ByteArrayOutputStream();
+ this.errContent = new ByteArrayOutputStream();
+ this.originalOut = System.out;
+ this.originalErr = System.err;
+ System.setOut(new PrintStream(this.outContent));
+ System.setErr(new PrintStream(this.errContent));
+ }
+
+ @AfterEach
+ void afterEach() {
+ System.setOut(this.originalOut);
+ System.setErr(this.originalErr);
+ }
+
+ private String getStdOut() {
+ return this.outContent.toString(StandardCharsets.UTF_8);
+ }
+
+ private String getStdErr() {
+ return this.errContent.toString(StandardCharsets.UTF_8);
+ }
+
+ @Test
+ void testNoArguments() {
+ final ExitStatus status = runInternal();
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.CLI_ERROR)),
+ () -> assertThat(getStdErr(), startsWith("oft: Missing command"))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.command-selection~1]
+ void testIllegalCommand() {
+ final ExitStatus status = runInternal(ILLEGAL_COMMAND);
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.CLI_ERROR)),
+ () -> assertThat(getStdErr(), startsWith("oft: '" + ILLEGAL_COMMAND + "' is not an OFT command."))
+ );
+ }
+
+ @ValueSource(strings = {HELP_COMMAND, "-h", "--help"})
+ @ParameterizedTest
+ void testHelpPrintsUsage(final String command) {
+ final String nl = "\n";
+ final ExitStatus status = runInternal(command);
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertThat(getStdOut(), startsWith("OpenFastTrace" + nl + nl + "Usage:"))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.command-selection~1]
+ void testConvertWithoutExplicitInputs() {
+ final ExitStatus status = runInternal(CONVERT_COMMAND);
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertThat(getStdOut(), startsWith(SPECOBJECT_PREAMBLE))
+ );
+ }
+
+ @Test
+ void testConvertUnknownExporter() {
+ final ExitStatus status = runInternal(
+ CONVERT_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FORMAT_PARAMETER, "illegal",
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.CLI_ERROR)),
+ () -> assertThat(getStdErr(), startsWith("oft: export format 'illegal' is not supported."))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.conversion.output-format~1]
+ void testConvertToSpecobjectFile() {
+ final ExitStatus status = runInternal(
+ CONVERT_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FORMAT_PARAMETER, "specobject",
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith(SPECOBJECT_PREAMBLE + "\n dsn~cli.conversion.default-output-format~1]
+ void testConvertDefaultOutputFormat() {
+ final ExitStatus status = runInternal(CONVERT_COMMAND, DOC_DIR.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertThat(getStdOut(), startsWith(SPECOBJECT_PREAMBLE))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.input-file-selection~1]
+ void testConvertDefaultOutputFormatIntoFile() {
+ final ExitStatus status = runInternal(CONVERT_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith(SPECOBJECT_PREAMBLE)
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.default-input~1]
+ void testConvertDefaultInputDir() {
+ final ExitStatus status = runInternal(
+ CONVERT_COMMAND,
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith(SPECOBJECT_PREAMBLE),
+ () -> assertOutputFileLength(2000)
+ );
+ }
+
+ @Test
+ void testTraceNoArguments() {
+ final ExitStatus status = runInternalWithWorkingDir(Path.of(".").toAbsolutePath(), TRACE_COMMAND);
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.FAILURE)),
+ () -> assertThat(getStdOut(), containsString("not ok\u001B[0m - 43 total, 43 defect"))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.command-selection~1]
+ void testTrace() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND,
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ DOC_DIR.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith("ok - 5 total")
+ );
+ }
+
+ @Test
+ void testTraceWithReportVerbosityMinimal() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ REPORT_VERBOSITY_PARAMETER, "MINIMAL");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith("ok")
+ );
+ }
+
+ @Test
+ void testTraceWithReportVerbosityQuietToStdOut() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND, DOC_DIR.toString(),
+ REPORT_VERBOSITY_PARAMETER, "QUIET");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertThat(getStdOut(), is(emptyString())),
+ () -> assertOutputFileExists(false)
+ );
+ }
+
+ @Test
+ void testTraceWithReportVerbosityQuietToFileMustBeRejected() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ REPORT_VERBOSITY_PARAMETER, "QUIET");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.CLI_ERROR)),
+ () -> assertThat(getStdErr(), containsString("oft: combining stream verbosity 'quiet' and output to file is not supported."))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.default-input~1]
+ void testTraceDefaultInputDir() {
+ final ExitStatus status = runInternal(TRACE_COMMAND, OUTPUT_FILE_PARAMETER, this.outputFile.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.FAILURE)),
+ () -> assertThat(getStdOut(), is(emptyString())),
+ () -> assertOutputFileExists(true)
+ );
+ }
+
+ @Test
+ void testBasicHtmlTrace() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FORMAT_PARAMETER, "html");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertThat(getStdOut(), startsWith(""))
+ );
+ }
+
+ @Test
+ // [itest->dsn~cli.tracing.output-format~1]
+ void testTraceOutputFormatPlain() {
+ final ExitStatus status = runInternal(TRACE_COMMAND, DOC_DIR.toString(), OUTPUT_FILE_PARAMETER,
+ this.outputFile.toString(), OUTPUT_FORMAT_PARAMETER, "plain");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith("ok - 5 total")
+ );
+ }
+
+ @Test
+ void testTraceMacNewlines() {
+ final ExitStatus status = runInternal(TRACE_COMMAND,
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ NEWLINE_PARAMETER, "OLDMAC",
+ DOC_DIR.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ this::assertOutputFileContainsOldMacNewlines,
+ this::assertOutputFileContainsNoUnixNewlines);
+ }
+
+ @Test
+ // [itest->dsn~cli.default-newline-format~1]
+ void testTraceDefaultNewlines() {
+ final ExitStatus status = runInternal(TRACE_COMMAND,
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ DOC_DIR.toString());
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ this::assertPlatformNewlines,
+ this::assertNoOffendingNewlines);
+ }
+
+ @Test
+ void testTraceWithFilteredArtifactType() {
+ final ExitStatus status = runInternal(
+ TRACE_COMMAND, DOC_DIR.toString(),
+ OUTPUT_FILE_PARAMETER, this.outputFile.toString(),
+ WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req");
+ assertAll(
+ () -> assertThat(status, equalTo(ExitStatus.OK)),
+ () -> assertOutputFileExists(true),
+ () -> assertOutputFileContentStartsWith("ok - 3 total")
+ );
+ }
+
+ 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));
+ }
+
+ private void assertPlatformNewlines() {
+ assertThat("Has native platform line separator",
+ getOutputFileContent().contains(System.lineSeparator()), equalTo(true));
+ }
+
+ private void assertNoOffendingNewlines() {
+ final String systemLineSeparator = System.lineSeparator();
+ switch (systemLineSeparator) {
+ case NEWLINE:
+ assertThat("Has no carriage returns", getOutputFileContent().contains(CARRIAGE_RETURN),
+ equalTo(false));
+ break;
+ case CARRIAGE_RETURN:
+ assertThat("Has no newlines", getOutputFileContent().contains(NEWLINE), equalTo(false));
+ break;
+ case NEWLINE + CARRIAGE_RETURN:
+ assertThat("Has no newline without carriage return and vice-versa",
+ getOutputFileContent().matches("\n[^\r]|[^\n]\r"), equalTo(false));
+ break;
+ case CARRIAGE_RETURN + NEWLINE:
+ assertThat("Has no carriage return without newline and vice-versa",
+ getOutputFileContent().matches("\r[^\n]|[^\r]\n"), equalTo(false));
+ break;
+ default:
+ final String hexCode = systemLineSeparator.chars()
+ .mapToObj(c -> String.format("\\u%04x", c))
+ .collect(joining());
+ fail("Unsupported line separator '%s' (%s)".formatted(systemLineSeparator, hexCode));
+ }
+ }
+
+ private void assertOutputFileContentStartsWith(final String content) {
+ assertThat(getOutputFileContent(), startsWith(content));
+ }
+
+ private void assertOutputFileExists(final boolean fileExists) {
+ assertThat("Output file %s exists".formatted(this.outputFile), Files.exists(this.outputFile),
+ equalTo(fileExists));
+ }
+
+ private void assertOutputFileLength(final int length) {
+ assertThat(getOutputFileContent().length(), greaterThan(length));
+ }
+
+ private String getOutputFileContent() {
+ final Path file = this.outputFile;
+ if (!Files.exists(file)) {
+ throw new AssertionError("Expected output file %s does not exist".formatted(file));
+ }
+ try {
+ return Files.readString(file);
+ } catch (final IOException exception) {
+ throw new UncheckedIOException("Failed to read file %s".formatted(file), exception);
+ }
+ }
+
+ private ExitStatus runInternal(final String... arguments) {
+ return runInternalWithWorkingDir(Path.of("..").toAbsolutePath(), arguments);
+ }
+
+ private ExitStatus runInternalWithWorkingDir(final Path workingDir, final String... arguments) {
+ return CliStarter.mainDelegate(arguments, new StandardDirectoryService() {
+ @Override
+ public String getCurrent() {
+ return workingDir.toString();
+ }
+ });
+ }
+}