From 0a445f7763bb7e7487eb4513439036fb74d9f619 Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Sat, 23 May 2026 16:50:09 +0200 Subject: [PATCH 1/8] #503: Added `-h` / `--help` to the command line. --- .../core/cli/ArgumentValidator.java | 6 ++- .../openfasttrace/core/cli/CliArguments.java | 33 ++++++++++++++ .../openfasttrace/core/cli/CliStarter.java | 43 ++++++++----------- .../core/cli/CommandLineInterpreter.java | 4 +- .../core/cli/commands/HelpCommand.java | 11 +++-- doc/changes/changes.md | 3 +- doc/changes/changes_4.5.0.md | 12 ++++++ .../openfasttrace/cli/CliStarterIT.java | 9 ++-- 8 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 doc/changes/changes_4.5.0.md diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java index 250667812..f80eb1cff 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java @@ -40,7 +40,11 @@ private boolean validate() { final Optional command = this.arguments.getCommand(); boolean ok = false; - if (command.isEmpty()) + if (this.arguments.isHelpSet()) + { + ok = true; + } + else if (command.isEmpty()) { this.error = "Missing command"; this.suggestion = "Add one of " + listCommands(); 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 629ffdfb0..1abae862c 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 @@ -45,6 +45,7 @@ public class CliArguments // [impl->dsn~cli.plugins.log~1] private LogLevel logLevel; + private boolean isHelpSet; /** * Create new {@link CliArguments}. @@ -254,6 +255,38 @@ public void setN(final Newline newline) setNewline(newline); } + /** + * Check if the help switch is set + * + * @return {@code true} if the help switch is set + */ + public boolean isHelpSet() + { + return this.isHelpSet; + } + + /** + * Set the help switch (no arguments) + * + * @param helpSet + * {@code true} to set the help switch + */ + public void setHelp(final boolean helpSet) + { + this.isHelpSet = helpSet; + } + + /** + * Set the help switch (no arguments) + * + * @param helpSet + * {@code true} to set the help switch + */ + public void setH(final boolean helpSet) + { + setHelp(helpSet); + } + /** * Get a list of artifact types to be applied as filter during import * 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 a94a433df..44bd33311 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 @@ -6,11 +6,14 @@ import org.itsallcode.openfasttrace.core.cli.commands.*; import org.itsallcode.openfasttrace.core.cli.logging.LoggingConfigurator; +import static org.itsallcode.openfasttrace.core.cli.ExitStatus.*; + /** * The main entry point class for the command line application. */ public class CliStarter { + private static final String MISSING_COMMAND = "missing"; private final CliArguments arguments; /** @@ -59,7 +62,7 @@ public static void main(final String[] args, final DirectoryService directorySer { printToStdError( "oft: " + validator.getError() + "\n" + validator.getSuggestion() + "\n"); - exit(ExitStatus.CLI_ERROR); + exit(CLI_ERROR); } } @@ -75,7 +78,7 @@ private static CliArguments parseCommandLineArguments(final String[] args, catch (final CliException e) { printToStdError("oft: " + e.getMessage()); - exit(ExitStatus.CLI_ERROR); + exit(CLI_ERROR); } return arguments; } @@ -93,37 +96,29 @@ private static void printToStdError(final String message) // [impl->dsn~cli.command-selection~1] public void run() { - final Optional command = this.arguments.getCommand(); - if (!command.isPresent()) - { - new HelpCommand().run(); - throw new IllegalStateException("Command missing trying to execute OFT mode."); - } - final Performable performable; - switch (command.get()) + final String command = this.arguments.isHelpSet() + ? HelpCommand.COMMAND_NAME + : this.arguments.getCommand().orElse(MISSING_COMMAND); + ExitStatus exitStatus; + switch (command) { case ConvertCommand.COMMAND_NAME: - performable = new ConvertCommand(this.arguments); + exitStatus = new ConvertCommand(this.arguments).run() ? OK : FAILURE; break; case TraceCommand.COMMAND_NAME: - performable = new TraceCommand(this.arguments); + exitStatus = new TraceCommand(this.arguments).run() ? OK : FAILURE; break; case HelpCommand.COMMAND_NAME: - performable = new HelpCommand(); + exitStatus = new HelpCommand(true).run() ? OK : FAILURE; break; + case MISSING_COMMAND: default: - new HelpCommand().run(); - exit(ExitStatus.CLI_ERROR); - return; - } - if (performable.run()) - { - exit(ExitStatus.OK); - } - else - { - exit(ExitStatus.FAILURE); + new HelpCommand(false).run(); + printToStdError("Compand missing trying to execute OFT. Choose one of: trace, convert, help"); + exitStatus = CLI_ERROR; + break; } + exit(exitStatus); } // [impl->dsn~cli.tracing.exit-status~1] 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 4cbf5e982..997bc8d97 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 @@ -15,8 +15,8 @@ * * 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 + *

