View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.AbstractPathTestSupport.addEndOfLine;
24  import static com.puppycrawl.tools.checkstyle.internal.utils.TestUtil.isUtilsClassHasPrivateConstructor;
25  import static org.junit.jupiter.api.Assumptions.assumeTrue;
26  import static org.mockito.Mockito.mock;
27  import static org.mockito.Mockito.mockStatic;
28  import static org.mockito.Mockito.verify;
29  
30  import java.io.BufferedReader;
31  import java.io.ByteArrayOutputStream;
32  import java.io.File;
33  import java.io.IOException;
34  import java.io.PrintStream;
35  import java.lang.reflect.Method;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.nio.file.Path;
39  import java.util.ArrayList;
40  import java.util.List;
41  import java.util.Locale;
42  import java.util.logging.Handler;
43  import java.util.logging.Level;
44  import java.util.logging.Logger;
45  import java.util.regex.Pattern;
46  import java.util.stream.Collectors;
47  
48  import org.itsallcode.io.Capturable;
49  import org.itsallcode.junit.sysextensions.SystemErrGuard;
50  import org.itsallcode.junit.sysextensions.SystemErrGuard.SysErr;
51  import org.itsallcode.junit.sysextensions.SystemOutGuard;
52  import org.itsallcode.junit.sysextensions.SystemOutGuard.SysOut;
53  import org.junit.jupiter.api.BeforeEach;
54  import org.junit.jupiter.api.Test;
55  import org.junit.jupiter.api.extension.ExtendWith;
56  import org.junit.jupiter.api.io.TempDir;
57  import org.mockito.MockedStatic;
58  import org.mockito.Mockito;
59  
60  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
61  import com.puppycrawl.tools.checkstyle.api.AuditListener;
62  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
63  import com.puppycrawl.tools.checkstyle.api.Violation;
64  import com.puppycrawl.tools.checkstyle.internal.testmodules.TestRootModuleChecker;
65  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
66  import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
67  
68  @ExtendWith({SystemErrGuard.class, SystemOutGuard.class})
69  public class MainTest {
70  
71      private static final String SHORT_USAGE = String.format(Locale.ROOT,
72              "Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...%n"
73              + "Try 'checkstyle --help' for more information.%n");
74  
75      private static final String USAGE = String.format(Locale.ROOT,
76            "Usage: checkstyle [-dEghjJtTV] [-b=<xpath>] [-c=<configurationFile>] "
77                    + "[-f=<format>]%n"
78                    + "                  [-o=<outputPath>] [-p=<propertiesFile>] "
79                    + "[-s=<suppressionLineColumnNumber>]%n"
80                    + "                  [-w=<tabWidth>] [-e=<exclude>]... [-x=<excludeRegex>]... "
81                    + "<files or folders>...%n"
82                    + "Checkstyle verifies that the specified source code files adhere to the"
83                    + " specified rules. By default,%n"
84                    + "violations are reported to standard out in plain format. Checkstyle requires"
85                    + " a configuration XML%n"
86                    + "file that configures the checks to apply.%n"
87                    + "      <files or folders>... One or more source files to verify%n"
88                    + "  -b, --branch-matching-xpath=<xpath>%n"
89                    + "                            Shows Abstract Syntax Tree(AST) branches that"
90                    + " match given XPath query.%n"
91                    + "  -c=<configurationFile>    Specifies the location of the file that defines"
92                    + " the configuration%n"
93                    + "                              modules. The location can either be a"
94                    + " filesystem location, or a name%n"
95                    + "                              passed to the ClassLoader.getResource()"
96                    + " method.%n"
97                    + "  -d, --debug               Prints all debug logging of CheckStyle utility.%n"
98                    + "  -e, --exclude=<exclude>   Directory/file to exclude from CheckStyle. The"
99                    + " path can be the full,%n"
100                   + "                              absolute path, or relative to the current"
101                   + " path. Multiple excludes are%n"
102                   + "                              allowed.%n"
103                   + "  -E, --executeIgnoredModules%n"
104                   + "                            Allows ignored modules to be run.%n"
105                   + "  -f=<format>               Specifies the output format. Valid values: "
106                   + "xml, sarif, plain for%n"
107                   + "                              XMLLogger, SarifLogger, and "
108                   + "DefaultLogger respectively. Defaults to%n"
109                   + "                              plain.%n"
110                   + "  -g, --generate-xpath-suppression%n"
111                   + "                            Generates to output a suppression xml to use"
112                   + " to suppress all violations%n"
113                   + "                              from user's config. Instead of printing every"
114                   + " violation, all%n"
115                   + "                              violations will be catched and single"
116                   + " suppressions xml file will be%n"
117                   + "                              printed out. Used only with -c option. Output"
118                   + " location can be%n"
119                   + "                              specified with -o option.%n"
120                   + "  -h, --help                Show this help message and exit.%n"
121                   + "  -j, --javadocTree         This option is used to print the Parse Tree of"
122                   + " the Javadoc comment. The%n"
123                   + "                              file has to contain only Javadoc comment"
124                   + " content excluding '/**' and%n"
125                   + "                              '*/' at the beginning and at the end"
126                   + " respectively. It can only be%n"
127                   + "                              used on a single file and cannot be"
128                   + " combined with other options.%n"
129                   + "  -J, --treeWithJavadoc     This option is used to display the Abstract"
130                   + " Syntax Tree (AST) with%n"
131                   + "                              Javadoc nodes of the specified file. It can"
132                   + " only be used on a single%n"
133                   + "                              file and cannot be combined"
134                   + " with other options.%n"
135                   + "  -o=<outputPath>           Sets the output file. Defaults to stdout.%n"
136                   + "  -p=<propertiesFile>       Sets the property files to load.%n"
137                   + "  -s=<suppressionLineColumnNumber>%n"
138                   + "                            Prints xpath suppressions at the file's line and"
139                   + " column position.%n"
140                   + "                              Argument is the line and column number"
141                   + " (separated by a : ) in the%n"
142                   + "                              file that the suppression should be generated"
143                   + " for. The option cannot%n"
144                   + "                              be used with other options and requires exactly"
145                   + " one file to run on to%n"
146                   + "                              be specified. Note that the generated result"
147                   + " will have few queries,%n"
148                   + "                              joined by pipe(|). Together they will match all"
149                   + " AST nodes on%n"
150                   + "                              specified line and column. You need to choose"
151                   + " only one and recheck%n"
152                   + "                              that it works. Usage of all of them is also ok,"
153                   + " but might result in%n"
154                   + "                              undesirable matching and suppress other"
155                   + " issues.%n"
156                   + "  -t, --tree                This option is used to display the Abstract"
157                   + " Syntax Tree (AST) without%n"
158                   + "                              any comments of the specified file. It can"
159                   + " only be used on a single%n"
160                   + "                              file and cannot be combined with"
161                   + " other options.%n"
162                   + "  -T, --treeWithComments    This option is used to display the Abstract"
163                   + " Syntax Tree (AST) with%n"
164                   + "                              comment nodes excluding Javadoc of the"
165                   + " specified file. It can only be%n"
166                   + "                              used on a single file and cannot be combined"
167                   + " with other options.%n"
168                   + "  -V, --version             Print version information and exit.%n"
169                   + "  -w, --tabWidth=<tabWidth> Sets the length of the tab character. Used only"
170                   + " with -s option. Default%n"
171                   + "                              value is 8.%n"
172                   + "  -x, --exclude-regexp=<excludeRegex>%n"
173                   + "                            Directory/file pattern to exclude from CheckStyle."
174                   + " Multiple excludes%n"
175                   + "                              are allowed.%n");
176 
177     private static final Logger LOG = Logger.getLogger(MainTest.class.getName()).getParent();
178     private static final Handler[] HANDLERS = LOG.getHandlers();
179     private static final Level ORIGINAL_LOG_LEVEL = LOG.getLevel();
180 
181     private static final String EOL = System.lineSeparator();
182 
183     @TempDir
184     public File temporaryFolder;
185 
186     private final LocalizedMessage auditStartMessage = new LocalizedMessage(
187             Definitions.CHECKSTYLE_BUNDLE, getClass(),
188             "DefaultLogger.auditStarted");
189 
190     private final LocalizedMessage auditFinishMessage = new LocalizedMessage(
191             Definitions.CHECKSTYLE_BUNDLE, getClass(),
192             "DefaultLogger.auditFinished");
193 
194     private final String noViolationsOutput = auditStartMessage.getMessage() + EOL
195                     + auditFinishMessage.getMessage() + EOL;
196 
197     private static String getPath(String filename) {
198         return "src/test/resources/com/puppycrawl/tools/checkstyle/main/" + filename;
199     }
200 
201     private static String getNonCompilablePath(String filename) {
202         return "src/test/resources-noncompilable/com/puppycrawl/tools/checkstyle/main/" + filename;
203     }
204 
205     private static String getFilePath(String filename) throws IOException {
206         return new File(getPath(filename)).getCanonicalPath();
207     }
208 
209     /**
210      * Configures the environment for each test.
211      * <ul>
212      * <li>Restore original logging level and HANDLERS to prevent bleeding into other tests;</li>
213      * <li>Turn off colors for picocli to not conflict with tests if they are auto turned on.</li>
214      * <li>Start output capture for {@link System#err} and {@link System#out}</li>
215      * </ul>
216      *
217      * @param systemErr wrapper for {@code System.err}
218      * @param systemOut wrapper for {@code System.out}
219      */
220     @BeforeEach
221     public void setUp(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
222         systemErr.captureMuted();
223         systemOut.captureMuted();
224 
225         System.setProperty("picocli.ansi", "false");
226 
227         LOG.setLevel(ORIGINAL_LOG_LEVEL);
228 
229         for (Handler handler : LOG.getHandlers()) {
230             boolean found = false;
231 
232             for (Handler savedHandler : HANDLERS) {
233                 if (handler == savedHandler) {
234                     found = true;
235                     break;
236                 }
237             }
238 
239             if (!found) {
240                 LOG.removeHandler(handler);
241             }
242         }
243     }
244 
245     @Test
246     public void testIsProperUtilsClass() throws ReflectiveOperationException {
247         assertWithMessage("Constructor is not private")
248                 .that(isUtilsClassHasPrivateConstructor(Main.class))
249                 .isTrue();
250     }
251 
252     @Test
253     public void testVersionPrint(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
254         assertMainReturnCode(0, "-V");
255         assertWithMessage("Unexpected output log")
256             .that(systemOut.getCapturedData())
257             .isEqualTo("Checkstyle version: null" + System.lineSeparator());
258         assertWithMessage("Unexpected system error log")
259             .that(systemErr.getCapturedData())
260             .isEqualTo("");
261     }
262 
263     @Test
264     public void testUsageHelpPrint(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
265         assertMainReturnCode(0, "-h");
266         assertWithMessage("Unexpected output log")
267             .that(systemOut.getCapturedData())
268             .isEqualTo(USAGE);
269         assertWithMessage("Unexpected system error log")
270             .that(systemErr.getCapturedData())
271             .isEqualTo("");
272     }
273 
274     @Test
275     public void testWrongArgument(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
276         // need to specify a file:
277         // <files> is defined as a required positional param;
278         // picocli verifies required parameters before checking unknown options
279         assertMainReturnCode(-1, "-q", "file");
280         final String usage = "Unknown option: '-q'" + EOL + SHORT_USAGE;
281         assertWithMessage("Unexpected output log")
282             .that(systemOut.getCapturedData())
283             .isEqualTo("");
284         assertWithMessage("Unexpected system error log")
285             .that(systemErr.getCapturedData())
286             .isEqualTo(usage);
287     }
288 
289     @Test
290     public void testWrongArgumentMissingFiles(@SysErr Capturable systemErr,
291             @SysOut Capturable systemOut) {
292         assertMainReturnCode(-1, "-q");
293         // files is defined as a required positional param;
294         // picocli verifies required parameters before checking unknown options
295         final String usage = "Missing required parameter: '<files or folders>'" + EOL + SHORT_USAGE;
296         assertWithMessage("Unexpected output log")
297             .that(systemOut.getCapturedData())
298             .isEqualTo("");
299         assertWithMessage("Unexpected system error log")
300             .that(systemErr.getCapturedData())
301             .isEqualTo(usage);
302     }
303 
304     @Test
305     public void testNoConfigSpecified(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
306         assertMainReturnCode(-1, getPath("InputMain.java"));
307         assertWithMessage("Unexpected output log")
308             .that(systemOut.getCapturedData())
309             .isEqualTo("Must specify a config XML file." + System.lineSeparator());
310         assertWithMessage("Unexpected system error log")
311             .that(systemErr.getCapturedData())
312             .isEqualTo("");
313     }
314 
315     @Test
316     public void testNonExistentTargetFile(@SysErr Capturable systemErr,
317             @SysOut Capturable systemOut) {
318         assertMainReturnCode(-1, "-c", "/google_checks.xml", "NonExistentFile.java");
319         assertWithMessage("Unexpected output log")
320             .that(systemOut.getCapturedData())
321             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
322         assertWithMessage("Unexpected system error log")
323             .that(systemErr.getCapturedData())
324             .isEqualTo("");
325     }
326 
327     @Test
328     public void testExistingTargetFileButWithoutReadAccess(
329             @SysErr Capturable systemErr, @SysOut Capturable systemOut) throws IOException {
330         final File file = File.createTempFile(
331                 "testExistingTargetFileButWithoutReadAccess", null, temporaryFolder);
332         // skip execution if file is still readable, it is possible on some Windows machines
333         // see https://github.com/checkstyle/checkstyle/issues/7032 for details
334         assumeTrue(file.setReadable(false), "file is still readable");
335 
336         final String canonicalPath = file.getCanonicalPath();
337         assertMainReturnCode(-1, "-c", "/google_checks.xml", canonicalPath);
338         assertWithMessage("Unexpected output log")
339             .that(systemOut.getCapturedData())
340             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
341         assertWithMessage("Unexpected system error log")
342             .that(systemErr.getCapturedData())
343             .isEqualTo("");
344     }
345 
346     @Test
347     public void testCustomSeverityVariableForGoogleConfig(@SysOut Capturable systemOut) {
348         assertMainReturnCode(1, "-c", "/google_checks.xml",
349                 "-p", getPath("InputMainCustomSeverityForGoogleConfig.properties"),
350                 getPath("InputMainCustomSeverityForGoogleConfig.java"));
351 
352         final String expectedOutputStart = addEndOfLine(auditStartMessage.getMessage())
353             + "[ERROR] ";
354         final String expectedOutputEnd = addEndOfLine(
355                 "InputMainCustomSeverityForGoogleConfig.java:3:1:"
356                     + " Missing a Javadoc comment. [MissingJavadocType]",
357                 auditFinishMessage.getMessage());
358         assertWithMessage("Unexpected output log")
359             .that(systemOut.getCapturedData())
360             .startsWith(expectedOutputStart);
361         assertWithMessage("Unexpected output log")
362             .that(systemOut.getCapturedData())
363             .endsWith(expectedOutputEnd);
364     }
365 
366     @Test
367     public void testDefaultSeverityVariableForGoogleConfig(@SysOut Capturable systemOut) {
368         assertMainReturnCode(0, "-c", "/google_checks.xml",
369                 getPath("InputMainCustomSeverityForGoogleConfig.java"));
370 
371         final String expectedOutputStart = addEndOfLine(auditStartMessage.getMessage())
372                 + "[WARN] ";
373         final String expectedOutputEnd = addEndOfLine(
374                 "InputMainCustomSeverityForGoogleConfig.java:3:1:"
375                         + " Missing a Javadoc comment. [MissingJavadocType]",
376                 auditFinishMessage.getMessage());
377         assertWithMessage("Unexpected output log")
378                 .that(systemOut.getCapturedData())
379                 .startsWith(expectedOutputStart);
380         assertWithMessage("Unexpected output log")
381                 .that(systemOut.getCapturedData())
382                 .endsWith(expectedOutputEnd);
383     }
384 
385     @Test
386     public void testNonExistentConfigFile(@SysErr Capturable systemErr,
387             @SysOut Capturable systemOut) {
388         assertMainReturnCode(-1, "-c", "src/main/resources/non_existent_config.xml",
389                     getPath("InputMain.java"));
390         assertWithMessage("Unexpected output log")
391             .that(systemOut.getCapturedData())
392             .isEqualTo(addEndOfLine("Could not find config XML file "
393                     + "'src/main/resources/non_existent_config.xml'."));
394         assertWithMessage("Unexpected system error log")
395             .that(systemErr.getCapturedData())
396             .isEqualTo("");
397     }
398 
399     @Test
400     public void testNonExistentOutputFormat(@SysErr Capturable systemErr,
401             @SysOut Capturable systemOut) {
402         assertMainReturnCode(-1, "-c", "/google_checks.xml", "-f", "xmlp",
403                 getPath("InputMain.java"));
404         assertWithMessage("Unexpected output log")
405             .that(systemOut.getCapturedData())
406             .isEqualTo("");
407         assertWithMessage("Unexpected system error log")
408             .that(systemErr.getCapturedData())
409             .isEqualTo("Invalid value for option '-f': expected one of [XML, SARIF, PLAIN]"
410                     + " (case-insensitive) but was 'xmlp'" + EOL + SHORT_USAGE);
411     }
412 
413     @Test
414     public void testNonExistentClass(@SysErr Capturable systemErr) {
415         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-non-existent-classname.xml"),
416                     getPath("InputMain.java"));
417         final String cause = "com.puppycrawl.tools.checkstyle.api.CheckstyleException:"
418                 + " cannot initialize module TreeWalker - ";
419         assertWithMessage("Unexpected system error log")
420                 .that(systemErr.getCapturedData())
421                 .startsWith(cause);
422     }
423 
424     @Test
425     public void testExistingTargetFile(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
426         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"),
427                 getPath("InputMain.java"));
428         assertWithMessage("Unexpected output log")
429             .that(systemOut.getCapturedData())
430             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
431                 auditFinishMessage.getMessage()));
432         assertWithMessage("Unexpected system error log")
433             .that(systemErr.getCapturedData())
434             .isEqualTo("");
435     }
436 
437     @Test
438     public void testExistingTargetFileXmlOutput(@SysErr Capturable systemErr,
439             @SysOut Capturable systemOut) throws IOException {
440         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "xml",
441                 getPath("InputMain.java"));
442         final String expectedPath = getFilePath("InputMain.java");
443         final String version = Main.class.getPackage().getImplementationVersion();
444         assertWithMessage("Unexpected output log")
445             .that(systemOut.getCapturedData())
446             .isEqualTo(addEndOfLine(
447             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
448                 "<checkstyle version=\"" + version + "\">",
449                 "<file name=\"" + expectedPath + "\">",
450                 "</file>",
451                 "</checkstyle>"));
452         assertWithMessage("Unexpected system error log")
453             .that(systemErr.getCapturedData())
454             .isEqualTo("");
455     }
456 
457     /**
458      * This test method is created only to cover
459      * pitest mutation survival at {@code Main#getOutputStreamOptions}.
460      * Parameters {@code systemErr} and {@code systemOut} are used to restore
461      * the original system streams.
462      *
463      * @param systemErr the system error stream
464      * @param systemOut the system output stream
465      */
466     @Test
467     public void testNonClosedSystemStreams(@SysErr Capturable systemErr,
468            @SysOut Capturable systemOut) {
469         try (ShouldNotBeClosedStream stream = new ShouldNotBeClosedStream()) {
470             System.setOut(stream);
471             System.setErr(stream);
472             assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "xml",
473                     getPath("InputMain.java"));
474             assertWithMessage("stream should not be closed")
475                 .that(stream.isClosed)
476                 .isFalse();
477             assertWithMessage("System.err should be not used")
478                 .that(systemErr.getCapturedData())
479                 .isEmpty();
480             assertWithMessage("System.out should be not used")
481                 .that(systemOut.getCapturedData())
482                 .isEmpty();
483         }
484     }
485 
486     /**
487      * This test method is created only to cover
488      * pitest mutation survival at Main#getOutputStreamOptions.
489      * No ability to test it by out general tests.
490      * It is hard test that inner stream is closed, so pure UT is created to validate result
491      * of private method Main.getOutputStreamOptions
492      *
493      * @throws Exception if there is an error.
494      */
495     @Test
496     public void testGetOutputStreamOptionsMethod() throws Exception {
497         final Path path = new File(getPath("InputMain.java")).toPath();
498         final OutputStreamOptions option =
499                 TestUtil.invokeStaticMethod(Main.class, "getOutputStreamOptions", path);
500         assertWithMessage("Main.getOutputStreamOptions return CLOSE on not null Path")
501                 .that(option)
502                 .isEqualTo(OutputStreamOptions.CLOSE);
503     }
504 
505     @Test
506     public void testExistingTargetFilePlainOutput(@SysErr Capturable systemErr,
507             @SysOut Capturable systemOut) {
508         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "plain",
509                 getPath("InputMain.java"));
510         assertWithMessage("Unexpected output log")
511             .that(systemOut.getCapturedData())
512             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
513                 auditFinishMessage.getMessage()));
514         assertWithMessage("Unexpected system error log")
515             .that(systemErr.getCapturedData())
516             .isEqualTo("");
517     }
518 
519     @Test
520     public void testExistingTargetFileWithViolations(@SysErr Capturable systemErr,
521             @SysOut Capturable systemOut) throws IOException {
522         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname2.xml"),
523                 getPath("InputMain.java"));
524         final Violation invalidPatternMessageMain = new Violation(1,
525                 "com.puppycrawl.tools.checkstyle.checks.naming.messages",
526                 "name.invalidPattern", new String[] {"InputMain", "^[a-z0-9]*$"},
527                 null, getClass(), null);
528         final Violation invalidPatternMessageMainInner = new Violation(1,
529                 "com.puppycrawl.tools.checkstyle.checks.naming.messages",
530                 "name.invalidPattern", new String[] {"InputMainInner", "^[a-z0-9]*$"},
531                 null, getClass(), null);
532         final String expectedPath = getFilePath("InputMain.java");
533         assertWithMessage("Unexpected output log")
534             .that(systemOut.getCapturedData())
535             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
536                     "[WARN] " + expectedPath + ":3:14: "
537                         + invalidPatternMessageMain.getViolation()
538                         + " [TypeName]",
539                     "[WARN] " + expectedPath + ":5:7: "
540                         + invalidPatternMessageMainInner.getViolation()
541                         + " [TypeName]",
542                     auditFinishMessage.getMessage()));
543         assertWithMessage("Unexpected system error log")
544             .that(systemErr.getCapturedData())
545             .isEqualTo("");
546     }
547 
548     @Test
549     public void testViolationsByGoogleAndXpathSuppressions(@SysErr Capturable systemErr,
550             @SysOut Capturable systemOut) {
551         System.setProperty("org.checkstyle.google.suppressionxpathfilter.config",
552                 getPath("InputMainViolationsForGoogleXpathSuppressions.xml"));
553         assertMainReturnCode(0, "-c", "/google_checks.xml",
554                 getPath("InputMainViolationsForGoogle.java"));
555         assertWithMessage("Unexpected output log")
556             .that(systemOut.getCapturedData())
557             .isEqualTo(noViolationsOutput);
558         assertWithMessage("Unexpected system error log")
559             .that(systemErr.getCapturedData())
560             .isEqualTo("");
561     }
562 
563     @Test
564     public void testViolationsByGoogleAndSuppressions(@SysErr Capturable systemErr,
565             @SysOut Capturable systemOut) {
566         System.setProperty("org.checkstyle.google.suppressionfilter.config",
567                 getPath("InputMainViolationsForGoogleSuppressions.xml"));
568         assertMainReturnCode(0, "-c", "/google_checks.xml",
569                 getPath("InputMainViolationsForGoogle.java"));
570         assertWithMessage("Unexpected output log")
571             .that(systemOut.getCapturedData())
572             .isEqualTo(noViolationsOutput);
573         assertWithMessage("Unexpected system error log")
574             .that(systemErr.getCapturedData())
575             .isEqualTo("");
576     }
577 
578     @Test
579     public void testExistingTargetFileWithError(@SysErr Capturable systemErr,
580             @SysOut Capturable systemOut) throws Exception {
581         assertMainReturnCode(2, "-c", getPath("InputMainConfig-classname2-error.xml"),
582                     getPath("InputMain.java"));
583         final Violation errorCounterTwoMessage = new Violation(1,
584                 Definitions.CHECKSTYLE_BUNDLE, Main.ERROR_COUNTER,
585                 new String[] {String.valueOf(2)}, null, getClass(), null);
586         final Violation invalidPatternMessageMain = new Violation(1,
587                 "com.puppycrawl.tools.checkstyle.checks.naming.messages",
588                 "name.invalidPattern", new String[] {"InputMain", "^[a-z0-9]*$"},
589                 null, getClass(), null);
590         final Violation invalidPatternMessageMainInner = new Violation(1,
591                 "com.puppycrawl.tools.checkstyle.checks.naming.messages",
592                 "name.invalidPattern", new String[] {"InputMainInner", "^[a-z0-9]*$"},
593                 null, getClass(), null);
594         final String expectedPath = getFilePath("InputMain.java");
595         assertWithMessage("Unexpected output log")
596             .that(systemOut.getCapturedData())
597             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
598                     "[ERROR] " + expectedPath + ":3:14: "
599                         + invalidPatternMessageMain.getViolation() + " [TypeName]",
600                     "[ERROR] " + expectedPath + ":5:7: "
601                         + invalidPatternMessageMainInner.getViolation() + " [TypeName]",
602                     auditFinishMessage.getMessage()));
603         assertWithMessage("Unexpected system error log")
604             .that(systemErr.getCapturedData())
605             .isEqualTo(addEndOfLine(errorCounterTwoMessage.getViolation()));
606     }
607 
608     /**
609      * Similar test to {@link #testExistingTargetFileWithError}, but for PIT mutation tests:
610      * this test fails if the boundary condition is changed from {@code if (exitStatus > 0)}
611      * to {@code if (exitStatus > 1)}.
612      *
613      * @throws Exception should not throw anything
614      */
615     @Test
616     public void testExistingTargetFileWithOneError(@SysErr Capturable systemErr,
617             @SysOut Capturable systemOut) throws Exception {
618         assertMainReturnCode(1, "-c", getPath("InputMainConfig-classname2-error.xml"),
619                     getPath("InputMain1.java"));
620         final Violation errorCounterTwoMessage = new Violation(1,
621                 Definitions.CHECKSTYLE_BUNDLE, Main.ERROR_COUNTER,
622                 new String[] {String.valueOf(1)}, null, getClass(), null);
623         final Violation invalidPatternMessageMain = new Violation(1,
624                 "com.puppycrawl.tools.checkstyle.checks.naming.messages",
625                 "name.invalidPattern", new String[] {"InputMain1", "^[a-z0-9]*$"},
626                 null, getClass(), null);
627         final String expectedPath = getFilePath("InputMain1.java");
628         assertWithMessage("Unexpected output log")
629             .that(systemOut.getCapturedData())
630             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
631                     "[ERROR] " + expectedPath + ":3:14: "
632                         + invalidPatternMessageMain.getViolation() + " [TypeName]",
633                     auditFinishMessage.getMessage()));
634         assertWithMessage("Unexpected system error log")
635             .that(systemErr.getCapturedData())
636             .isEqualTo(addEndOfLine(errorCounterTwoMessage.getViolation()));
637     }
638 
639     @Test
640     public void testExistingTargetFileWithOneErrorAgainstSunCheck(@SysErr Capturable systemErr,
641             @SysOut Capturable systemOut) throws Exception {
642         assertMainReturnCode(1, "-c", "/sun_checks.xml", getPath("InputMain1.java"));
643         final Violation errorCounterTwoMessage = new Violation(1,
644                 Definitions.CHECKSTYLE_BUNDLE, Main.ERROR_COUNTER,
645                 new String[] {String.valueOf(1)}, null, getClass(), null);
646         final Violation message = new Violation(1,
647                 "com.puppycrawl.tools.checkstyle.checks.javadoc.messages",
648                 "javadoc.packageInfo", new String[] {},
649                 null, getClass(), null);
650         final String expectedPath = getFilePath("InputMain1.java");
651         assertWithMessage("Unexpected output log")
652             .that(systemOut.getCapturedData())
653             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
654                 "[ERROR] " + expectedPath + ":1: " + message.getViolation() + " [JavadocPackage]",
655                 auditFinishMessage.getMessage()));
656         assertWithMessage("Unexpected system error log")
657             .that(systemErr.getCapturedData())
658             .isEqualTo(addEndOfLine(errorCounterTwoMessage.getViolation()));
659     }
660 
661     @Test
662     public void testExistentTargetFilePlainOutputToNonExistentFile(@SysErr Capturable systemErr,
663             @SysOut Capturable systemOut) {
664         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "plain",
665                 "-o", temporaryFolder + "/output.txt", getPath("InputMain.java"));
666         assertWithMessage("Unexpected output log")
667             .that(systemOut.getCapturedData())
668             .isEqualTo("");
669         assertWithMessage("Unexpected system error log")
670             .that(systemErr.getCapturedData())
671             .isEqualTo("");
672     }
673 
674     @Test
675     public void testExistingTargetFilePlainOutputToFile(@SysErr Capturable systemErr,
676             @SysOut Capturable systemOut) throws Exception {
677         final String outputFile =
678                 File.createTempFile("file", ".output", temporaryFolder).getCanonicalPath();
679         assertWithMessage("File must exist")
680                 .that(new File(outputFile).exists())
681                 .isTrue();
682         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "plain",
683                 "-o", outputFile, getPath("InputMain.java"));
684         assertWithMessage("Unexpected output log")
685             .that(systemOut.getCapturedData())
686             .isEqualTo("");
687         assertWithMessage("Unexpected system error log")
688             .that(systemErr.getCapturedData())
689             .isEqualTo("");
690     }
691 
692     @Test
693     public void testCreateNonExistentOutputFile() throws IOException {
694         final String outputFile = new File(temporaryFolder, "nonexistent.out").getCanonicalPath();
695         assertWithMessage("File must not exist")
696                 .that(new File(outputFile).exists())
697                 .isFalse();
698         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname.xml"), "-f", "plain",
699                 "-o", outputFile, getPath("InputMain.java"));
700         assertWithMessage("File must exist")
701                 .that(new File(outputFile).exists())
702                 .isTrue();
703     }
704 
705     @Test
706     public void testExistingTargetFilePlainOutputProperties(@SysErr Capturable systemErr,
707             @SysOut Capturable systemOut) {
708         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname-prop.xml"),
709                 "-p", getPath("InputMainMycheckstyle.properties"), getPath("InputMain.java"));
710         assertWithMessage("Unexpected output log")
711             .that(systemOut.getCapturedData())
712             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
713                 auditFinishMessage.getMessage()));
714         assertWithMessage("Unexpected system error log")
715             .that(systemErr.getCapturedData())
716             .isEqualTo("");
717     }
718 
719     @Test
720     public void testPropertyFileWithPropertyChaining(@SysErr Capturable systemErr,
721             @SysOut Capturable systemOut) {
722         assertMainReturnCode(0, "-c", getPath("InputMainConfig-classname-prop.xml"),
723             "-p", getPath("InputMainPropertyChaining.properties"), getPath("InputMain.java"));
724 
725         assertWithMessage("Unexpected output log")
726             .that(systemOut.getCapturedData())
727             .isEqualTo(addEndOfLine(auditStartMessage.getMessage(),
728                 auditFinishMessage.getMessage()));
729         assertWithMessage("Unexpected system error log")
730             .that(systemErr.getCapturedData())
731             .isEqualTo("");
732     }
733 
734     @Test
735     public void testPropertyFileWithPropertyChainingUndefinedProperty(@SysErr Capturable systemErr,
736             @SysOut Capturable systemOut) {
737         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-classname-prop.xml"),
738                 "-p", getPath("InputMainPropertyChainingUndefinedProperty.properties"),
739                 getPath("InputMain.java"));
740 
741         assertWithMessage("Invalid error message")
742             .that(systemErr.getCapturedData())
743             .contains(ChainedPropertyUtil.UNDEFINED_PROPERTY_MESSAGE);
744         assertWithMessage("Unexpected output log")
745             .that(systemOut.getCapturedData())
746             .isEqualTo("");
747     }
748 
749     @Test
750     public void testExistingTargetFilePlainOutputNonexistentProperties(@SysErr Capturable systemErr,
751             @SysOut Capturable systemOut) {
752         assertMainReturnCode(-1, "-c", getPath("InputMainConfig-classname-prop.xml"),
753                     "-p", "nonexistent.properties", getPath("InputMain.java"));
754         assertWithMessage("Unexpected output log")
755             .that(systemOut.getCapturedData())
756             .isEqualTo("Could not find file 'nonexistent.properties'."
757                 + System.lineSeparator());
758         assertWithMessage("Unexpected system error log")
759             .that(systemErr.getCapturedData())
760             .isEqualTo("");
761     }
762 
763     @Test
764     public void testExistingIncorrectConfigFile(@SysErr Capturable systemErr) {
765         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-Incorrect.xml"),
766                 getPath("InputMain.java"));
767         final String errorOutput = "com.puppycrawl.tools.checkstyle.api."
768             + "CheckstyleException: unable to parse configuration stream - ";
769         assertWithMessage("Unexpected system error log")
770                 .that(systemErr.getCapturedData())
771                 .startsWith(errorOutput);
772     }
773 
774     @Test
775     public void testExistingIncorrectChildrenInConfigFile(@SysErr Capturable systemErr) {
776         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-incorrectChildren.xml"),
777                     getPath("InputMain.java"));
778         final String errorOutput = "com.puppycrawl.tools.checkstyle.api."
779                 + "CheckstyleException: cannot initialize module RegexpSingleline"
780                 + " - RegexpSingleline is not allowed as a child in RegexpSingleline";
781         assertWithMessage("Unexpected system error log")
782                 .that(systemErr.getCapturedData())
783                 .startsWith(errorOutput);
784     }
785 
786     @Test
787     public void testExistingIncorrectChildrenInConfigFile2(@SysErr Capturable systemErr) {
788         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-incorrectChildren2.xml"),
789                     getPath("InputMain.java"));
790         final String errorOutput = "com.puppycrawl.tools.checkstyle.api."
791                 + "CheckstyleException: cannot initialize module TreeWalker - "
792                 + "cannot initialize module JavadocMethod - "
793                 + "JavadocVariable is not allowed as a child in JavadocMethod";
794         assertWithMessage("Unexpected system error log")
795                 .that(systemErr.getCapturedData())
796                 .startsWith(errorOutput);
797     }
798 
799     @Test
800     public void testLoadPropertiesIoException() throws Exception {
801         final Class<?>[] param = new Class<?>[1];
802         param[0] = File.class;
803         final Class<?> cliOptionsClass = Class.forName(Main.class.getName());
804         final Method method = cliOptionsClass.getDeclaredMethod("loadProperties", param);
805         method.setAccessible(true);
806         try {
807             method.invoke(null, new File("."));
808             assertWithMessage("Exception was expected").fail();
809         }
810         catch (ReflectiveOperationException ex) {
811             assertWithMessage("Invalid error cause")
812                     .that(ex)
813                     .hasCauseThat()
814                     .isInstanceOf(CheckstyleException.class);
815             // We do separate validation for message as in Windows
816             // disk drive letter appear in message,
817             // so we skip that drive letter for compatibility issues
818             final Violation loadPropertiesMessage = new Violation(1,
819                     Definitions.CHECKSTYLE_BUNDLE, Main.LOAD_PROPERTIES_EXCEPTION,
820                     new String[] {""}, null, getClass(), null);
821             final String causeMessage = ex.getCause().getLocalizedMessage();
822             final String violation = loadPropertiesMessage.getViolation();
823             final boolean samePrefix = causeMessage.substring(0, causeMessage.indexOf(' '))
824                     .equals(violation
825                             .substring(0, violation.indexOf(' ')));
826             final boolean sameSuffix =
827                     causeMessage.substring(causeMessage.lastIndexOf(' '))
828                     .equals(violation
829                             .substring(violation.lastIndexOf(' ')));
830             assertWithMessage("Invalid violation")
831                     .that(samePrefix || sameSuffix)
832                     .isTrue();
833             assertWithMessage("Invalid violation")
834                     .that(causeMessage)
835                     .contains(".'");
836         }
837     }
838 
839     @Test
840     public void testExistingDirectoryWithViolations(@SysErr Capturable systemErr,
841             @SysOut Capturable systemOut) throws IOException {
842         // we just reference there all violations
843         final String[][] outputValues = {
844                 {"InputMainComplexityOverflow", "1", "172"},
845         };
846 
847         final int allowedLength = 170;
848         final String msgKey = "maxLen.file";
849         final String bundle = "com.puppycrawl.tools.checkstyle.checks.sizes.messages";
850 
851         assertMainReturnCode(0, "-c", getPath("InputMainConfig-filelength.xml"),
852                 getPath(""));
853         final String expectedPath = getFilePath("") + File.separator;
854         final StringBuilder sb = new StringBuilder(28);
855         sb.append(auditStartMessage.getMessage())
856                 .append(EOL);
857         final String format = "[WARN] " + expectedPath + outputValues[0][0] + ".java:"
858                 + outputValues[0][1] + ": ";
859         for (String[] outputValue : outputValues) {
860             final String violation = new Violation(1, bundle,
861                     msgKey, new Integer[] {Integer.valueOf(outputValue[2]), allowedLength},
862                     null, getClass(), null).getViolation();
863             final String line = format + violation + " [FileLength]";
864             sb.append(line).append(EOL);
865         }
866         sb.append(auditFinishMessage.getMessage())
867                 .append(EOL);
868         assertWithMessage("Unexpected output log")
869             .that(systemOut.getCapturedData())
870             .isEqualTo(sb.toString());
871         assertWithMessage("Unexpected system error log")
872             .that(systemErr.getCapturedData())
873             .isEqualTo("");
874     }
875 
876     /**
877      * Test doesn't need to be serialized.
878      *
879      * @noinspection SerializableInnerClassWithNonSerializableOuterClass
880      * @noinspectionreason SerializableInnerClassWithNonSerializableOuterClass - mocked file
881      *      for test does not require serialization
882      */
883     @Test
884     public void testListFilesNotFile() throws Exception {
885         final File fileMock = new File("") {
886             private static final long serialVersionUID = 1L;
887 
888             @Override
889             public boolean canRead() {
890                 return true;
891             }
892 
893             @Override
894             public boolean isDirectory() {
895                 return false;
896             }
897 
898             @Override
899             public boolean isFile() {
900                 return false;
901             }
902         };
903 
904         final List<File> result = TestUtil.invokeStaticMethod(Main.class, "listFiles",
905                 fileMock, new ArrayList<>());
906         assertWithMessage("Invalid result size")
907             .that(result)
908             .isEmpty();
909     }
910 
911     /**
912      * Test doesn't need to be serialized.
913      *
914      * @noinspection SerializableInnerClassWithNonSerializableOuterClass
915      * @noinspectionreason SerializableInnerClassWithNonSerializableOuterClass - mocked file
916      *      for test does not require serialization
917      */
918     @Test
919     public void testListFilesDirectoryWithNull() throws Exception {
920         final File[] nullResult = null;
921         final File fileMock = new File("") {
922             private static final long serialVersionUID = 1L;
923 
924             @Override
925             public boolean canRead() {
926                 return true;
927             }
928 
929             @Override
930             public boolean isDirectory() {
931                 return true;
932             }
933 
934             @Override
935             public File[] listFiles() {
936                 return nullResult;
937             }
938         };
939 
940         final List<File> result = TestUtil.invokeStaticMethod(Main.class, "listFiles",
941                 fileMock, new ArrayList<>());
942         assertWithMessage("Invalid result size")
943             .that(result)
944             .isEmpty();
945     }
946 
947     @Test
948     public void testFileReferenceDuringException(@SysErr Capturable systemErr) {
949         // We put xml as source to cause parse exception
950         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-classname.xml"),
951                     getNonCompilablePath("InputMainIncorrectClass.java"));
952         final String exceptionMessage = addEndOfLine("com.puppycrawl.tools.checkstyle.api."
953                 + "CheckstyleException: Exception was thrown while processing "
954                 + new File(getNonCompilablePath("InputMainIncorrectClass.java")).getPath());
955         assertWithMessage("Unexpected system error log")
956                 .that(systemErr.getCapturedData())
957                 .contains(exceptionMessage);
958     }
959 
960     @Test
961     public void testRemoveLexerDefaultErrorListener(@SysErr Capturable systemErr) {
962         assertMainReturnCode(-2, "-t", getNonCompilablePath("InputMainIncorrectClass.java"));
963 
964         assertWithMessage("First line of exception message should not contain lexer error.")
965             .that(systemErr.getCapturedData().startsWith("line 2:2 token recognition error"))
966                 .isFalse();
967     }
968 
969     @Test
970     public void testRemoveParserDefaultErrorListener(@SysErr Capturable systemErr) {
971         assertMainReturnCode(-2, "-t", getNonCompilablePath("InputMainIncorrectClass.java"));
972         final String capturedData = systemErr.getCapturedData();
973 
974         assertWithMessage("First line of exception message should not contain parser error.")
975             .that(capturedData.startsWith("line 2:0 no viable alternative"))
976                 .isFalse();
977         assertWithMessage("Second line of exception message should not contain parser error.")
978             .that(capturedData.startsWith("line 2:0 no viable alternative",
979                     capturedData.indexOf('\n') + 1))
980                 .isFalse();
981     }
982 
983     @Test
984     public void testPrintTreeOnMoreThanOneFile(@SysErr Capturable systemErr,
985             @SysOut Capturable systemOut) {
986         assertMainReturnCode(-1, "-t", getPath(""));
987         assertWithMessage("Unexpected output log")
988             .that(systemOut.getCapturedData())
989             .isEqualTo("Printing AST is allowed for only one file." + System.lineSeparator());
990         assertWithMessage("Unexpected system error log")
991             .that(systemErr.getCapturedData())
992             .isEqualTo("");
993     }
994 
995     @Test
996     public void testPrintTreeOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
997         final String expected = addEndOfLine(
998             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
999             "|--PACKAGE_DEF -> package [1:0]",
1000             "|   |--ANNOTATIONS -> ANNOTATIONS [1:39]",
1001             "|   |--DOT -> . [1:39]",
1002             "|   |   |--DOT -> . [1:28]",
1003             "|   |   |   |--DOT -> . [1:22]",
1004             "|   |   |   |   |--DOT -> . [1:11]",
1005             "|   |   |   |   |   |--IDENT -> com [1:8]",
1006             "|   |   |   |   |   `--IDENT -> puppycrawl [1:12]",
1007             "|   |   |   |   `--IDENT -> tools [1:23]",
1008             "|   |   |   `--IDENT -> checkstyle [1:29]",
1009             "|   |   `--IDENT -> main [1:40]",
1010             "|   `--SEMI -> ; [1:44]",
1011             "|--CLASS_DEF -> CLASS_DEF [3:0]",
1012             "|   |--MODIFIERS -> MODIFIERS [3:0]",
1013             "|   |   `--LITERAL_PUBLIC -> public [3:0]",
1014             "|   |--LITERAL_CLASS -> class [3:7]",
1015             "|   |--IDENT -> InputMain [3:13]",
1016             "|   `--OBJBLOCK -> OBJBLOCK [3:23]",
1017             "|       |--LCURLY -> { [3:23]",
1018             "|       `--RCURLY -> } [4:0]",
1019             "`--CLASS_DEF -> CLASS_DEF [5:0]",
1020             "    |--MODIFIERS -> MODIFIERS [5:0]",
1021             "    |--LITERAL_CLASS -> class [5:0]",
1022             "    |--IDENT -> InputMainInner [5:6]",
1023             "    `--OBJBLOCK -> OBJBLOCK [5:21]",
1024             "        |--LCURLY -> { [5:21]",
1025             "        `--RCURLY -> } [6:0]");
1026 
1027         assertMainReturnCode(0, "-t", getPath("InputMain.java"));
1028         assertWithMessage("Unexpected output log")
1029             .that(systemOut.getCapturedData())
1030             .isEqualTo(expected);
1031         assertWithMessage("Unexpected system error log")
1032             .that(systemErr.getCapturedData())
1033             .isEqualTo("");
1034     }
1035 
1036     @Test
1037     public void testPrintXpathOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1038         final String expected = addEndOfLine(
1039             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1040             "|--CLASS_DEF -> CLASS_DEF [3:0]",
1041             "|   `--OBJBLOCK -> OBJBLOCK [3:28]",
1042             "|       |--METHOD_DEF -> METHOD_DEF [4:4]",
1043             "|       |   `--SLIST -> { [4:20]",
1044             "|       |       |--VARIABLE_DEF -> VARIABLE_DEF [5:8]",
1045             "|       |       |   |--IDENT -> a [5:12]");
1046         assertMainReturnCode(0, "-b",
1047                 "/COMPILATION_UNIT/CLASS_DEF//METHOD_DEF[./IDENT[@text='methodOne']]"
1048                         + "//VARIABLE_DEF/IDENT",
1049                 getPath("InputMainXPath.java"));
1050         assertWithMessage("Unexpected output log")
1051             .that(systemOut.getCapturedData())
1052             .isEqualTo(expected);
1053         assertWithMessage("Unexpected system error log")
1054             .that(systemErr.getCapturedData())
1055             .isEqualTo("");
1056     }
1057 
1058     @Test
1059     public void testPrintXpathCommentNode(@SysErr Capturable systemErr,
1060             @SysOut Capturable systemOut) {
1061         final String expected = addEndOfLine(
1062             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1063             "`--CLASS_DEF -> CLASS_DEF [17:0]",
1064             "    `--OBJBLOCK -> OBJBLOCK [17:19]",
1065             "        |--CTOR_DEF -> CTOR_DEF [19:4]",
1066             "        |   |--BLOCK_COMMENT_BEGIN -> /* [18:4]");
1067         assertMainReturnCode(0, "-b", "/COMPILATION_UNIT/CLASS_DEF//BLOCK_COMMENT_BEGIN",
1068                 getPath("InputMainXPath.java"));
1069         assertWithMessage("Unexpected output log")
1070             .that(systemOut.getCapturedData())
1071             .isEqualTo(expected);
1072         assertWithMessage("Unexpected system error log")
1073             .that(systemErr.getCapturedData())
1074             .isEqualTo("");
1075     }
1076 
1077     @Test
1078     public void testPrintXpathNodeParentNull(@SysErr Capturable systemErr,
1079             @SysOut Capturable systemOut) {
1080         final String expected = addEndOfLine("COMPILATION_UNIT -> COMPILATION_UNIT [1:0]");
1081         assertMainReturnCode(0, "-b", "/COMPILATION_UNIT", getPath("InputMainXPath.java"));
1082         assertWithMessage("Unexpected output log")
1083             .that(systemOut.getCapturedData())
1084             .isEqualTo(expected);
1085         assertWithMessage("Unexpected system error log")
1086             .that(systemErr.getCapturedData())
1087             .isEqualTo("");
1088     }
1089 
1090     @Test
1091     public void testPrintXpathFullOption(
1092             @SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1093         final String expected = addEndOfLine(
1094             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1095             "|--CLASS_DEF -> CLASS_DEF [3:0]",
1096             "|   `--OBJBLOCK -> OBJBLOCK [3:28]",
1097             "|       |--METHOD_DEF -> METHOD_DEF [8:4]",
1098             "|       |   `--SLIST -> { [8:26]",
1099             "|       |       |--VARIABLE_DEF -> VARIABLE_DEF [9:8]",
1100             "|       |       |   |--IDENT -> a [9:12]");
1101         final String xpath = "/COMPILATION_UNIT/CLASS_DEF//METHOD_DEF[./IDENT[@text='method']]"
1102                 + "//VARIABLE_DEF/IDENT";
1103         assertMainReturnCode(0, "--branch-matching-xpath", xpath, getPath("InputMainXPath.java"));
1104         assertWithMessage("Unexpected output log")
1105             .that(systemOut.getCapturedData())
1106             .isEqualTo(expected);
1107         assertWithMessage("Unexpected system error log")
1108             .that(systemErr.getCapturedData())
1109             .isEqualTo("");
1110     }
1111 
1112     @Test
1113     public void testPrintXpathTwoResults(
1114             @SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1115         final String expected = addEndOfLine(
1116             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1117             "|--CLASS_DEF -> CLASS_DEF [12:0]",
1118             "|   `--OBJBLOCK -> OBJBLOCK [12:10]",
1119             "|       |--METHOD_DEF -> METHOD_DEF [13:4]",
1120             "---------",
1121             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1122             "|--CLASS_DEF -> CLASS_DEF [12:0]",
1123             "|   `--OBJBLOCK -> OBJBLOCK [12:10]",
1124             "|       |--METHOD_DEF -> METHOD_DEF [14:4]");
1125         assertMainReturnCode(0, "--branch-matching-xpath",
1126                 "/COMPILATION_UNIT/CLASS_DEF[./IDENT[@text='Two']]//METHOD_DEF",
1127                 getPath("InputMainXPath.java"));
1128         assertWithMessage("Unexpected output log")
1129             .that(systemOut.getCapturedData())
1130             .isEqualTo(expected);
1131         assertWithMessage("Unexpected system error log")
1132             .that(systemErr.getCapturedData())
1133             .isEqualTo("");
1134     }
1135 
1136     @Test
1137     public void testPrintXpathInvalidXpath(@SysErr Capturable systemErr) throws Exception {
1138         final String invalidXpath = "\\/COMPILATION_UNIT/CLASS_DEF[./IDENT[@text='Two']]"
1139                 + "//METHOD_DEF";
1140         final String filePath = getFilePath("InputMainXPath.java");
1141         assertMainReturnCode(-2, "--branch-matching-xpath", invalidXpath, filePath);
1142         final String exceptionFirstLine = addEndOfLine("com.puppycrawl.tools.checkstyle.api."
1143             + "CheckstyleException: Error during evaluation for xpath: " + invalidXpath
1144             + ", file: " + filePath);
1145         assertWithMessage("Unexpected system error log")
1146             .that(systemErr.getCapturedData())
1147             .startsWith(exceptionFirstLine);
1148     }
1149 
1150     @Test
1151     public void testPrintTreeCommentsOption(@SysErr Capturable systemErr,
1152             @SysOut Capturable systemOut) {
1153         final String expected = addEndOfLine(
1154             "COMPILATION_UNIT -> COMPILATION_UNIT [1:0]",
1155             "|--PACKAGE_DEF -> package [1:0]",
1156             "|   |--ANNOTATIONS -> ANNOTATIONS [1:39]",
1157             "|   |--DOT -> . [1:39]",
1158             "|   |   |--DOT -> . [1:28]",
1159             "|   |   |   |--DOT -> . [1:22]",
1160             "|   |   |   |   |--DOT -> . [1:11]",
1161             "|   |   |   |   |   |--IDENT -> com [1:8]",
1162             "|   |   |   |   |   `--IDENT -> puppycrawl [1:12]",
1163             "|   |   |   |   `--IDENT -> tools [1:23]",
1164             "|   |   |   `--IDENT -> checkstyle [1:29]",
1165             "|   |   `--IDENT -> main [1:40]",
1166             "|   `--SEMI -> ; [1:44]",
1167             "|--CLASS_DEF -> CLASS_DEF [3:0]",
1168             "|   |--MODIFIERS -> MODIFIERS [3:0]",
1169             "|   |   |--BLOCK_COMMENT_BEGIN -> /* [2:0]",
1170             "|   |   |   |--COMMENT_CONTENT -> comment [2:2]",
1171             "|   |   |   `--BLOCK_COMMENT_END -> */ [2:8]",
1172             "|   |   `--LITERAL_PUBLIC -> public [3:0]",
1173             "|   |--LITERAL_CLASS -> class [3:7]",
1174             "|   |--IDENT -> InputMain [3:13]",
1175             "|   `--OBJBLOCK -> OBJBLOCK [3:23]",
1176             "|       |--LCURLY -> { [3:23]",
1177             "|       `--RCURLY -> } [4:0]",
1178             "`--CLASS_DEF -> CLASS_DEF [5:0]",
1179             "    |--MODIFIERS -> MODIFIERS [5:0]",
1180             "    |--LITERAL_CLASS -> class [5:0]",
1181             "    |--IDENT -> InputMainInner [5:6]",
1182             "    `--OBJBLOCK -> OBJBLOCK [5:21]",
1183             "        |--LCURLY -> { [5:21]",
1184             "        `--RCURLY -> } [6:0]");
1185 
1186         assertMainReturnCode(0, "-T", getPath("InputMain.java"));
1187         assertWithMessage("Unexpected output log")
1188             .that(systemOut.getCapturedData())
1189             .isEqualTo(expected);
1190         assertWithMessage("Unexpected system error log")
1191             .that(systemErr.getCapturedData())
1192             .isEqualTo("");
1193     }
1194 
1195     /**
1196      * Verifies the output of the command line parameter "-j".
1197      *
1198      * @param systemErr wrapper for {@code System.err}
1199      * @param systemOut wrapper for {@code System.out}
1200      * @throws IOException if I/O exception occurs while reading the test input.
1201      * @noinspection RedundantThrows
1202      * @noinspectionreason RedundantThrows - false positive
1203      */
1204     @Test
1205     public void testPrintTreeJavadocOption(@SysErr Capturable systemErr,
1206             @SysOut Capturable systemOut) throws IOException {
1207         final String expected = Files.readString(Path.of(
1208             getPath("InputMainExpectedInputJavadocComment.txt")))
1209             .replaceAll("\\\\r\\\\n", "\\\\n").replaceAll("\r\n", "\n");
1210 
1211         assertMainReturnCode(0, "-j", getPath("InputMainJavadocComment.javadoc"));
1212         assertWithMessage("Unexpected output log")
1213             .that(systemOut.getCapturedData().replaceAll("\\\\r\\\\n", "\\\\n")
1214                         .replaceAll("\r\n", "\n"))
1215             .isEqualTo(expected);
1216         assertWithMessage("Unexpected system error log")
1217             .that(systemErr.getCapturedData())
1218             .isEqualTo("");
1219     }
1220 
1221     @Test
1222     public void testPrintSuppressionOption(@SysErr Capturable systemErr,
1223             @SysOut Capturable systemOut) {
1224         final String expected = addEndOfLine(
1225             "/COMPILATION_UNIT/CLASS_DEF[./IDENT[@text='InputMainSuppressionsStringPrinter']]",
1226                 "/COMPILATION_UNIT/CLASS_DEF[./IDENT[@text='InputMainSuppressionsStringPrinter']]"
1227                         + "/MODIFIERS",
1228                 "/COMPILATION_UNIT/CLASS_DEF[./IDENT[@text='InputMainSuppressionsStringPrinter']"
1229                         + "]/LITERAL_CLASS");
1230 
1231         assertMainReturnCode(0, getPath("InputMainSuppressionsStringPrinter.java"), "-s", "3:1");
1232         assertWithMessage("Unexpected output log")
1233             .that(systemOut.getCapturedData())
1234             .isEqualTo(expected);
1235         assertWithMessage("Unexpected system error log")
1236             .that(systemErr.getCapturedData())
1237             .isEqualTo("");
1238     }
1239 
1240     @Test
1241     public void testPrintSuppressionAndTabWidthOption(@SysErr Capturable systemErr,
1242             @SysOut Capturable systemOut) {
1243         final String expected = addEndOfLine(
1244             "/COMPILATION_UNIT/CLASS_DEF"
1245                     + "[./IDENT[@text='InputMainSuppressionsStringPrinter']]/OBJBLOCK"
1246                     + "/METHOD_DEF[./IDENT[@text='getName']]"
1247                     + "/SLIST/VARIABLE_DEF[./IDENT[@text='var']]",
1248                 "/COMPILATION_UNIT/CLASS_DEF"
1249                     + "[./IDENT[@text='InputMainSuppressionsStringPrinter']]/OBJBLOCK"
1250                     + "/METHOD_DEF[./IDENT[@text='getName']]/SLIST"
1251                     + "/VARIABLE_DEF[./IDENT[@text='var']]/MODIFIERS",
1252                 "/COMPILATION_UNIT/CLASS_DEF"
1253                     + "[./IDENT[@text='InputMainSuppressionsStringPrinter']]/OBJBLOCK"
1254                     + "/METHOD_DEF[./IDENT[@text='getName']]/SLIST"
1255                     + "/VARIABLE_DEF[./IDENT[@text='var']]/TYPE",
1256                 "/COMPILATION_UNIT/CLASS_DEF"
1257                     + "[./IDENT[@text='InputMainSuppressionsStringPrinter']]/OBJBLOCK"
1258                     + "/METHOD_DEF[./IDENT[@text='getName']]/SLIST"
1259                     + "/VARIABLE_DEF[./IDENT[@text='var']]/TYPE/LITERAL_INT");
1260 
1261         assertMainReturnCode(0, getPath("InputMainSuppressionsStringPrinter.java"),
1262                 "-s", "7:9", "--tabWidth", "2");
1263         assertWithMessage("Unexpected output log")
1264             .that(systemOut.getCapturedData())
1265             .isEqualTo(expected);
1266         assertWithMessage("Unexpected system error log")
1267             .that(systemErr.getCapturedData())
1268             .isEqualTo("");
1269     }
1270 
1271     @Test
1272     public void testPrintSuppressionConflictingOptionsTvsC(@SysErr Capturable systemErr,
1273             @SysOut Capturable systemOut) {
1274         assertMainReturnCode(-1, "-c", "/google_checks.xml", getPath(""), "-s", "2:4");
1275         assertWithMessage("Unexpected output log")
1276             .that(systemOut.getCapturedData())
1277             .isEqualTo("Option '-s' cannot be used with other options."
1278                 + System.lineSeparator());
1279         assertWithMessage("Unexpected system error log")
1280             .that(systemErr.getCapturedData())
1281             .isEqualTo("");
1282     }
1283 
1284     @Test
1285     public void testPrintSuppressionConflictingOptionsTvsP(@SysErr Capturable systemErr,
1286             @SysOut Capturable systemOut) {
1287         assertMainReturnCode(-1, "-p", getPath("InputMainMycheckstyle.properties"), "-s", "2:4",
1288                 getPath(""));
1289         assertWithMessage("Unexpected output log")
1290             .that(systemOut.getCapturedData())
1291             .isEqualTo("Option '-s' cannot be used with other options."
1292                 + System.lineSeparator());
1293         assertWithMessage("Unexpected system error log")
1294             .that(systemErr.getCapturedData())
1295             .isEqualTo("");
1296     }
1297 
1298     @Test
1299     public void testPrintSuppressionConflictingOptionsTvsF(@SysErr Capturable systemErr,
1300             @SysOut Capturable systemOut) {
1301         assertMainReturnCode(-1, "-f", "plain", "-s", "2:4", getPath(""));
1302         assertWithMessage("Unexpected output log")
1303             .that(systemOut.getCapturedData())
1304             .isEqualTo("Option '-s' cannot be used with other options."
1305                 + System.lineSeparator());
1306         assertWithMessage("Unexpected system error log")
1307             .that(systemErr.getCapturedData())
1308             .isEqualTo("");
1309     }
1310 
1311     @Test
1312     public void testPrintSuppressionConflictingOptionsTvsO(@SysErr Capturable systemErr,
1313             @SysOut Capturable systemOut) throws IOException {
1314         final String outputPath = new File(temporaryFolder, "file.output").getCanonicalPath();
1315 
1316         assertMainReturnCode(-1, "-o", outputPath, "-s", "2:4", getPath(""));
1317         assertWithMessage("Unexpected output log")
1318             .that(systemOut.getCapturedData())
1319             .isEqualTo("Option '-s' cannot be used with other options."
1320                 + System.lineSeparator());
1321         assertWithMessage("Unexpected system error log")
1322             .that(systemErr.getCapturedData())
1323             .isEqualTo("");
1324     }
1325 
1326     @Test
1327     public void testPrintSuppressionOnMoreThanOneFile(@SysErr Capturable systemErr,
1328             @SysOut Capturable systemOut) {
1329         assertMainReturnCode(-1, "-s", "2:4", getPath(""), getPath(""));
1330         assertWithMessage("Unexpected output log")
1331             .that(systemOut.getCapturedData())
1332             .isEqualTo("Printing xpath suppressions is allowed for only one file."
1333                 + System.lineSeparator());
1334         assertWithMessage("Unexpected system error log")
1335             .that(systemErr.getCapturedData())
1336             .isEqualTo("");
1337     }
1338 
1339     @Test
1340     public void testGenerateXpathSuppressionOptionOne(@SysErr Capturable systemErr,
1341             @SysOut Capturable systemOut) {
1342         final String expected = addEndOfLine(
1343             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1344                 "<!DOCTYPE suppressions PUBLIC",
1345                 "    \"-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2"
1346                     + "//EN\"",
1347                 "    \"https://checkstyle.org/dtds/suppressions_1_2_xpath_experimental.dtd\">",
1348                 "<suppressions>",
1349                 "<suppress-xpath",
1350                 "       files=\"InputMainComplexityOverflow.java\"",
1351                 "       checks=\"MissingJavadocMethodCheck\"",
1352                 "       query=\"/COMPILATION_UNIT/CLASS_DEF"
1353                     + "[./IDENT[@text='InputMainComplexityOverflow']]/OBJBLOCK"
1354                     + "/METHOD_DEF[./IDENT[@text='provokeNpathIntegerOverflow']]\"/>",
1355                 "<suppress-xpath",
1356                 "       files=\"InputMainComplexityOverflow.java\"",
1357                 "       id=\"LeftCurlyEol\"",
1358                 "       query=\"/COMPILATION_UNIT/CLASS_DEF"
1359                     + "[./IDENT[@text='InputMainComplexityOverflow']]/OBJBLOCK"
1360                     + "/METHOD_DEF[./IDENT[@text='provokeNpathIntegerOverflow']]/SLIST\"/>",
1361                 "</suppressions>");
1362 
1363         assertMainReturnCode(0, "-c", "/google_checks.xml", "--generate-xpath-suppression",
1364                 getPath("InputMainComplexityOverflow.java"));
1365         assertWithMessage("Unexpected output log")
1366             .that(systemOut.getCapturedData())
1367             .isEqualTo(expected);
1368         assertWithMessage("Unexpected system error log")
1369             .that(systemErr.getCapturedData())
1370             .isEqualTo("");
1371     }
1372 
1373     @Test
1374     public void testGenerateXpathSuppressionOptionTwo(@SysErr Capturable systemErr,
1375             @SysOut Capturable systemOut) {
1376         final String expected = addEndOfLine(
1377             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1378             "<!DOCTYPE suppressions PUBLIC",
1379             "    \"-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2"
1380                 + "//EN\"",
1381             "    \"https://checkstyle.org/dtds/suppressions_1_2_xpath_experimental.dtd\">",
1382             "<suppressions>",
1383             "<suppress-xpath",
1384             "       files=\"InputMainGenerateXpathSuppressions.java\"",
1385             "       checks=\"ExplicitInitializationCheck\"",
1386             "       query=\"/COMPILATION_UNIT/CLASS_DEF"
1387                 + "[./IDENT[@text='InputMainGenerateXpathSuppressions']]"
1388                 + "/OBJBLOCK/VARIABLE_DEF/IDENT[@text='low']\"/>",
1389             "<suppress-xpath",
1390             "       files=\"InputMainGenerateXpathSuppressions.java\"",
1391             "       checks=\"IllegalThrowsCheck\"",
1392             "       query=\"/COMPILATION_UNIT/CLASS_DEF"
1393                 + "[./IDENT[@text='InputMainGenerateXpathSuppressions']]"
1394                 + "/OBJBLOCK/METHOD_DEF[./IDENT[@text='test']]/LITERAL_THROWS"
1395                 + "/IDENT[@text='RuntimeException']\"/>",
1396             "<suppress-xpath",
1397             "       files=\"InputMainGenerateXpathSuppressions.java\"",
1398             "       checks=\"NestedForDepthCheck\"",
1399             "       query=\"/COMPILATION_UNIT/CLASS_DEF"
1400                 + "[./IDENT[@text='InputMainGenerateXpathSuppressions']]"
1401                 + "/OBJBLOCK/METHOD_DEF[./IDENT[@text='test']]/SLIST/LITERAL_FOR/SLIST"
1402                 + "/LITERAL_FOR/SLIST/LITERAL_FOR\"/>",
1403             "</suppressions>");
1404 
1405         assertMainReturnCode(0, "-c", getPath("InputMainConfig-xpath-suppressions.xml"),
1406                 "--generate-xpath-suppression",
1407                 getPath("InputMainGenerateXpathSuppressions.java"));
1408         assertWithMessage("Unexpected output log")
1409             .that(systemOut.getCapturedData())
1410             .isEqualTo(expected);
1411         assertWithMessage("Unexpected system error log")
1412             .that(systemErr.getCapturedData())
1413             .isEqualTo("");
1414     }
1415 
1416     @Test
1417     public void testGenerateXpathSuppressionOptionEmptyConfig(@SysErr Capturable systemErr,
1418             @SysOut Capturable systemOut) {
1419         final String expected = "";
1420 
1421         assertMainReturnCode(0, "-c", getPath("InputMainConfig-empty.xml"),
1422                 "--generate-xpath-suppression", getPath("InputMainComplexityOverflow.java"));
1423         assertWithMessage("Unexpected output log")
1424             .that(systemOut.getCapturedData())
1425             .isEqualTo(expected);
1426         assertWithMessage("Unexpected system error log")
1427             .that(systemErr.getCapturedData())
1428             .isEqualTo("");
1429     }
1430 
1431     @Test
1432     public void testGenerateXpathSuppressionOptionCustomOutput(@SysErr Capturable systemErr)
1433             throws IOException {
1434         final String expected = addEndOfLine(
1435             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1436                 "<!DOCTYPE suppressions PUBLIC",
1437                 "    \"-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2"
1438                     + "//EN\"",
1439                 "    \"https://checkstyle.org/dtds/suppressions_1_2_xpath_experimental.dtd\">",
1440                 "<suppressions>",
1441                 "<suppress-xpath",
1442                 "       files=\"InputMainGenerateXpathSuppressionsTabWidth.java\"",
1443                 "       checks=\"ExplicitInitializationCheck\"",
1444                 "       query=\"/COMPILATION_UNIT/CLASS_DEF[./IDENT["
1445                     + "@text='InputMainGenerateXpathSuppressionsTabWidth']]"
1446                     + "/OBJBLOCK/VARIABLE_DEF/IDENT[@text='low']\"/>",
1447                 "</suppressions>");
1448         final File file = new File(temporaryFolder, "file.output");
1449         assertMainReturnCode(0, "-c", getPath("InputMainConfig-xpath-suppressions.xml"), "-o",
1450                 file.getPath(), "--generate-xpath-suppression",
1451                 getPath("InputMainGenerateXpathSuppressionsTabWidth.java"));
1452         try (BufferedReader br = Files.newBufferedReader(file.toPath())) {
1453             final String fileContent = br.lines().collect(Collectors.joining(EOL, "", EOL));
1454             assertWithMessage("Unexpected output log")
1455                 .that(fileContent)
1456                 .isEqualTo(expected);
1457             assertWithMessage("Unexpected system error log")
1458                 .that(systemErr.getCapturedData())
1459                 .isEqualTo("");
1460         }
1461     }
1462 
1463     @Test
1464     public void testGenerateXpathSuppressionOptionDefaultTabWidth(@SysErr Capturable systemErr,
1465             @SysOut Capturable systemOut) {
1466         final String expected = addEndOfLine(
1467             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1468                 "<!DOCTYPE suppressions PUBLIC",
1469                 "    \"-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2"
1470                     + "//EN\"",
1471                 "    \"https://checkstyle.org/dtds/suppressions_1_2_xpath_experimental.dtd\">",
1472                 "<suppressions>",
1473                 "<suppress-xpath",
1474                 "       files=\"InputMainGenerateXpathSuppressionsTabWidth.java\"",
1475                 "       checks=\"ExplicitInitializationCheck\"",
1476                 "       query=\"/COMPILATION_UNIT/CLASS_DEF[./IDENT["
1477                     + "@text='InputMainGenerateXpathSuppressionsTabWidth']]"
1478                     + "/OBJBLOCK/VARIABLE_DEF/IDENT[@text='low']\"/>",
1479                 "</suppressions>");
1480 
1481         assertMainReturnCode(0, "-c", getPath("InputMainConfig-xpath-suppressions.xml"),
1482                 "--generate-xpath-suppression",
1483                 getPath("InputMainGenerateXpathSuppressionsTabWidth.java"));
1484         assertWithMessage("Unexpected output log")
1485             .that(systemOut.getCapturedData())
1486             .isEqualTo(expected);
1487         assertWithMessage("Unexpected system error log")
1488             .that(systemErr.getCapturedData())
1489             .isEqualTo("");
1490     }
1491 
1492     @Test
1493     public void testGenerateXpathSuppressionOptionCustomTabWidth(@SysErr Capturable systemErr,
1494             @SysOut Capturable systemOut) {
1495         final String expected = "";
1496 
1497         assertMainReturnCode(0, "-c", getPath("InputMainConfig-xpath-suppressions.xml"),
1498                 "--generate-xpath-suppression", "--tabWidth", "20",
1499                 getPath("InputMainGenerateXpathSuppressionsTabWidth.java"));
1500         assertWithMessage("Unexpected output log")
1501             .that(systemOut.getCapturedData())
1502             .isEqualTo(expected);
1503         assertWithMessage("Unexpected system error log")
1504             .that(systemErr.getCapturedData())
1505             .isEqualTo("");
1506     }
1507 
1508     /**
1509      * Verifies the output of the command line parameter "-J".
1510      *
1511      * @param systemErr wrapper for {@code System.err}
1512      * @param systemOut wrapper for {@code System.out}
1513      * @throws IOException if I/O exception occurs while reading the test input.
1514      * @noinspection RedundantThrows
1515      * @noinspectionreason RedundantThrows - false positive
1516      */
1517     @Test
1518     public void testPrintFullTreeOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut)
1519             throws IOException {
1520         final String expected = Files.readString(Path.of(
1521             getPath("InputMainExpectedInputAstTreeStringPrinterJavadoc.txt")))
1522                 .replaceAll("\\\\r\\\\n", "\\\\n")
1523                 .replaceAll("\r\n", "\n");
1524 
1525         assertMainReturnCode(0, "-J", getPath("InputMainAstTreeStringPrinterJavadoc.java"));
1526         assertWithMessage("Unexpected output log")
1527             .that(systemOut.getCapturedData().replaceAll("\\\\r\\\\n", "\\\\n")
1528                         .replaceAll("\r\n", "\n"))
1529             .isEqualTo(expected);
1530         assertWithMessage("Unexpected system error log")
1531             .that(systemErr.getCapturedData())
1532             .isEqualTo("");
1533     }
1534 
1535     @Test
1536     public void testConflictingOptionsTvsC(@SysErr Capturable systemErr,
1537             @SysOut Capturable systemOut) {
1538         assertMainReturnCode(-1, "-c", "/google_checks.xml", "-t", getPath(""));
1539         assertWithMessage("Unexpected output log")
1540             .that(systemOut.getCapturedData())
1541             .isEqualTo("Option '-t' cannot be used with other options." + System.lineSeparator());
1542         assertWithMessage("Unexpected system error log")
1543             .that(systemErr.getCapturedData())
1544             .isEqualTo("");
1545     }
1546 
1547     @Test
1548     public void testConflictingOptionsTvsP(@SysErr Capturable systemErr,
1549             @SysOut Capturable systemOut) {
1550         assertMainReturnCode(-1, "-p", getPath("InputMainMycheckstyle.properties"), "-t",
1551                 getPath(""));
1552         assertWithMessage("Unexpected output log")
1553             .that(systemOut.getCapturedData())
1554             .isEqualTo("Option '-t' cannot be used with other options." + System.lineSeparator());
1555         assertWithMessage("Unexpected system error log")
1556             .that(systemErr.getCapturedData())
1557             .isEqualTo("");
1558     }
1559 
1560     @Test
1561     public void testConflictingOptionsTvsF(@SysErr Capturable systemErr,
1562             @SysOut Capturable systemOut) {
1563         assertMainReturnCode(-1, "-f", "plain", "-t", getPath(""));
1564         assertWithMessage("Unexpected output log")
1565             .that(systemOut.getCapturedData())
1566             .isEqualTo("Option '-t' cannot be used with other options." + System.lineSeparator());
1567         assertWithMessage("Unexpected system error log")
1568             .that(systemErr.getCapturedData())
1569             .isEqualTo("");
1570     }
1571 
1572     @Test
1573     public void testConflictingOptionsTvsS(@SysErr Capturable systemErr,
1574             @SysOut Capturable systemOut) throws IOException {
1575         final String outputPath = new File(temporaryFolder, "file.output").getCanonicalPath();
1576 
1577         assertMainReturnCode(-1, "-s", outputPath, "-t", getPath(""));
1578         assertWithMessage("Unexpected output log")
1579             .that(systemOut.getCapturedData())
1580             .isEqualTo("Option '-t' cannot be used with other options." + System.lineSeparator());
1581         assertWithMessage("Unexpected system error log")
1582             .that(systemErr.getCapturedData())
1583             .isEqualTo("");
1584     }
1585 
1586     @Test
1587     public void testConflictingOptionsTvsO(@SysErr Capturable systemErr,
1588             @SysOut Capturable systemOut) throws IOException {
1589         final String outputPath = new File(temporaryFolder, "file.output").getCanonicalPath();
1590 
1591         assertMainReturnCode(-1, "-o", outputPath, "-t", getPath(""));
1592         assertWithMessage("Unexpected output log")
1593             .that(systemOut.getCapturedData())
1594             .isEqualTo("Option '-t' cannot be used with other options." + System.lineSeparator());
1595         assertWithMessage("Unexpected system error log")
1596             .that(systemErr.getCapturedData())
1597             .isEqualTo("");
1598     }
1599 
1600     @Test
1601     public void testDebugOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1602         assertMainReturnCode(0, "-c", "/google_checks.xml", getPath("InputMain.java"), "-d");
1603         assertWithMessage("Unexpected system error log")
1604             .that(systemErr.getCapturedData())
1605             .contains("FINE: Checkstyle debug logging enabled");
1606         assertWithMessage("Unexpected system error log")
1607             .that(systemOut.getCapturedData())
1608             .contains("Audit done.");
1609 
1610     }
1611 
1612     @Test
1613     public void testExcludeOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut)
1614             throws IOException {
1615         final String filePath = getFilePath("");
1616         assertMainReturnCode(-1, "-c", "/google_checks.xml", filePath, "-e", filePath);
1617         assertWithMessage("Unexpected output log")
1618             .that(systemOut.getCapturedData())
1619             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
1620         assertWithMessage("Unexpected system error log")
1621             .that(systemErr.getCapturedData())
1622             .isEqualTo("");
1623     }
1624 
1625     @Test
1626     public void testExcludeOptionFile(@SysErr Capturable systemErr, @SysOut Capturable systemOut)
1627             throws IOException {
1628         final String filePath = getFilePath("InputMain.java");
1629         assertMainReturnCode(-1, "-c", "/google_checks.xml", filePath, "-e", filePath);
1630         assertWithMessage("Unexpected output log")
1631             .that(systemOut.getCapturedData())
1632             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
1633         assertWithMessage("Unexpected system error log")
1634             .that(systemErr.getCapturedData())
1635             .isEqualTo("");
1636     }
1637 
1638     @Test
1639     public void testExcludeRegexpOption(@SysErr Capturable systemErr, @SysOut Capturable systemOut)
1640             throws IOException {
1641         final String filePath = getFilePath("");
1642         assertMainReturnCode(-1, "-c", "/google_checks.xml", filePath, "-x", ".");
1643         assertWithMessage("Unexpected output log")
1644             .that(systemOut.getCapturedData())
1645             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
1646         assertWithMessage("Unexpected output log")
1647             .that(systemErr.getCapturedData())
1648             .isEqualTo("");
1649     }
1650 
1651     @Test
1652     public void testExcludeRegexpOptionFile(@SysErr Capturable systemErr,
1653             @SysOut Capturable systemOut) throws IOException {
1654         final String filePath = getFilePath("InputMain.java");
1655         assertMainReturnCode(-1, "-c", "/google_checks.xml", filePath, "-x", ".");
1656         assertWithMessage("Unexpected output log")
1657             .that(systemOut.getCapturedData())
1658             .isEqualTo("Files to process must be specified, found 0." + System.lineSeparator());
1659         assertWithMessage("Unexpected output log")
1660             .that(systemErr.getCapturedData())
1661             .isEqualTo("");
1662     }
1663 
1664     @Test
1665     @SuppressWarnings("unchecked")
1666     public void testExcludeDirectoryNotMatch() throws Exception {
1667         final Class<?> optionsClass = Class.forName(Main.class.getName());
1668         final Method method = optionsClass.getDeclaredMethod("listFiles", File.class, List.class);
1669         method.setAccessible(true);
1670         final List<Pattern> list = new ArrayList<>();
1671         list.add(Pattern.compile("BAD_PATH"));
1672 
1673         final List<File> result = (List<File>) method.invoke(null, new File(getFilePath("")),
1674                 list);
1675         assertWithMessage("Invalid result size")
1676             .that(result)
1677             .isNotEmpty();
1678     }
1679 
1680     @Test
1681     public void testCustomRootModule(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1682         TestRootModuleChecker.reset();
1683 
1684         assertMainReturnCode(0, "-c", getPath("InputMainConfig-custom-root-module.xml"),
1685                 getPath("InputMain.java"));
1686         assertWithMessage("Unexpected output log")
1687             .that(systemOut.getCapturedData())
1688             .isEqualTo("");
1689         assertWithMessage("Unexpected system error log")
1690             .that(systemErr.getCapturedData())
1691             .isEqualTo("");
1692         assertWithMessage("Invalid Checker state")
1693                 .that(TestRootModuleChecker.isProcessed())
1694                 .isTrue();
1695         assertWithMessage("RootModule should be destroyed")
1696                 .that(TestRootModuleChecker.isDestroyed())
1697                 .isTrue();
1698     }
1699 
1700     @Test
1701     public void testCustomSimpleRootModule(@SysErr Capturable systemErr) {
1702         TestRootModuleChecker.reset();
1703         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-custom-simple-root-module.xml"),
1704                 getPath("InputMain.java"));
1705         final String checkstylePackage = "com.puppycrawl.tools.checkstyle.";
1706         final LocalizedMessage unableToInstantiateExceptionMessage = new LocalizedMessage(
1707                 Definitions.CHECKSTYLE_BUNDLE,
1708                 getClass(),
1709                 "PackageObjectFactory.unableToInstantiateExceptionMessage",
1710                 "TestRootModuleChecker",
1711                 checkstylePackage
1712                         + "TestRootModuleChecker, "
1713                         + "TestRootModuleCheckerCheck, " + checkstylePackage
1714                         + "TestRootModuleCheckerCheck");
1715         assertWithMessage(
1716                 "Unexpected system error log")
1717                         .that(systemErr.getCapturedData())
1718                         .startsWith(checkstylePackage + "api.CheckstyleException: "
1719                                 + unableToInstantiateExceptionMessage.getMessage());
1720         assertWithMessage("Invalid checker state")
1721                 .that(TestRootModuleChecker.isProcessed())
1722                 .isFalse();
1723     }
1724 
1725     @Test
1726     public void testExceptionOnExecuteIgnoredModuleWithUnknownModuleName(
1727             @SysErr Capturable systemErr) {
1728         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-non-existent-classname-ignore.xml"),
1729                     "--executeIgnoredModules", getPath("InputMain.java"));
1730         final String cause = "com.puppycrawl.tools.checkstyle.api.CheckstyleException:"
1731                 + " cannot initialize module TreeWalker - ";
1732         assertWithMessage("Unexpected system error log")
1733                 .that(systemErr.getCapturedData())
1734                 .startsWith(cause);
1735     }
1736 
1737     @Test
1738     public void testExceptionOnExecuteIgnoredModuleWithBadPropertyValue(
1739             @SysErr Capturable systemErr) {
1740         assertMainReturnCode(-2, "-c", getPath("InputMainConfig-TypeName-bad-value.xml"),
1741                     "--executeIgnoredModules", getPath("InputMain.java"));
1742         final String cause = "com.puppycrawl.tools.checkstyle.api.CheckstyleException:"
1743                 + " cannot initialize module TreeWalker - ";
1744         final String causeDetail = "it is not a boolean";
1745         assertWithMessage("Unexpected system error log")
1746                 .that(systemErr.getCapturedData())
1747                 .startsWith(cause);
1748         assertWithMessage("Unexpected system error log")
1749                 .that(systemErr.getCapturedData())
1750                 .contains(causeDetail);
1751     }
1752 
1753     @Test
1754     public void testNoProblemOnExecuteIgnoredModuleWithBadPropertyValue(
1755             @SysErr Capturable systemErr) {
1756         assertMainReturnCode(0, "-c", getPath("InputMainConfig-TypeName-bad-value.xml"),
1757                     "", getPath("InputMain.java"));
1758         assertWithMessage("Unexpected system error log")
1759             .that(systemErr.getCapturedData())
1760                 .isEmpty();
1761     }
1762 
1763     @Test
1764     public void testMissingFiles(@SysErr Capturable systemErr, @SysOut Capturable systemOut) {
1765         assertMainReturnCode(-1);
1766         final String usage = "Missing required parameter: '<files or folders>'" + EOL + SHORT_USAGE;
1767         assertWithMessage("Unexpected output log")
1768             .that(systemOut.getCapturedData())
1769             .isEqualTo("");
1770         assertWithMessage("Unexpected system error log")
1771             .that(systemErr.getCapturedData())
1772             .isEqualTo(usage);
1773     }
1774 
1775     @Test
1776     public void testOutputFormatToStringLowercase() {
1777         assertWithMessage("expected xml")
1778             .that(Main.OutputFormat.XML.toString())
1779             .isEqualTo("xml");
1780         assertWithMessage("expected plain")
1781             .that(Main.OutputFormat.PLAIN.toString())
1782             .isEqualTo("plain");
1783     }
1784 
1785     @Test
1786     public void testXmlOutputFormatCreateListener() throws IOException {
1787         final ByteArrayOutputStream out = new ByteArrayOutputStream();
1788         final AuditListener listener = Main.OutputFormat.XML.createListener(out,
1789                 OutputStreamOptions.CLOSE);
1790         assertWithMessage("listener is XMLLogger")
1791                 .that(listener)
1792                 .isInstanceOf(XMLLogger.class);
1793     }
1794 
1795     @Test
1796     public void testSarifOutputFormatCreateListener() throws IOException {
1797         final ByteArrayOutputStream out = new ByteArrayOutputStream();
1798         final AuditListener listener = Main.OutputFormat.SARIF.createListener(out,
1799                 OutputStreamOptions.CLOSE);
1800         assertWithMessage("listener is SarifLogger")
1801                 .that(listener)
1802                 .isInstanceOf(SarifLogger.class);
1803     }
1804 
1805     @Test
1806     public void testPlainOutputFormatCreateListener() throws IOException {
1807         final ByteArrayOutputStream out = new ByteArrayOutputStream();
1808         final AuditListener listener = Main.OutputFormat.PLAIN.createListener(out,
1809                 OutputStreamOptions.CLOSE);
1810         assertWithMessage("listener is DefaultLogger")
1811                 .that(listener)
1812                 .isInstanceOf(DefaultLogger.class);
1813     }
1814 
1815     /**
1816      * Helper method to run {@link Main#main(String...)} and verify the exit code.
1817      * Uses {@link Mockito#mockStatic(Class)} to mock method {@link Runtime#exit(int)}
1818      * to avoid VM termination.
1819      *
1820      * @param expectedExitCode the expected exit code to verify
1821      * @param arguments the command line arguments
1822      * @noinspection CallToSystemExit, ResultOfMethodCallIgnored
1823      * @noinspectionreason CallToSystemExit - test helper method requires workaround to
1824      *      verify exit code
1825      * @noinspectionreason ResultOfMethodCallIgnored - Setup for mockito to only
1826      *                     mock getRuntime to avoid VM termination.
1827      */
1828     private static void assertMainReturnCode(int expectedExitCode, String... arguments) {
1829         final Runtime mock = mock();
1830         try (MockedStatic<Runtime> runtime = mockStatic(Runtime.class)) {
1831             runtime.when(Runtime::getRuntime)
1832                     .thenReturn(mock);
1833             Main.main(arguments);
1834         }
1835         catch (IOException exception) {
1836             assertWithMessage("Unexpected exception: %s", exception).fail();
1837         }
1838         verify(mock).exit(expectedExitCode);
1839     }
1840 
1841     /**
1842      * Print stream that shouldn't be closed. The purpose of this class is to ensure that
1843      * {@code System.out} and {@code System.err} are not closed by Checkstyle.
1844      */
1845     private static final class ShouldNotBeClosedStream extends PrintStream {
1846 
1847         private boolean isClosed;
1848 
1849         private ShouldNotBeClosedStream() {
1850             super(new ByteArrayOutputStream(), false, StandardCharsets.UTF_8);
1851         }
1852 
1853         @Override
1854         public void close() {
1855             isClosed = true;
1856             super.close();
1857         }
1858 
1859     }
1860 
1861 }