/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. * Portions Copyright (c) 2017, Steven Haehn. */ package org.opengrok.indexer.util; import java.lang.reflect.Field; import java.text.ParseException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opengrok.indexer.index.Indexer; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author shaehn */ class OptionParserTest { int actionCounter; @BeforeEach public void setUp() { actionCounter = 0; } // Scan parser should ignore all options it does not recognize. @Test void scanParserIgnoreUnrecognizableOptions() throws ParseException { String configPath = "/the/config/path"; OptionParser scanner = OptionParser.scan(parser -> { parser.on("-R configPath").execute(v -> { assertEquals(v, configPath); actionCounter++; }); }); String[] args = {"-a", "-b", "-R", configPath}; scanner.parse(args); assertEquals(1, actionCounter); } // Validate that option can have multiple names // with both short and long versions. @Test void optionNameAliases() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("-?", "--help").execute(v -> { assertEquals("", v); actionCounter++; }); }); String[] args = {"-?"}; opts.parse(args); assertEquals(1, actionCounter); String[] args2 = {"--help"}; opts.parse(args2); assertEquals(2, actionCounter); } // Show that parser will throw exception // when option is not recognized. @Test void unrecognizedOption() { OptionParser opts = OptionParser.execute(parser -> { parser.on("-?", "--help").execute(v -> { }); }); try { String[] args = {"--unrecognizedOption"}; opts.parse(args); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Unknown option: --unrecognizedOption", msg); } } // Show that parser will throw exception when missing option value @Test void missingOptionValue() { OptionParser opts = OptionParser.execute(parser -> { parser.on("-a=VALUE").execute(v -> { }); }); try { String[] args = {"-a"}; // supply option without value opts.parse(args); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Option -a requires a value.", msg); } } // Test parser ability to find short option value whether // it is glued next to the option (eg. -xValue), or comes // as a following argument (eg. -x Value) @Test void shortOptionValue() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("-a=VALUE").execute(v -> { assertEquals("3", v); actionCounter++; }); }); String[] separateValue = {"-a", "3"}; opts.parse(separateValue); assertEquals(1, actionCounter); String[] joinedValue = {"-a3"}; opts.parse(joinedValue); assertEquals(2, actionCounter); } // Validate the ability of parser to convert // string option values into internal data types. @Test void testSupportedDataCoercion() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("--int=VALUE", Integer.class).execute(v -> { assertEquals(3, v); actionCounter++; }); parser.on("--float=VALUE", Float.class).execute(v -> { assertEquals((float) 3.23, v); actionCounter++; }); parser.on("--double=VALUE", Double.class).execute(v -> { assertEquals(3.23, v); actionCounter++; }); parser.on("-t", "--truth", "=VALUE", Boolean.class).execute(v -> { assertTrue((Boolean) v); actionCounter++; }); parser.on("-f VALUE", Boolean.class).execute(v -> { assertFalse((Boolean) v); actionCounter++; }); parser.on("-a array", String[].class).execute(v -> { String[] x = {"a", "b", "c"}; assertArrayEquals(x, (String[]) v); actionCounter++; }); }); String[] integer = {"--int", "3"}; opts.parse(integer); assertEquals(1, actionCounter); String[] floats = {"--float", "3.23"}; opts.parse(floats); assertEquals(2, actionCounter); String[] doubles = {"--double", "3.23"}; opts.parse(doubles); assertEquals(3, actionCounter); actionCounter = 0; String[] verity = {"-t", "true", "-t", "True", "-t", "on", "-t", "ON", "-t", "yeS"}; opts.parse(verity); assertEquals(5, actionCounter); actionCounter = 0; String[] falsehood = {"-f", "false", "-f", "FALSE", "-f", "oFf", "-f", "no", "-f", "NO"}; opts.parse(falsehood); assertEquals(5, actionCounter); try { // test illegal value to Boolean String[] liar = {"--truth", "liar"}; opts.parse(liar); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Failed to parse (liar) as value of [-t, --truth]", msg); } actionCounter = 0; String[] array = {"-a", "a,b,c"}; opts.parse(array); assertEquals(1, actionCounter); } // Make sure that option can take specific addOption of values // and when an candidate values is seen, an exception is given. @Test void specificOptionValues() { OptionParser opts = OptionParser.execute(parser -> { String[] onOff = {"on", "off"}; parser.on("--setTest on/off", onOff).execute(v -> actionCounter++); }); try { String[] args1 = {"--setTest", "on"}; opts.parse(args1); assertEquals(1, actionCounter); String[] args2 = {"--setTest", "off"}; opts.parse(args2); assertEquals(2, actionCounter); String[] args3 = {"--setTest", "nono"}; opts.parse(args3); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("'nono' is unknown value for option [--setTest]. Must be one of [on, off]", msg); } } // See that option value matches a regular expression @Test void optionValuePatternMatch() { OptionParser opts = OptionParser.execute(parser -> { parser.on("--pattern PERCENT", "/[0-9]+%?/").execute(v -> { actionCounter++; }); }); try { String[] args1 = {"--pattern", "3%"}; opts.parse(args1); assertEquals(1, actionCounter); String[] args2 = {"--pattern", "120%"}; opts.parse(args2); assertEquals(2, actionCounter); String[] args3 = {"--pattern", "75"}; opts.parse(args3); assertEquals(3, actionCounter); String[] args4 = {"--pattern", "NotNumber"}; opts.parse(args4); } catch (ParseException e) { String msg = e.getMessage(); assertEquals(msg, "Value 'NotNumber' for option [--pattern]PERCENT\n" + " does not match pattern [0-9]+%?"); } } // Verify option may have non-required value @Test void missingValueOnOptionAllowed() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("--value=[optional]").execute(v -> { actionCounter++; if (v.equals("")) { assertEquals("", v); } else { assertEquals("hasOne", v); } }); parser.on("-o[=optional]").execute(v -> { actionCounter++; if (v.equals("")) { assertEquals("", v); } else { assertEquals("hasOne", v); } }); parser.on("-v[optional]").execute(v -> { actionCounter++; if (v.equals("")) { assertEquals("", v); } else { assertEquals("hasOne", v); } }); }); String[] args1 = {"--value", "hasOne"}; opts.parse(args1); assertEquals(1, actionCounter); String[] args2 = {"--value"}; opts.parse(args2); assertEquals(2, actionCounter); String[] args3 = {"-ohasOne"}; opts.parse(args3); assertEquals(3, actionCounter); String[] args4 = {"-o"}; opts.parse(args4); assertEquals(4, actionCounter); String[] args5 = {"-v", "hasOne"}; opts.parse(args5); assertEquals(5, actionCounter); String[] args6 = {"-v"}; opts.parse(args6); assertEquals(6, actionCounter); String[] args7 = {"--value", "-o", "hasOne"}; opts.parse(args7); assertEquals(8, actionCounter); } // Verify default option summary @Test void defaultOptionSummary() { OptionParser opts = OptionParser.execute(parser -> { parser.on("--help").execute(v -> { String summary = parser.getUsage(); // assertTrue(summary.startsWith("Usage: JUnitTestRunner [options]")); // fails on travis assertTrue(summary.matches("(?s)Usage: \\w+ \\[options\\].*")); }); }); try { String[] args = {"--help"}; opts.parse(args); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Unknown option: --unrecognizedOption", msg); } } // Allowing user entry of initial substrings to long option names. // Therefore, must be able to catch when option entry matches more // than one entry. @Test void catchAmbigousOptions() { OptionParser opts = OptionParser.execute(parser -> { parser.on("--help"); parser.on("--help-me-out"); }); try { String[] args = {"--he"}; opts.parse(args); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Ambiguous option --he matches [--help-me-out, --help]", msg); } } // Allow user to enter an initial substring to long option names @Test void allowInitialSubstringOptionNames() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("--help-me-out").execute(v -> actionCounter++); }); String[] args = {"--help"}; opts.parse(args); assertEquals(1, actionCounter); } // Specific test to evalutate the internal option candidate method @Test void testInitialSubstringOptionNames() throws ParseException { OptionParser opts = OptionParser.execute(parser -> { parser.on("--help-me-out"); parser.on("--longOption"); }); assertEquals("--longOption", opts.candidate("--l", 0)); assertEquals("--help-me-out", opts.candidate("--h", 0)); assertNull(opts.candidate("--thisIsUnknownOption", 0)); } // Catch duplicate option names in parser construction. @Test void catchDuplicateOptionNames() { try { OptionParser.execute(parser -> { parser.on("--duplicate"); parser.on("--duplicate"); }); } catch (IllegalArgumentException e) { String msg = e.getMessage(); assertEquals("** Programmer error! Option --duplicate already defined", msg); } } // Catch single '-' in argument list @Test void catchNamelessOption() { OptionParser opts = OptionParser.execute(parser -> { parser.on("--help-me-out"); }); try { String[] args = {"-", "data"}; opts.parse(args); } catch (ParseException e) { String msg = e.getMessage(); assertEquals("Stand alone '-' found in arguments, not allowed", msg); } } // Fail options put into Indexer.java that do not have a description. @Test void catchIndexerOptionsWithoutDescription() throws NoSuchFieldException, IllegalAccessException, ParseException { String[] argv = {"---unitTest"}; assertDoesNotThrow(() -> Indexer.parseOptions(argv)); // Use reflection to get the option parser from Indexer. Field f = Indexer.class.getDeclaredField("optParser"); f.setAccessible(true); OptionParser op = (OptionParser) f.get(Indexer.class); for (OptionParser.Option o : op.getOptionList()) { assertNotNull(o.description, "'" + o.names.get(0) + "' option needs description"); assertFalse(o.description.toString().isEmpty(), "'" + o.names.get(0) + "' option needs non-empty description"); } // This just tests that the description is actually null. op = OptionParser.execute(parser -> { parser.on("--help-me-out"); }); for (OptionParser.Option o : op.getOptionList()) { assertNull(o.description); } } @Test void testAddLongDescription() { String longDescription = "A".repeat(OptionParser.Option.MAX_DESCRIPTION_LINE_LENGTH + 1); assertThrows(IllegalArgumentException.class, () -> OptionParser.execute(parser -> { parser.on("--foo", longDescription); })); } @Test void testParseOptions() { String[] argv = {"---unitTest"}; assertDoesNotThrow(() -> Indexer.parseOptions(argv)); } }