setUnnamedValues that * will receive all argument values that are unnamed. */ public class CommandLineInterpreter diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/HelpCommand.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/HelpCommand.java index 2cdacc173..83885060d 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/HelpCommand.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/HelpCommand.java @@ -11,13 +11,17 @@ public class HelpCommand implements Performable { /** The command line action for running this command. */ public static final String COMMAND_NAME = "help"; + /** Whether the OFT command was used correctly. */ + private final boolean validUsage; /** * Create a new {@link HelpCommand}. + * + * @param validUsage whether the OFT command was used correctly */ - public HelpCommand() + public HelpCommand(final boolean validUsage) { - // empty by intention + this.validUsage = validUsage; } @Override @@ -26,7 +30,7 @@ public boolean run() { final String usage = loadResource("/usage.txt"); System.out.println(usage); - return true; + return validUsage; } private String loadResource(final String resourceName) @@ -52,5 +56,4 @@ private URL getResource(final String resourceName) } return url; } - } diff --git a/doc/changes/changes.md b/doc/changes/changes.md index 1fa0a9fff..1c43bcd47 100644 --- a/doc/changes/changes.md +++ b/doc/changes/changes.md @@ -1,6 +1,7 @@ # Changes -* [4.3.1](changes_4.4.0.md) +* [4.5.0](changes_4.5.0.md) +* [4.4.0](changes_4.4.0.md) * [4.3.0](changes_4.3.0.md) * [4.2.3](changes_4.2.3.md) * [4.2.2](changes_4.2.2.md) diff --git a/doc/changes/changes_4.5.0.md b/doc/changes/changes_4.5.0.md new file mode 100644 index 000000000..987323838 --- /dev/null +++ b/doc/changes/changes_4.5.0.md @@ -0,0 +1,12 @@ +# OpenFastTrace 4.4.0, released 2026-05-?? + +Code name: ??? + +## Summary + +In this release we added the a `-h` / `--help` to the command line. + + +## Features + +* #503: Added `-h` / `--help` to the command line. \ No newline at end of file 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 b2f7f84f9..d26119394 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/CliStarterIT.java @@ -20,6 +20,8 @@ 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) @@ -74,11 +76,12 @@ void testIllegalCommand() "oft: '" + ILLEGAL_COMMAND + "' is not an OFT command."); } - @Test - void testHelpPrintsUsage() + @ValueSource(strings = {HELP_COMMAND, "-h", "--help"}) + @ParameterizedTest + void testHelpPrintsUsage(final String command) { final String nl = "\n"; - assertExitOkWithStdOutStart(jarLauncher(HELP_COMMAND), "OpenFastTrace" + nl + nl + "Usage:"); + assertExitOkWithStdOutStart(jarLauncher(command), "OpenFastTrace" + nl + nl + "Usage:"); } // [itest->dsn~cli.command-selection~1] From 9f1f5f4afd94ea598e05c03addacc430bd4bc272 Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Sat, 23 May 2026 16:58:51 +0200 Subject: [PATCH 2/8] #503: Add `-h`/`--help` argument to the CLI --- .../org/itsallcode/openfasttrace/core/cli/CliArguments.java | 4 ++-- .../org/itsallcode/openfasttrace/core/cli/CliStarter.java | 6 ++---- .../openfasttrace/core/cli/CommandLineInterpreter.java | 2 +- .../java/org/itsallcode/openfasttrace/cli/CliStarterIT.java | 1 + 4 files changed, 6 insertions(+), 7 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 1abae862c..464f3a690 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 @@ -290,7 +290,7 @@ public void setH(final boolean helpSet) /** * Get a list of artifact types to be applied as filter during import * - * @return list of wanted artifact types + * @return set of wanted artifact types */ public Set getWantedArtifactTypes() { @@ -327,7 +327,7 @@ public void setA(final String artifactTypes) /** * Get a list of tags to be applied as filter during import * - * @return list of wanted tags + * @return set of wanted tags */ public Set 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 - 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(); + } + }); + } +}