View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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  
24  import java.io.ByteArrayInputStream;
25  import java.io.ByteArrayOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.io.LineNumberReader;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.text.MessageFormat;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Locale;
40  import java.util.Map;
41  import java.util.ResourceBundle;
42  import java.util.stream.Collectors;
43  
44  import com.google.common.collect.ImmutableMap;
45  import com.google.common.collect.Maps;
46  import com.puppycrawl.tools.checkstyle.api.AuditListener;
47  import com.puppycrawl.tools.checkstyle.api.Configuration;
48  import com.puppycrawl.tools.checkstyle.api.DetailAST;
49  import com.puppycrawl.tools.checkstyle.bdd.InlineConfigParser;
50  import com.puppycrawl.tools.checkstyle.bdd.TestInputConfiguration;
51  import com.puppycrawl.tools.checkstyle.bdd.TestInputViolation;
52  import com.puppycrawl.tools.checkstyle.internal.utils.BriefUtLogger;
53  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
54  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
55  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
56  import com.puppycrawl.tools.checkstyle.xpath.RootNode;
57  
58  public abstract class AbstractModuleTestSupport extends AbstractPathTestSupport {
59  
60      protected static final String ROOT_MODULE_NAME = Checker.class.getSimpleName();
61  
62      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
63  
64      /**
65       * Returns log stream.
66       *
67       * @return stream with log
68       */
69      protected final ByteArrayOutputStream getStream() {
70          return stream;
71      }
72  
73      /**
74       * Returns test logger.
75       *
76       * @return logger for tests
77       */
78      protected final DefaultLogger getBriefUtLogger() {
79          return new BriefUtLogger(stream);
80      }
81  
82      /**
83       * Creates a default module configuration {@link DefaultConfiguration} for a given object
84       * of type {@link Class}.
85       *
86       * @param clazz a {@code Class} type object.
87       * @return default module configuration for the given {@code Class} instance.
88       */
89      protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
90          return new DefaultConfiguration(clazz.getName());
91      }
92  
93      /**
94       * Creates {@link Checker} instance based on the given {@link Configuration} instance.
95       *
96       * @param moduleConfig {@code Configuration} instance.
97       * @return {@code Checker} instance based on the given {@code Configuration} instance.
98       * @throws Exception if an exception occurs during checker configuration.
99       */
100     protected final Checker createChecker(Configuration moduleConfig)
101             throws Exception {
102         final String moduleName = moduleConfig.getName();
103         final Checker checker = new Checker();
104         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
105 
106         if (ROOT_MODULE_NAME.equals(moduleName)) {
107             checker.configure(moduleConfig);
108         }
109         else {
110             configureChecker(checker, moduleConfig);
111         }
112 
113         checker.addListener(getBriefUtLogger());
114         return checker;
115     }
116 
117     /**
118      * Configures the {@code checker} instance with {@code moduleConfig}.
119      *
120      * @param checker {@link Checker} instance.
121      * @param moduleConfig {@link Configuration} instance.
122      * @throws Exception if an exception occurs during configuration.
123      */
124     protected void configureChecker(Checker checker, Configuration moduleConfig) throws Exception {
125         final Class<?> moduleClass = Class.forName(moduleConfig.getName());
126 
127         final Configuration config;
128         if (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(moduleClass)
129                 || ModuleReflectionUtil.isTreeWalkerFilterModule(moduleClass)) {
130             config = createTreeWalkerConfig(moduleConfig);
131         }
132         else {
133             config = createRootConfig(moduleConfig);
134         }
135         checker.configure(config);
136     }
137 
138     /**
139      * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
140      * based on the given {@link Configuration} instance.
141      *
142      * @param config {@code Configuration} instance.
143      * @return {@code DefaultConfiguration} for the {@code TreeWalker}
144      *     based on the given {@code Configuration} instance.
145      */
146     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
147         final DefaultConfiguration rootConfig =
148                 new DefaultConfiguration(ROOT_MODULE_NAME);
149         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
150         // make sure that the tests always run with this charset
151         rootConfig.addProperty("charset", StandardCharsets.UTF_8.name());
152         rootConfig.addChild(twConf);
153         twConf.addChild(config);
154         return rootConfig;
155     }
156 
157     /**
158      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
159      *
160      * @param config {@code Configuration} instance.
161      * @return {@code DefaultConfiguration} for the given {@code Configuration} instance.
162      */
163     protected static DefaultConfiguration createRootConfig(Configuration config) {
164         final DefaultConfiguration rootConfig = new DefaultConfiguration(ROOT_MODULE_NAME);
165         if (config != null) {
166             rootConfig.addChild(config);
167         }
168         return rootConfig;
169     }
170 
171     /**
172      * Returns canonical path for the file with the given file name.
173      * The path is formed base on the non-compilable resources location.
174      *
175      * @param filename file name.
176      * @return canonical path for the file with the given file name.
177      * @throws IOException if I/O exception occurs while forming the path.
178      */
179     protected final String getNonCompilablePath(String filename) throws IOException {
180         return new File("src/" + getResourceLocation()
181                 + "/resources-noncompilable/" + getPackageLocation() + "/"
182                 + filename).getCanonicalPath();
183     }
184 
185     /**
186      * Returns canonical path for the Javadoc file that intentionally contains errors.
187      *
188      * @param filename file name.
189      * @return canonical path for the file with Javadoc errors.
190      * @throws IOException if I/O exception occurs while forming the path.
191      */
192     protected final String getJavadocWithErrorPath(String filename) throws IOException {
193         return new File("src/" + getResourceLocation()
194                 + "/resources-with-javadoc-error/" + getPackageLocation() + "/"
195                 + filename).getCanonicalPath();
196     }
197 
198     /**
199      * Creates a RootNode for non-compilable test files.
200      *
201      * @param fileName name of the test file
202      * @return RootNode for the parsed AST
203      * @throws Exception if file parsing fails
204      */
205     protected RootNode getRootNodeForNonCompilable(String fileName) throws Exception {
206         final File file = new File(getNonCompilablePath(fileName));
207         final DetailAST rootAst = JavaParser.parseFile(file, JavaParser.Options.WITHOUT_COMMENTS);
208         return new RootNode(rootAst);
209     }
210 
211     /**
212      * Returns URI-representation of the path for the given file name.
213      * The path is formed base on the root location.
214      *
215      * @param filename file name.
216      * @return URI-representation of the path for the file with the given file name.
217      */
218     protected final String getUriString(String filename) {
219         return new File("src/test/resources/" + getPackageLocation() + "/" + filename).toURI()
220                 .toString();
221     }
222 
223     /**
224      * Performs verification of the file with the given file path using specified configuration
225      * and the array of expected messages. Also performs verification of the config with filters
226      * specified in the input file.
227      *
228      * @param filePath file path to verify.
229      * @param expectedUnfiltered an array of expected unfiltered config.
230      * @param expectedFiltered an array of expected config with filters.
231      * @throws Exception if exception occurs during verification process.
232      */
233     protected final void verifyFilterWithInlineConfigParser(String filePath,
234                                                             String[] expectedUnfiltered,
235                                                             String... expectedFiltered)
236             throws Exception {
237         final TestInputConfiguration testInputConfiguration =
238                 InlineConfigParser.parseWithFilteredViolations(filePath);
239         final DefaultConfiguration configWithoutFilters =
240                 testInputConfiguration.createConfigurationWithoutFilters();
241         final List<TestInputViolation> violationsWithoutFilters =
242                 new ArrayList<>(testInputConfiguration.violations());
243         violationsWithoutFilters.addAll(testInputConfiguration.filteredViolations());
244         Collections.sort(violationsWithoutFilters);
245         verifyViolations(configWithoutFilters, filePath, violationsWithoutFilters);
246         verify(configWithoutFilters, filePath, expectedUnfiltered);
247         final DefaultConfiguration configWithFilters =
248                 testInputConfiguration.createConfiguration();
249         verifyViolations(configWithFilters, filePath, testInputConfiguration.violations());
250         verify(configWithFilters, filePath, expectedFiltered);
251     }
252 
253     /**
254      * Performs verification of the file with given file path using configurations parsed from
255      * xml header of the file and the array expected messages. Also performs verification of
256      * the config specified in input file.
257      *
258      * @param filePath file path to verify
259      * @param expected an array of expected messages
260      * @throws Exception if exception occurs
261      */
262     protected final void verifyWithInlineXmlConfig(String filePath, String... expected)
263             throws Exception {
264         final TestInputConfiguration testInputConfiguration =
265                 InlineConfigParser.parseWithXmlHeader(filePath);
266         final Configuration xmlConfig =
267                 testInputConfiguration.xmlConfiguration();
268         verifyViolations(xmlConfig, filePath, testInputConfiguration.violations());
269         verify(xmlConfig, filePath, expected);
270     }
271 
272     /**
273      * Performs verification of the file with the given file path using configuration,
274      * loaded from an external XML resource and the array of expected messages.
275      *
276      * @param configPath path to the XML configuration resource.
277      * @param filePath file path to verify.
278      * @param expected an array of expected messages.
279      * @throws Exception if exception occurs during verification process.
280      */
281     protected void verifyWithExternalXmlConfig(
282             String configPath,
283             String filePath,
284             String... expected)
285                     throws Exception {
286         final Configuration config =
287                 ConfigurationLoader.loadConfiguration(
288                         configPath,
289                         new PropertiesExpander(System.getProperties()),
290                         ConfigurationLoader.IgnoredModulesOptions.EXECUTE);
291         verify(config, filePath, expected);
292     }
293 
294     /**
295      * Performs verification of the file with the given file path using specified configuration
296      * and the array expected messages. Also performs verification of the config specified in
297      * input file.
298      *
299      * @param filePath file path to verify.
300      * @param expected an array of expected messages.
301      * @throws Exception if exception occurs during verification process.
302      */
303     protected final void verifyWithInlineConfigParser(String filePath, String... expected)
304             throws Exception {
305         final TestInputConfiguration testInputConfiguration =
306                 InlineConfigParser.parse(filePath);
307         final DefaultConfiguration parsedConfig =
308                 testInputConfiguration.createConfiguration();
309         final List<String> actualViolations = getActualViolationsForFile(parsedConfig, filePath);
310         verifyViolations(filePath, testInputConfiguration.violations(), actualViolations);
311         assertWithMessage("Violations for %s differ.", filePath)
312             .that(actualViolations)
313             .containsExactlyElementsIn(expected);
314     }
315 
316     /**
317      * Performs verification of multiple files with their given file paths using the configuration
318      * specified in the first file. Also performs verification of the configuration specified in
319      * each input file.
320      *
321      * @param filePaths paths of files to verify
322      * @param expectedViolations expected messages for each file
323      * @throws Exception if exception occurs during verification process
324      */
325     protected final void verifyWithInlineConfigParser(List<String> filePaths,
326                                                       List<List<String>> expectedViolations)
327             throws Exception {
328         assertWithMessage("At least one input file must be supplied")
329                 .that(filePaths)
330                 .isNotEmpty();
331         assertWithMessage("Expected violations must be supplied for every input file")
332                 .that(expectedViolations)
333                 .hasSize(filePaths.size());
334         final TestInputConfiguration firstTestInputConfiguration =
335                 InlineConfigParser.parse(filePaths.getFirst());
336         final DefaultConfiguration parsedConfig =
337                 firstTestInputConfiguration.createConfiguration();
338         final File[] inputs = new File[filePaths.size()];
339         final ImmutableMap.Builder<String, List<String>> expectedViolationsByFile =
340                 ImmutableMap.builder();
341 
342         for (int index = 0; index < filePaths.size(); index++) {
343             final String filePath = filePaths.get(index);
344             final TestInputConfiguration testInputConfiguration =
345                     InlineConfigParser.parse(filePath);
346             final DefaultConfiguration inputConfig =
347                     testInputConfiguration.createConfiguration();
348             verifyViolations(inputConfig, filePath, testInputConfiguration.violations());
349             inputs[index] = new File(filePath);
350             expectedViolationsByFile.put(filePath, expectedViolations.get(index));
351         }
352 
353         verify(createChecker(parsedConfig), inputs, expectedViolationsByFile.buildOrThrow());
354     }
355 
356     /**
357      * Performs verification of two files with their given file paths using specified
358      * configuration of one file only. Also performs verification of the config specified
359      * in the input file. This method needs to be implemented when two given files need to be
360      * checked through a single check only.
361      *
362      * @param filePath1 file path of first file to verify
363      * @param filePath2 file path of second file to verify
364      * @param expected an array of expected messages
365      * @throws Exception if exception occurs during verification process
366      */
367     protected final void verifyWithInlineConfigParser(String filePath1,
368                                                       String filePath2,
369                                                       String... expected)
370             throws Exception {
371         final TestInputConfiguration testInputConfiguration1 =
372                 InlineConfigParser.parse(filePath1);
373         final DefaultConfiguration parsedConfig =
374                 testInputConfiguration1.createConfiguration();
375         final TestInputConfiguration testInputConfiguration2 =
376                 InlineConfigParser.parse(filePath2);
377         verifyViolations(parsedConfig, filePath1, testInputConfiguration1.violations());
378         verifyViolations(parsedConfig, filePath2, testInputConfiguration2.violations());
379         verify(createChecker(parsedConfig),
380                 new File[] {new File(filePath1), new File(filePath2)},
381                 filePath1,
382                 expected);
383     }
384 
385     /**
386      * Performs verification of two files with their given file paths.
387      * using specified configuration of one file only. Also performs
388      * verification of the config specified in the input file. This method
389      * needs to be implemented when two given files need to be checked through a single check only.
390      *
391      * @param filePath1 file path of first file to verify
392      * @param filePath2 file path of first file to verify
393      * @param expectedFromFile1 list of expected message
394      * @param expectedFromFile2 list of expected message
395      * @throws Exception if exception occurs during verification process
396      */
397     protected final void verifyWithInlineConfigParser(String filePath1,
398                                                       String filePath2,
399                                                       List<String> expectedFromFile1,
400                                                       List<String> expectedFromFile2)
401             throws Exception {
402         final TestInputConfiguration testInputConfiguration = InlineConfigParser.parse(filePath1);
403         final DefaultConfiguration parsedConfig = testInputConfiguration.createConfiguration();
404         final TestInputConfiguration testInputConfiguration2 = InlineConfigParser.parse(filePath2);
405         final DefaultConfiguration parsedConfig2 = testInputConfiguration.createConfiguration();
406         final File[] inputs = {new File(filePath1), new File(filePath2)};
407         verifyViolations(parsedConfig, filePath1, testInputConfiguration.violations());
408         verifyViolations(parsedConfig2, filePath2, testInputConfiguration2.violations());
409         verify(createChecker(parsedConfig), inputs, ImmutableMap.of(
410             filePath1, expectedFromFile1,
411             filePath2, expectedFromFile2));
412     }
413 
414     /**
415      * Verifies the target file against the configuration specified in a separate configuration
416      * file.
417      * This method is intended for use cases when the configuration is stored in one file and the
418      * content to verify is stored in another file.
419      *
420      * @param fileWithConfig file path of the configuration file
421      * @param targetFile file path of the target file to be verified
422      * @param expected an array of expected messages
423      * @throws Exception if an exception occurs during verification process
424      */
425     protected final void verifyWithInlineConfigParserSeparateConfigAndTarget(String fileWithConfig,
426                                                                              String targetFile,
427                                                                              String... expected)
428             throws Exception {
429         final TestInputConfiguration testInputConfiguration1 =
430                 InlineConfigParser.parse(fileWithConfig);
431         final DefaultConfiguration parsedConfig =
432                 testInputConfiguration1.createConfiguration();
433         final List<String> actualViolations = getActualViolationsForFile(parsedConfig, targetFile);
434         // Blank targets cannot contain violation comments without changing the test input.
435         if (!Files.readString(Path.of(targetFile)).isBlank()) {
436             final List<TestInputViolation> inputViolations =
437                     InlineConfigParser.getViolationsFromInputFile(targetFile);
438             verifyViolations(targetFile, inputViolations, actualViolations);
439         }
440         assertWithMessage("Violations for %s differ.", targetFile)
441                 .that(actualViolations)
442                 .containsExactlyElementsIn(expected);
443     }
444 
445     /**
446      * Performs verification of the file with the given file path using specified configuration
447      * and the array expected messages. Also performs verification of the config specified in
448      * input file
449      *
450      * @param filePath file path to verify.
451      * @param expected an array of expected messages.
452      * @throws Exception if exception occurs during verification process.
453      */
454     protected void verifyWithInlineConfigParserTwice(String filePath, String... expected)
455             throws Exception {
456         final TestInputConfiguration testInputConfiguration =
457                 InlineConfigParser.parse(filePath);
458         final DefaultConfiguration parsedConfig =
459                 testInputConfiguration.createConfiguration();
460         verifyViolations(parsedConfig, filePath, testInputConfiguration.violations());
461         verify(parsedConfig, filePath, expected);
462     }
463 
464     /**
465      * Verifies logger output using the inline configuration parser.
466      * Expects an input file with configuration and violations, and a report file with expected
467      * output.
468      *
469      * @param inputFile path to the file with configuration and violations
470      * @param expectedReportFile path to the expected logger report file
471      * @param logger logger to test
472      * @param outputStream output stream where the logger writes its actual output
473      * @throws Exception if an exception occurs during verification
474      */
475     protected void verifyWithInlineConfigParserAndLogger(String inputFile,
476                                                          String expectedReportFile,
477                                                          AuditListener logger,
478                                                          ByteArrayOutputStream outputStream)
479             throws Exception {
480         final TestInputConfiguration testInputConfiguration =
481                 InlineConfigParser.parse(inputFile);
482         final DefaultConfiguration parsedConfig =
483                 testInputConfiguration.createConfiguration();
484         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
485         final String basePath = Path.of("").toAbsolutePath().toString();
486 
487         final Checker checker = createChecker(parsedConfig);
488         checker.setBasedir(basePath);
489         checker.addListener(logger);
490         checker.process(filesToCheck);
491 
492         verifyContent(expectedReportFile, outputStream);
493     }
494 
495     /**
496      * Verifies logger output using the inline configuration parser for default logger.
497      * Expects an input file with configuration and violations, and expected output file.
498      * Uses full Checker configuration.
499      *
500      * @param inputFile path to the file with configuration and violations
501      * @param expectedOutputFile path to the expected info stream output file
502      * @param logger logger to test
503      * @param outputStream where the logger writes its actual info stream output
504      * @throws Exception if an exception occurs during verification
505      */
506     protected final void verifyWithInlineConfigParserAndDefaultLogger(String inputFile,
507                                                               String expectedOutputFile,
508                                                               AuditListener logger,
509                                                               ByteArrayOutputStream outputStream)
510             throws Exception {
511         final TestInputConfiguration testInputConfiguration =
512                 InlineConfigParser.parseWithXmlHeader(inputFile);
513         final Configuration parsedConfig =
514                 testInputConfiguration.xmlConfiguration();
515         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
516         final String basePath = Path.of("").toAbsolutePath().toString();
517 
518         final Checker checker = createChecker(parsedConfig);
519         checker.setBasedir(basePath);
520         checker.addListener(logger);
521         checker.process(filesToCheck);
522 
523         verifyCleanedMessageContent(expectedOutputFile, outputStream, basePath);
524     }
525 
526     /**
527      * Verifies logger output using the inline configuration parser for default logger.
528      * Expects an input file with configuration and violations, and separate expected output files
529      * for info and error streams.
530      * Uses full Checker configuration.
531      *
532      * @param inputFile path to the file with configuration and violations
533      * @param expectedInfoFile path to the expected info stream output file
534      * @param expectedErrorFile path to the expected error stream output file
535      * @param logger logger to test
536      * @param infoStream where the logger writes its actual info stream output
537      * @param errorStream where the logger writes its actual error stream output
538      * @throws Exception if an exception occurs during verification
539      * @noinspection MethodWithTooManyParameters
540      * @noinspectionreason MethodWithTooManyParameters - Method requires a lot of parameters to
541      *                     verify the default logger output.
542      */
543     protected final void verifyWithInlineConfigParserAndDefaultLogger(String inputFile,
544                                                          String expectedInfoFile,
545                                                          String expectedErrorFile,
546                                                          AuditListener logger,
547                                                          ByteArrayOutputStream infoStream,
548                                                          ByteArrayOutputStream errorStream)
549             throws Exception {
550         final TestInputConfiguration testInputConfiguration =
551                 InlineConfigParser.parseWithXmlHeader(inputFile);
552         final Configuration parsedConfig =
553                 testInputConfiguration.xmlConfiguration();
554         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
555         final String basePath = Path.of("").toAbsolutePath().toString();
556 
557         final Checker checker = createChecker(parsedConfig);
558         checker.setBasedir(basePath);
559         checker.addListener(logger);
560         checker.process(filesToCheck);
561 
562         verifyContent(expectedInfoFile, infoStream);
563         verifyCleanedMessageContent(expectedErrorFile, errorStream, basePath);
564     }
565 
566     /**
567      * Performs verification of the file with the given file name. Uses specified configuration.
568      * Expected messages are represented by the array of strings.
569      * This implementation uses overloaded
570      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
571      *
572      * @param config configuration.
573      * @param fileName file name to verify.
574      * @param expected an array of expected messages.
575      * @throws Exception if exception occurs during verification process.
576      */
577     protected final void verify(Configuration config, String fileName, String... expected)
578             throws Exception {
579         verify(createChecker(config), fileName, fileName, expected);
580     }
581 
582     /**
583      * Performs verification of the file with the given file name.
584      * Uses provided {@link Checker} instance.
585      * Expected messages are represented by the array of strings.
586      * This implementation uses overloaded
587      * {@link AbstractModuleTestSupport#verify(Checker, String, String, String...)} method inside.
588      *
589      * @param checker {@code Checker} instance.
590      * @param fileName file name to verify.
591      * @param expected an array of expected messages.
592      * @throws Exception if exception occurs during verification process.
593      */
594     protected void verify(Checker checker, String fileName, String... expected)
595             throws Exception {
596         verify(checker, fileName, fileName, expected);
597     }
598 
599     /**
600      * Performs verification of the given files.
601      *
602      * @param checker {@link Checker} instance
603      * @param processedFiles files to process.
604      * @param expectedViolations a map of expected violations per files.
605      * @throws Exception if exception occurs during verification process.
606      */
607     protected final void verify(Checker checker,
608                           File[] processedFiles,
609                           Map<String, List<String>> expectedViolations)
610             throws Exception {
611         stream.flush();
612         stream.reset();
613         final List<File> theFiles = new ArrayList<>();
614         Collections.addAll(theFiles, processedFiles);
615         checker.process(theFiles);
616 
617         // process each of the lines
618         final Map<String, List<String>> actualViolations = getActualViolations();
619         final Map<String, List<String>> realExpectedViolations =
620                 Maps.filterValues(expectedViolations, input -> !input.isEmpty());
621 
622         assertWithMessage("Files with expected violations and actual violations differ.")
623             .that(actualViolations.keySet())
624             .isEqualTo(realExpectedViolations.keySet());
625 
626         realExpectedViolations.forEach((fileName, violationList) -> {
627             assertWithMessage("Violations for %s differ.", fileName)
628                 .that(actualViolations.get(fileName))
629                 .containsExactlyElementsIn(violationList);
630         });
631 
632         checker.destroy();
633     }
634 
635     /**
636      * Performs verification of the file with the given file name.
637      * Uses provided {@link Checker} instance.
638      * Expected messages are represented by the array of strings.
639      * This implementation uses overloaded
640      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
641      *
642      * @param checker {@code Checker} instance.
643      * @param processedFilename file name to verify.
644      * @param messageFileName message file name.
645      * @param expected an array of expected messages.
646      * @throws Exception if exception occurs during verification process.
647      */
648     protected final void verify(Checker checker,
649                           String processedFilename,
650                           String messageFileName,
651                           String... expected)
652             throws Exception {
653         verify(checker,
654                 new File[] {new File(processedFilename)},
655                 messageFileName, expected);
656     }
657 
658     /**
659      *  Performs verification of the given files against the array of
660      *  expected messages using the provided {@link Checker} instance.
661      *
662      *  @param checker {@code Checker} instance.
663      *  @param processedFiles list of files to verify.
664      *  @param messageFileName message file name.
665      *  @param expected an array of expected messages.
666      *  @throws Exception if exception occurs during verification process.
667      */
668     protected void verify(Checker checker,
669                           File[] processedFiles,
670                           String messageFileName,
671                           String... expected)
672             throws Exception {
673         final Map<String, List<String>> expectedViolations = new HashMap<>();
674         expectedViolations.put(messageFileName, Arrays.asList(expected));
675         verify(checker, processedFiles, expectedViolations);
676     }
677 
678     /**
679      * Runs 'verifyWithInlineConfigParser' with limited stack size and time duration.
680      *
681      * @param fileName file name to verify.
682      * @param expected an array of expected messages.
683      * @throws Exception if exception occurs during verification process.
684      */
685     protected final void verifyWithLimitedResources(String fileName, String... expected)
686             throws Exception {
687         TestUtil.getResultWithLimitedResources(() -> {
688             verifyWithInlineConfigParser(fileName, expected);
689             return null;
690         });
691     }
692 
693     /**
694      * Runs 'verifyWithInlineConfigParser' with limited stack size suitable for XPath-based
695      * checks, allowing Saxon's XPath engine to initialize while still detecting stack
696      * overflows caused by deep AST traversal.
697      *
698      * @param fileName file name to verify.
699      * @param expected an array of expected messages.
700      * @throws Exception if exception occurs during verification process.
701      */
702     protected final void verifyWithLimitedXpathResources(String fileName, String... expected)
703             throws Exception {
704         TestUtil.runWithLimitedXpathResources(() -> {
705             verifyWithInlineConfigParser(fileName, expected);
706             return null;
707         });
708     }
709 
710     /**
711      * Executes given config on a list of files only. Does not verify violations.
712      *
713      * @param config check configuration
714      * @param filenames names of files to process
715      * @throws Exception if there is a problem during checker configuration
716      */
717     protected final void execute(Configuration config, String... filenames) throws Exception {
718         final Checker checker = createChecker(config);
719         final List<File> files = Arrays.stream(filenames)
720                 .map(File::new)
721                 .toList();
722         checker.process(files);
723         checker.destroy();
724     }
725 
726     /**
727      * Executes given config on a list of files only. Does not verify violations.
728      *
729      * @param checker check configuration
730      * @param filenames names of files to process
731      * @throws Exception if there is a problem during checker configuration
732      */
733     protected static void execute(Checker checker, String... filenames) throws Exception {
734         final List<File> files = Arrays.stream(filenames)
735                 .map(File::new)
736                 .toList();
737         checker.process(files);
738         checker.destroy();
739     }
740 
741     /**
742      * Performs verification of violation lines.
743      *
744      * @param config parsed config.
745      * @param file file path.
746      * @param testInputViolations List of TestInputViolation objects.
747      * @throws Exception if exception occurs during verification process.
748      */
749     private void verifyViolations(Configuration config,
750                                   String file,
751                                   List<TestInputViolation> testInputViolations)
752             throws Exception {
753         final List<String> actualViolations = getActualViolationsForFile(config, file);
754         final List<Integer> actualViolationLines = actualViolations.stream()
755                 .map(violation -> violation.substring(0, violation.indexOf(':')))
756                 .map(Integer::valueOf)
757                 .toList();
758         final List<Integer> expectedViolationLines = testInputViolations.stream()
759                 .map(TestInputViolation::getLineNo)
760                 .toList();
761         assertWithMessage("Violation lines for %s differ.", file)
762                 .that(actualViolationLines)
763                 .isEqualTo(expectedViolationLines);
764         for (int index = 0; index < actualViolations.size(); index++) {
765             assertWithMessage("Actual and expected violations differ.")
766                     .that(actualViolations.get(index))
767                     .matches(testInputViolations.get(index).toRegex());
768         }
769     }
770 
771     /**
772      * Performs verification of violation lines.
773      *
774      * @param file file path.
775      * @param testInputViolations List of TestInputViolation objects.
776      * @param actualViolations for a file
777      */
778     private static void verifyViolations(String file,
779                                   List<TestInputViolation> testInputViolations,
780                                   List<String> actualViolations) {
781         final List<Integer> actualViolationLines = actualViolations.stream()
782                 .map(violation -> violation.substring(0, violation.indexOf(':')))
783                 .map(Integer::valueOf)
784                 .toList();
785         final List<Integer> expectedViolationLines = testInputViolations.stream()
786                 .map(TestInputViolation::getLineNo)
787                 .toList();
788         assertWithMessage("Violation lines for %s differ.", file)
789                 .that(actualViolationLines)
790                 .isEqualTo(expectedViolationLines);
791         for (int index = 0; index < actualViolations.size(); index++) {
792             assertWithMessage("Actual and expected violations differ.")
793                     .that(actualViolations.get(index))
794                     .matches(testInputViolations.get(index).toRegex());
795         }
796     }
797 
798     /**
799      * Verifies that the logger's actual output matches the expected report file.
800      *
801      * @param expectedOutputFile path to the expected logger report file
802      * @param outputStream output stream containing the actual logger output
803      * @throws IOException if an exception occurs while reading the file
804      */
805     private static void verifyContent(
806             String expectedOutputFile,
807             ByteArrayOutputStream outputStream)
808                     throws IOException {
809         final String expectedContent = readFile(expectedOutputFile);
810         final String actualContent =
811                 toLfLineEnding(outputStream.toString(StandardCharsets.UTF_8));
812         assertWithMessage("Content should match")
813                 .that(actualContent)
814                 .isEqualTo(expectedContent);
815     }
816 
817     /**
818      * Verifies that the logger output matches the expected report file content,
819      * keeping only severity-tagged lines (e.g. [ERROR], [WARN], [INFO]) or lines containing
820      * "Starting audit..." or "Audit done".
821      *
822      * <p>
823      * This method strips:
824      * <ul>
825      *   <li>any stack trace lines from exception outputs (i.e. lines not starting with a severity
826      *   tag),</li>
827      *   <li>any absolute {@code basePath} prefixes in the message content.</li>
828      * </ul>
829      * The result is compared with expected output that includes only severity-tagged lines.
830      *
831      * @param expectedOutputFile path to a file that contains the expected first line
832      * @param outputStream output stream containing the actual logger output
833      * @param basePath absolute path prefix to strip before comparison
834      * @throws IOException if an exception occurs while reading the file
835      */
836     private static void verifyCleanedMessageContent(
837             String expectedOutputFile,
838             ByteArrayOutputStream outputStream,
839             String basePath)
840                     throws IOException {
841         final String expectedContent = readFile(expectedOutputFile);
842         final String rawActualContent =
843                 toLfLineEnding(outputStream.toString(StandardCharsets.UTF_8));
844 
845         final String cleanedActualContent = rawActualContent.lines()
846                 .filter(line -> {
847                     return line.startsWith("[")
848                             || line.contains("Starting audit...")
849                             || line.contains("Audit done.");
850                 })
851                 .map(line -> line.replace(basePath, ""))
852                 .map(line -> line.replace('\\', '/'))
853                 .collect(Collectors.joining("\n", "", "\n"));
854 
855         assertWithMessage("Content should match")
856                 .that(cleanedActualContent)
857                 .isEqualTo(expectedContent);
858     }
859 
860     /**
861      * Tests the file with the check config.
862      *
863      * @param config check configuration.
864      * @param file input file path.
865      * @return list of actual violations.
866      * @throws Exception if exception occurs during verification process.
867      */
868     private List<String> getActualViolationsForFile(Configuration config,
869                                                     String file)
870             throws Exception {
871         stream.flush();
872         stream.reset();
873         final List<File> files = Collections.singletonList(new File(file));
874         final Checker checker = createChecker(config);
875         checker.process(files);
876         final Map<String, List<String>> actualViolations =
877                 getActualViolations();
878         checker.destroy();
879         return actualViolations.getOrDefault(file, new ArrayList<>());
880     }
881 
882     /**
883      * Returns the actual violations for each file that has been checked against {@link Checker}.
884      * Each file is mapped to their corresponding violation messages. Reads input stream for these
885      * messages using instance of {@link InputStreamReader}.
886      *
887      * @return a {@link Map} object containing file names and the corresponding violation messages.
888      * @throws IOException exception can occur when reading input stream.
889      */
890     private Map<String, List<String>> getActualViolations() throws IOException {
891         // process each of the lines
892         try (ByteArrayInputStream inputStream =
893                 new ByteArrayInputStream(stream.toByteArray());
894             LineNumberReader lnr = new LineNumberReader(
895                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
896             final Map<String, List<String>> actualViolations = new HashMap<>();
897             for (String line = lnr.readLine(); line != null;
898                  line = lnr.readLine()) {
899                 if ("Audit done.".equals(line) || line.contains("at com")) {
900                     break;
901                 }
902                 // have at least 2 characters before the splitting colon,
903                 // to not split after the drive letter on Windows
904                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
905                 final String actualViolationFileName = actualViolation[0];
906                 final String actualViolationMessage = actualViolation[1];
907 
908                 actualViolations
909                         .computeIfAbsent(actualViolationFileName, key -> new ArrayList<>())
910                         .add(actualViolationMessage);
911             }
912 
913             return actualViolations;
914         }
915     }
916 
917     /**
918      * Gets the check message 'as is' from appropriate 'messages.properties' file.
919      *
920      * @param messageKey the key of message in 'messages.properties' file.
921      * @param arguments  the arguments of message in 'messages.properties' file.
922      * @return The message of the check with the arguments applied.
923      */
924     protected final String getCheckMessage(String messageKey, Object... arguments) {
925         return internalGetCheckMessage(getMessageBundle(), messageKey, arguments);
926     }
927 
928     /**
929      * Gets the check message 'as is' from appropriate 'messages.properties' file.
930      *
931      * @param clazz the related check class.
932      * @param messageKey the key of message in 'messages.properties' file.
933      * @param arguments the arguments of message in 'messages.properties' file.
934      * @return The message of the check with the arguments applied.
935      */
936     protected static String getCheckMessage(
937             Class<?> clazz, String messageKey, Object... arguments) {
938         return internalGetCheckMessage(getMessageBundle(clazz.getName()), messageKey, arguments);
939     }
940 
941     /**
942      * Gets the check message 'as is' from appropriate 'messages.properties' file.
943      *
944      * @param messageBundle the bundle name.
945      * @param messageKey the key of message in 'messages.properties' file.
946      * @param arguments the arguments of message in 'messages.properties' file.
947      * @return The message of the check with the arguments applied.
948      */
949     private static String internalGetCheckMessage(
950             String messageBundle, String messageKey, Object... arguments) {
951         final ResourceBundle resourceBundle = ResourceBundle.getBundle(
952                 messageBundle,
953                 Locale.ROOT,
954                 Thread.currentThread().getContextClassLoader());
955         final String pattern = resourceBundle.getString(messageKey);
956         final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
957         return formatter.format(arguments);
958     }
959 
960     /**
961      * Returns message bundle for a class specified by its class name.
962      *
963      * @return a string of message bundles for the class using class name.
964      */
965     private String getMessageBundle() {
966         final String className = getClass().getName();
967         return getMessageBundle(className);
968     }
969 
970     /**
971      * Returns message bundles for a class by providing class name.
972      *
973      * @param className name of the class.
974      * @return message bundles containing package name.
975      */
976     private static String getMessageBundle(String className) {
977         final String messageBundle;
978         final String messages = "messages";
979         final int endIndex = className.lastIndexOf('.');
980         final Map<String, String> messageBundleMappings = new HashMap<>();
981         messageBundleMappings.put("SeverityMatchFilterExamplesTest",
982                 "com.puppycrawl.tools.checkstyle.checks.naming.messages");
983 
984         if (endIndex < 0) {
985             messageBundle = messages;
986         }
987         else {
988             final String packageName = className.substring(0, endIndex);
989             if ("com.puppycrawl.tools.checkstyle.filters".equals(packageName)) {
990                 messageBundle = messageBundleMappings.get(className.substring(endIndex + 1));
991             }
992             else {
993                 messageBundle = packageName + "." + messages;
994             }
995         }
996         return messageBundle;
997     }
998 
999     /**
1000      * Remove suppressed violation messages from actual violation messages.
1001      *
1002      * @param actualViolations actual violation messages
1003      * @param suppressedViolations suppressed violation messages
1004      * @return an array of actual violation messages minus suppressed violation messages
1005      */
1006     protected static String[] removeSuppressed(String[] actualViolations,
1007                                                String... suppressedViolations) {
1008         final List<String> actualViolationsList =
1009             Arrays.stream(actualViolations).collect(Collectors.toCollection(ArrayList::new));
1010         actualViolationsList.removeAll(Arrays.asList(suppressedViolations));
1011         return actualViolationsList.toArray(CommonUtil.EMPTY_STRING_ARRAY);
1012     }
1013 
1014 }