ConfigurableOptionParserHelpTest.java

/*
 The MIT License

 Copyright (c) 2004-2021 Paul R. Holser, Jr.

 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 "Software"), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 distribute, sublicense, and/or sell copies of the Software, and to
 permit persons to whom the Software is furnished to do so, subject to
 the following conditions:

 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package tests.joptsimple;

import java.io.StringWriter;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;

import static tests.joptsimple.Strings.EMPTY;
import static tests.joptsimple.Strings.LINE_SEPARATOR;
import static java.math.BigDecimal.*;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;

import joptsimple.BuiltinHelpFormatter;
import org.junit.Before;
import org.junit.Test;

import static joptsimple.util.DateConverter.*;
import static org.junit.Assert.*;

/**
 * @author <a href="mailto:pholser@alumni.rice.edu">Paul Holser</a>
 */
public class ConfigurableOptionParserHelpTest extends AbstractOptionParserFixture {
    private StringWriter sink;

    @Before
    public final void createSink() {
        parser.formatHelpWith( new BuiltinHelpFormatter( 120, 3 ) );
        sink = new StringWriter();
    }

    @Test
    public void unconfiguredParser() throws Exception {
        parser.printHelpOn( sink );

        assertHelpLines( "No options specified   ", EMPTY );
    }

    @Test
    public void oneOptionNoArgNoDescription() throws Exception {
        parser.accepts( "apple" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option    Description",
            "------    -----------",
            "--apple              ",
            EMPTY );
    }

    @Test
    public void oneOptionNoArgWithDescription() throws Exception {
        parser.accepts( "a", "some description" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option   Description     ",
            "------   -----------     ",
            "-a       some description",
            EMPTY );
    }

    @Test
    public void twoOptionsNoArgWithDescription() throws Exception {
        parser.accepts( "a", "some description" );
        parser.accepts( "verbose", "even more description" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option      Description          ",
            "------      -----------          ",
            "-a          some description     ",
            "--verbose   even more description",
            EMPTY );
    }

