xref: /OpenGrok/opengrok-indexer/src/test/java/org/opengrok/indexer/util/OptionParserTest.java (revision 779ff0e712da446608ce82a0ad544e1fad32d7ce)
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