1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2017, Steven Haehn. 23 */ 24 package org.opengrok.indexer.util; 25 26 import java.lang.reflect.Field; 27 import java.text.ParseException; 28 29 import org.junit.jupiter.api.BeforeEach; 30 import org.junit.jupiter.api.Test; 31 import org.opengrok.indexer.index.Indexer; 32 33 import static org.junit.jupiter.api.Assertions.assertArrayEquals; 34 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 35 import static org.junit.jupiter.api.Assertions.assertEquals; 36 import static org.junit.jupiter.api.Assertions.assertFalse; 37 import static org.junit.jupiter.api.Assertions.assertNotNull; 38 import static org.junit.jupiter.api.Assertions.assertNull; 39 import static org.junit.jupiter.api.Assertions.assertThrows; 40 import static org.junit.jupiter.api.Assertions.assertTrue; 41 42 /** 43 * @author shaehn 44 */ 45 class OptionParserTest { 46 47 int actionCounter; 48 49 @BeforeEach setUp()50 public void setUp() { 51 actionCounter = 0; 52 } 53 54 // Scan parser should ignore all options it does not recognize. 55 @Test scanParserIgnoreUnrecognizableOptions()56 void scanParserIgnoreUnrecognizableOptions() throws ParseException { 57 58 String configPath = "/the/config/path"; 59 60 OptionParser scanner = OptionParser.scan(parser -> { 61 parser.on("-R configPath").execute(v -> { 62 assertEquals(v, configPath); 63 actionCounter++; 64 }); 65 }); 66 67 String[] args = {"-a", "-b", "-R", configPath}; 68 scanner.parse(args); 69 assertEquals(1, actionCounter); 70 } 71 72 // Validate that option can have multiple names 73 // with both short and long versions. 74 @Test optionNameAliases()75 void optionNameAliases() throws ParseException { 76 77 OptionParser opts = OptionParser.execute(parser -> { 78 79 parser.on("-?", "--help").execute(v -> { 80 assertEquals("", v); 81 actionCounter++; 82 }); 83 }); 84 85 String[] args = {"-?"}; 86 opts.parse(args); 87 assertEquals(1, actionCounter); 88 89 String[] args2 = {"--help"}; 90 opts.parse(args2); 91 assertEquals(2, actionCounter); 92 } 93 94 // Show that parser will throw exception 95 // when option is not recognized. 96 @Test unrecognizedOption()97 void unrecognizedOption() { 98 99 OptionParser opts = OptionParser.execute(parser -> { 100 parser.on("-?", "--help").execute(v -> { 101 }); 102 }); 103 104 try { 105 String[] args = {"--unrecognizedOption"}; 106 opts.parse(args); 107 108 } catch (ParseException e) { 109 String msg = e.getMessage(); 110 assertEquals("Unknown option: --unrecognizedOption", msg); 111 } 112 } 113 114 // Show that parser will throw exception when missing option value 115 @Test missingOptionValue()116 void missingOptionValue() { 117 118 OptionParser opts = OptionParser.execute(parser -> { 119 parser.on("-a=VALUE").execute(v -> { 120 }); 121 }); 122 123 try { 124 String[] args = {"-a"}; // supply option without value 125 opts.parse(args); 126 127 } catch (ParseException e) { 128 String msg = e.getMessage(); 129 assertEquals("Option -a requires a value.", msg); 130 } 131 } 132 133 // Test parser ability to find short option value whether 134 // it is glued next to the option (eg. -xValue), or comes 135 // as a following argument (eg. -x Value) 136 @Test shortOptionValue()137 void shortOptionValue() throws ParseException { 138 139 OptionParser opts = OptionParser.execute(parser -> { 140 parser.on("-a=VALUE").execute(v -> { 141 assertEquals("3", v); 142 actionCounter++; 143 }); 144 }); 145 146 String[] separateValue = {"-a", "3"}; 147 opts.parse(separateValue); 148 assertEquals(1, actionCounter); 149 150 String[] joinedValue = {"-a3"}; 151 opts.parse(joinedValue); 152 assertEquals(2, actionCounter); 153 } 154 155 // Validate the ability of parser to convert 156 // string option values into internal data types. 157 @Test testSupportedDataCoercion()158 void testSupportedDataCoercion() throws ParseException { 159 160 OptionParser opts = OptionParser.execute(parser -> { 161 162 parser.on("--int=VALUE", Integer.class).execute(v -> { 163 assertEquals(3, v); 164 actionCounter++; 165 }); 166 167 parser.on("--float=VALUE", Float.class).execute(v -> { 168 assertEquals((float) 3.23, v); 169 actionCounter++; 170 }); 171 172 parser.on("--double=VALUE", Double.class).execute(v -> { 173 assertEquals(3.23, v); 174 actionCounter++; 175 }); 176 177 parser.on("-t", "--truth", "=VALUE", Boolean.class).execute(v -> { 178 assertTrue((Boolean) v); 179 actionCounter++; 180 }); 181 182 parser.on("-f VALUE", Boolean.class).execute(v -> { 183 assertFalse((Boolean) v); 184 actionCounter++; 185 }); 186 187 parser.on("-a array", String[].class).execute(v -> { 188 String[] x = {"a", "b", "c"}; 189 assertArrayEquals(x, (String[]) v); 190 actionCounter++; 191 }); 192 }); 193 194 String[] integer = {"--int", "3"}; 195 opts.parse(integer); 196 assertEquals(1, actionCounter); 197 198 String[] floats = {"--float", "3.23"}; 199 opts.parse(floats); 200 assertEquals(2, actionCounter); 201 202 String[] doubles = {"--double", "3.23"}; 203 opts.parse(doubles); 204 assertEquals(3, actionCounter); 205 206 actionCounter = 0; 207 String[] verity = {"-t", "true", "-t", "True", "-t", "on", "-t", "ON", "-t", "yeS"}; 208 opts.parse(verity); 209 assertEquals(5, actionCounter); 210 211 actionCounter = 0; 212 String[] falsehood = {"-f", "false", "-f", "FALSE", "-f", "oFf", "-f", "no", "-f", "NO"}; 213 opts.parse(falsehood); 214 assertEquals(5, actionCounter); 215 216 try { // test illegal value to Boolean 217 String[] liar = {"--truth", "liar"}; 218 opts.parse(liar); 219 } catch (ParseException e) { 220 String msg = e.getMessage(); 221 assertEquals("Failed to parse (liar) as value of [-t, --truth]", msg); 222 } 223 224 actionCounter = 0; 225 String[] array = {"-a", "a,b,c"}; 226 opts.parse(array); 227 assertEquals(1, actionCounter); 228 } 229 230 // Make sure that option can take specific addOption of values 231 // and when an candidate values is seen, an exception is given. 232 @Test specificOptionValues()233 void specificOptionValues() { 234 235 OptionParser opts = OptionParser.execute(parser -> { 236 String[] onOff = {"on", "off"}; 237 parser.on("--setTest on/off", onOff).execute(v -> actionCounter++); 238 }); 239 240 try { 241 String[] args1 = {"--setTest", "on"}; 242 opts.parse(args1); 243 assertEquals(1, actionCounter); 244 245 String[] args2 = {"--setTest", "off"}; 246 opts.parse(args2); 247 assertEquals(2, actionCounter); 248 249 String[] args3 = {"--setTest", "nono"}; 250 opts.parse(args3); 251 } catch (ParseException e) { 252 String msg = e.getMessage(); 253 assertEquals("'nono' is unknown value for option [--setTest]. Must be one of [on, off]", msg); 254 } 255 } 256 257 // See that option value matches a regular expression 258 @Test optionValuePatternMatch()259 void optionValuePatternMatch() { 260 261 OptionParser opts = OptionParser.execute(parser -> { 262 263 parser.on("--pattern PERCENT", "/[0-9]+%?/").execute(v -> { 264 actionCounter++; 265 }); 266 }); 267 268 try { 269 String[] args1 = {"--pattern", "3%"}; 270 opts.parse(args1); 271 assertEquals(1, actionCounter); 272 273 String[] args2 = {"--pattern", "120%"}; 274 opts.parse(args2); 275 assertEquals(2, actionCounter); 276 277 String[] args3 = {"--pattern", "75"}; 278 opts.parse(args3); 279 assertEquals(3, actionCounter); 280 281 String[] args4 = {"--pattern", "NotNumber"}; 282 opts.parse(args4); 283 } catch (ParseException e) { 284 String msg = e.getMessage(); 285 assertEquals(msg, "Value 'NotNumber' for option [--pattern]PERCENT\n" + 286 " does not match pattern [0-9]+%?"); 287 } 288 } 289 290 // Verify option may have non-required value 291 @Test missingValueOnOptionAllowed()292 void missingValueOnOptionAllowed() throws ParseException { 293 294 OptionParser opts = OptionParser.execute(parser -> { 295 296 parser.on("--value=[optional]").execute(v -> { 297 actionCounter++; 298 if (v.equals("")) { 299 assertEquals("", v); 300 } else { 301 assertEquals("hasOne", v); 302 } 303 }); 304 parser.on("-o[=optional]").execute(v -> { 305 actionCounter++; 306 if (v.equals("")) { 307 assertEquals("", v); 308 } else { 309 assertEquals("hasOne", v); 310 } 311 }); 312 parser.on("-v[optional]").execute(v -> { 313 actionCounter++; 314 if (v.equals("")) { 315 assertEquals("", v); 316 } else { 317 assertEquals("hasOne", v); 318 } 319 }); 320 }); 321 322 String[] args1 = {"--value", "hasOne"}; 323 opts.parse(args1); 324 assertEquals(1, actionCounter); 325 326 String[] args2 = {"--value"}; 327 opts.parse(args2); 328 assertEquals(2, actionCounter); 329 330 String[] args3 = {"-ohasOne"}; 331 opts.parse(args3); 332 assertEquals(3, actionCounter); 333 334 String[] args4 = {"-o"}; 335 opts.parse(args4); 336 assertEquals(4, actionCounter); 337 338 String[] args5 = {"-v", "hasOne"}; 339 opts.parse(args5); 340 assertEquals(5, actionCounter); 341 342 String[] args6 = {"-v"}; 343 opts.parse(args6); 344 assertEquals(6, actionCounter); 345 346 String[] args7 = {"--value", "-o", "hasOne"}; 347 opts.parse(args7); 348 assertEquals(8, actionCounter); 349 } 350 351 // Verify default option summary 352 @Test defaultOptionSummary()353 void defaultOptionSummary() { 354 OptionParser opts = OptionParser.execute(parser -> { 355 parser.on("--help").execute(v -> { 356 String summary = parser.getUsage(); 357 // assertTrue(summary.startsWith("Usage: JUnitTestRunner [options]")); // fails on travis 358 assertTrue(summary.matches("(?s)Usage: \\w+ \\[options\\].*")); 359 }); 360 }); 361 362 try { 363 String[] args = {"--help"}; 364 opts.parse(args); 365 } catch (ParseException e) { 366 String msg = e.getMessage(); 367 assertEquals("Unknown option: --unrecognizedOption", msg); 368 } 369 } 370 371 // Allowing user entry of initial substrings to long option names. 372 // Therefore, must be able to catch when option entry matches more 373 // than one entry. 374 @Test catchAmbigousOptions()375 void catchAmbigousOptions() { 376 OptionParser opts = OptionParser.execute(parser -> { 377 parser.on("--help"); 378 parser.on("--help-me-out"); 379 }); 380 381 try { 382 String[] args = {"--he"}; 383 opts.parse(args); 384 } catch (ParseException e) { 385 String msg = e.getMessage(); 386 assertEquals("Ambiguous option --he matches [--help-me-out, --help]", msg); 387 } 388 } 389 390 // Allow user to enter an initial substring to long option names 391 @Test allowInitialSubstringOptionNames()392 void allowInitialSubstringOptionNames() throws ParseException { 393 OptionParser opts = OptionParser.execute(parser -> { 394 parser.on("--help-me-out").execute(v -> actionCounter++); 395 }); 396 397 String[] args = {"--help"}; 398 opts.parse(args); 399 assertEquals(1, actionCounter); 400 } 401 402 // Specific test to evalutate the internal option candidate method 403 @Test testInitialSubstringOptionNames()404 void testInitialSubstringOptionNames() throws ParseException { 405 OptionParser opts = OptionParser.execute(parser -> { 406 parser.on("--help-me-out"); 407 parser.on("--longOption"); 408 }); 409 410 assertEquals("--longOption", opts.candidate("--l", 0)); 411 assertEquals("--help-me-out", opts.candidate("--h", 0)); 412 assertNull(opts.candidate("--thisIsUnknownOption", 0)); 413 } 414 415 // Catch duplicate option names in parser construction. 416 @Test catchDuplicateOptionNames()417 void catchDuplicateOptionNames() { 418 try { 419 OptionParser.execute(parser -> { 420 parser.on("--duplicate"); 421 parser.on("--duplicate"); 422 }); 423 } catch (IllegalArgumentException e) { 424 String msg = e.getMessage(); 425 assertEquals("** Programmer error! Option --duplicate already defined", msg); 426 } 427 } 428 429 // Catch single '-' in argument list 430 @Test catchNamelessOption()431 void catchNamelessOption() { 432 OptionParser opts = OptionParser.execute(parser -> { 433 parser.on("--help-me-out"); 434 }); 435 436 try { 437 String[] args = {"-", "data"}; 438 opts.parse(args); 439 } catch (ParseException e) { 440 String msg = e.getMessage(); 441 assertEquals("Stand alone '-' found in arguments, not allowed", msg); 442 } 443 } 444 445 // Fail options put into Indexer.java that do not have a description. 446 @Test catchIndexerOptionsWithoutDescription()447 void catchIndexerOptionsWithoutDescription() throws NoSuchFieldException, IllegalAccessException, ParseException { 448 String[] argv = {"---unitTest"}; 449 assertDoesNotThrow(() -> Indexer.parseOptions(argv)); 450 451 // Use reflection to get the option parser from Indexer. 452 Field f = Indexer.class.getDeclaredField("optParser"); 453 f.setAccessible(true); 454 OptionParser op = (OptionParser) f.get(Indexer.class); 455 456 for (OptionParser.Option o : op.getOptionList()) { 457 assertNotNull(o.description, "'" + o.names.get(0) + "' option needs description"); 458 assertFalse(o.description.toString().isEmpty(), 459 "'" + o.names.get(0) + "' option needs non-empty description"); 460 } 461 462 // This just tests that the description is actually null. 463 op = OptionParser.execute(parser -> { 464 parser.on("--help-me-out"); 465 }); 466 467 for (OptionParser.Option o : op.getOptionList()) { 468 assertNull(o.description); 469 } 470 } 471 472 @Test testAddLongDescription()473 void testAddLongDescription() { 474 String longDescription = "A".repeat(OptionParser.Option.MAX_DESCRIPTION_LINE_LENGTH + 1); 475 476 assertThrows(IllegalArgumentException.class, () -> 477 OptionParser.execute(parser -> { 478 parser.on("--foo", longDescription); 479 })); 480 } 481 482 @Test testParseOptions()483 void testParseOptions() { 484 String[] argv = {"---unitTest"}; 485 assertDoesNotThrow(() -> Indexer.parseOptions(argv)); 486 } 487 } 488