    @Test
    public void oneOptionRequiredArgNoDescription() throws Exception {
        parser.accepts( "a" ).withRequiredArg();

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option        Description",
            "------        -----------",
            "-a <String>              ",
            EMPTY );
    }

    @Test
    public void oneOptionRequiredArgNoDescriptionWithType() throws Exception {
        parser.accepts( "a" ).withRequiredArg().ofType( Integer.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option         Description",
            "------         -----------",
            "-a <Integer>              ",
            EMPTY );
    }

    @Test
    public void oneOptionRequiredArgWithDescription() throws Exception {
        parser.accepts( "a", "some value you need" ).withRequiredArg().describedAs( "numerical" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                   Description        ",
            "------                   -----------        ",
            "-a <String: numerical>   some value you need",
            EMPTY );
    }

    @Test
    public void oneOptionRequiredArgWithDescriptionAndType() throws Exception {
        parser.accepts( "a", "some value you need" ).withRequiredArg().describedAs( "numerical" )
            .ofType( Integer.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                    Description        ",
            "------                    -----------        ",
            "-a <Integer: numerical>   some value you need",
            EMPTY );
    }

    @Test
    public void oneOptionOptionalArgNoDescription() throws Exception {
        parser.accepts( "threshold" ).withOptionalArg();

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                 Description",
            "------                 -----------",
            "--threshold [String]              ",
            EMPTY );
    }

    @Test
    public void oneOptionOptionalArgNoDescriptionWithType() throws Exception {
        parser.accepts( "a" ).withOptionalArg().ofType( Float.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option       Description",
            "------       -----------",
            "-a [Float]              ",
            EMPTY );
    }

    @Test
    public void oneOptionOptionalArgWithDescription() throws Exception {
        parser.accepts( "threshold", "some value you need" ).withOptionalArg().describedAs( "positive integer" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                                   Description        ",
            "------                                   -----------        ",
            "--threshold [String: positive integer]   some value you need",
            EMPTY );
    }

    @Test
    public void oneOptionOptionalArgWithDescriptionAndType() throws Exception {
        parser.accepts( "threshold", "some value you need" ).withOptionalArg().describedAs( "positive decimal" )
            .ofType( Double.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                                   Description        ",
            "------                                   -----------        ",
            "--threshold [Double: positive decimal]   some value you need",
            EMPTY );
    }

    @Test
    public void alternativeLongOptions() throws Exception {
        parser.recognizeAlternativeLongOptions( true );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                   Description                     ",
            "------                   -----------                     ",
            "-W <String: opt=value>   Alternative form of long options",
            EMPTY );
    }

    @Test
    public void optionSynonymsWithoutArguments() throws Exception {
        parser.acceptsAll( asList( "v", "chatty" ), "be verbose" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option         Description",
            "------         -----------",
            "-v, --chatty   be verbose ",
            EMPTY );
    }

    @Test
    public void optionSynonymsWithRequiredArgument() throws Exception {
        parser.acceptsAll( asList( "L", "index" ), "set level" ).withRequiredArg().ofType( Integer.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                  Description",
            "------                  -----------",
            "-L, --index <Integer>   set level  ",
            EMPTY );
    }

    @Test
    public void optionSynonymsWithOptionalArgument() throws Exception {
        parser.acceptsAll( asList( "d", "since" ), "date filter" ).withOptionalArg().describedAs( "yyyyMMdd" )
            .ofType( Date.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                         Description",
            "------                         -----------",
            "-d, --since [Date: yyyyMMdd]   date filter",
            EMPTY );
    }

    @Test
    public void optionSynonymsSortedByShortOptionThenLexicographical() throws Exception {
        parser.acceptsAll( asList( "v", "prolix", "chatty" ) );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                   Description",
            "------                   -----------",
            "-v, --chatty, --prolix              ",
            EMPTY );
    }

    @Test
    public void bothColumnsExceedingAllocatedWidths() throws Exception {
        parser.acceptsAll( asList( "t", "threshold", "cutoff" ),
                "a threshold value beyond which a certain level of the application should cease to write logs" )
                .withRequiredArg()
                .describedAs( "a positive decimal number that will represent the threshold that has been outlined" )
                .ofType( Double.class );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                                                     Description                                          ",
            "------                                                     -----------                                          ",
            "-t, --cutoff, --threshold <Double: a positive decimal      a threshold value beyond which a certain level of the",
            "  number that will represent the threshold that has been     application should cease to write logs             ",
            "  outlined>                                                                                                     ",
            EMPTY );
    }

    // Bug 2018262
    @Test
    public void gradleHelp() throws Exception {
        parser.acceptsAll( asList( "n", "non-recursive" ), "Do not execute primary tasks of child projects." );
        parser.acceptsAll( singletonList( "S" ),
            "Don't trigger a System.exit(0) for normal termination. Used for Gradle's internal testing." );
        parser.acceptsAll( asList( "I", "no-imports" ), "Disable usage of default imports for build script files." );
        parser.acceptsAll( asList( "u", "no-search-upward" ),
            "Don't search in parent folders for a settings.gradle file." );
        parser.acceptsAll( asList( "x", "cache-off" ), "No caching of compiled build scripts." );
        parser.acceptsAll( asList( "r", "rebuild-cache" ), "Rebuild the cache of compiled build scripts." );
        parser.acceptsAll( asList( "v", "version" ), "Print version info." );
        parser.acceptsAll( asList( "d", "debug" ), "Log in debug mode (includes normal stacktrace)." );
        parser.acceptsAll( asList( "q", "quiet" ), "Log errors only." );
        parser.acceptsAll( asList( "j", "ivy-debug" ), "Set Ivy log level to debug (very verbose)." );
        parser.acceptsAll( asList( "i", "ivy-quiet" ), "Set Ivy log level to quiet." );
        parser.acceptsAll( asList( "s", "stacktrace" ),
            "Print out the stacktrace also for user exceptions (e.g. compile error)." );
        parser.acceptsAll( asList( "f", "full-stacktrace" ),
            "Print out the full (very verbose) stacktrace for any exceptions." );
        parser.acceptsAll( asList( "t", "tasks" ), "Show list of all available tasks and their dependencies." );
        parser.acceptsAll( asList( "p", "project-dir" ),
            "Specifies the start dir for Gradle. Defaults to current dir." ).withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "g", "gradle-user-home" ), "Specifies the gradle user home dir." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "l", "plugin-properties-file" ), "Specifies the plugin.properties file." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "b", "buildfile" ),
            "Specifies the build file name (also for subprojects). Defaults to build.gradle." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "D", "systemprop" ), "Set system property of the JVM (e.g. -Dmyprop=myvalue)." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "P", "projectprop" ),
            "Set project property for the build script (e.g. -Pmyprop=myvalue)." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "e", "embedded" ), "Specify an embedded build script." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "B", "bootstrap-debug" ),
            "Specify a text to be logged at the beginning (e.g. used by Gradle's bootstrap class)." )
            .withRequiredArg().ofType( String.class );
        parser.acceptsAll( asList( "h", "?" ), "Shows this help message." ).forHelp();

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                                  Description                                                                    ",
            "------                                  -----------                                                                    ",
            "-?, -h                                  Shows this help message.                                                       ",
            "-B, --bootstrap-debug <String>          Specify a text to be logged at the beginning (e.g. used by Gradle's bootstrap  ",
            "                                          class).                                                                      ",
            "-D, --systemprop <String>               Set system property of the JVM (e.g. -Dmyprop=myvalue).                        ",
            "-I, --no-imports                        Disable usage of default imports for build script files.                       ",
            "-P, --projectprop <String>              Set project property for the build script (e.g. -Pmyprop=myvalue).             ",
            "-S                                      Don't trigger a System.exit(0) for normal termination. Used for Gradle's       ",
            "                                          internal testing.                                                            ",
            "-b, --buildfile <String>                Specifies the build file name (also for subprojects). Defaults to build.gradle.",
            "-d, --debug                             Log in debug mode (includes normal stacktrace).                                ",
            "-e, --embedded <String>                 Specify an embedded build script.                                              ",
            "-f, --full-stacktrace                   Print out the full (very verbose) stacktrace for any exceptions.               ",
            "-g, --gradle-user-home <String>         Specifies the gradle user home dir.                                            ",
            "-i, --ivy-quiet                         Set Ivy log level to quiet.                                                    ",
            "-j, --ivy-debug                         Set Ivy log level to debug (very verbose).                                     ",
            "-l, --plugin-properties-file <String>   Specifies the plugin.properties file.                                          ",
            "-n, --non-recursive                     Do not execute primary tasks of child projects.                                ",
            "-p, --project-dir <String>              Specifies the start dir for Gradle. Defaults to current dir.                   ",
            "-q, --quiet                             Log errors only.                                                               ",
            "-r, --rebuild-cache                     Rebuild the cache of compiled build scripts.                                   ",
            "-s, --stacktrace                        Print out the stacktrace also for user exceptions (e.g. compile error).        ",
            "-t, --tasks                             Show list of all available tasks and their dependencies.                       ",
            "-u, --no-search-upward                  Don't search in parent folders for a settings.gradle file.                     ",
            "-v, --version                           Print version info.                                                            ",
            "-x, --cache-off                         No caching of compiled build scripts.                                          ",
            EMPTY );
    }

    @Test
    public void dateConverterShowsDatePattern() throws Exception {
        parser.accepts( "date", "a date" ).withRequiredArg().withValuesConvertedBy( datePattern( "MM/dd/yy" ) );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option              Description",
            "------              -----------",
            "--date <MM/dd/yy>   a date     ",
            EMPTY );
    }

    @Test
    public void dateConverterShowsDatePatternInCombinationWithDescription() throws Exception {
        parser.accepts( "date", "a date" ).withOptionalArg().describedAs( "your basic date pattern" )
            .withValuesConvertedBy( datePattern( "MM/dd/yy" ) );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                                       Description",
            "------                                       -----------",
            "--date [MM/dd/yy: your basic date pattern]   a date     ",
            EMPTY );
    }

    @Test
    public void leavesEmbeddedNewlinesInDescriptionsAlone() throws Exception {
        List<String> descriptionPieces =
            asList( "Specify the output type.", "'raw' = raw data.", "'java' = java class" );
        parser.accepts(
            "type",
            descriptionPieces.stream().collect( joining( LINE_SEPARATOR ) ) );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option   Description             ",
            "------   -----------             ",
            "--type   Specify the output type.",
            "         'raw' = raw data.       ",
            "         'java' = java class     ",
            EMPTY );
    }

    @Test
    public void includesDefaultValueForRequiredOptionArgument() throws Exception {
        parser.accepts( "a" ).withRequiredArg().defaultsTo( "boo" );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option        Description   ",
            "------        -----------   ",
            "-a <String>   (default: boo)",
            EMPTY );
    }

    @Test
    public void includesDefaultValueForOptionalOptionArgument() throws Exception {
        parser.accepts( "b" ).withOptionalArg().ofType( Integer.class ).defaultsTo( 5 );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option         Description ",
            "------         ----------- ",
            "-b [Integer]   (default: 5)",
            EMPTY );
    }

    @Test
    public void includesDefaultValueForArgumentWithDescription() throws Exception {
        parser.accepts( "c", "a quantity" ).withOptionalArg().ofType( BigDecimal.class )
            .describedAs( "quantity" ).defaultsTo( TEN );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                      Description             ",
            "------                      -----------             ",
            "-c [BigDecimal: quantity]   a quantity (default: 10)",
            EMPTY );
    }

    @Test
    public void includesListOfDefaultsForArgumentWithDescription() throws Exception {
        parser.accepts( "d", "dizzle" ).withOptionalArg().ofType( Integer.class )
            .describedAs( "double dizzle" ).defaultsTo( 2, 3, 5, 7 );

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option                        Description                   ",
            "------                        -----------                   ",
            "-d [Integer: double dizzle]   dizzle (default: [2, 3, 5, 7])",
            EMPTY );
    }

    @Test
    public void marksRequiredOptionsSpecially() throws Exception {
        parser.accepts( "e" ).withRequiredArg().required();

        parser.printHelpOn( sink );

        assertHelpLines(
            "Option (* = required)   Description",
            "---------------------   -----------",
            "* -e <String>                      ",
            EMPTY );
    }

    private void assertHelpLines( String... expectedLines ) {
        assertEquals(
            Stream.of( expectedLines ).collect( joining( LINE_SEPARATOR ) ),
            sink.toString() );
    }
}