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.Path;
32  import java.text.MessageFormat;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collections;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.ResourceBundle;
41  import java.util.stream.Collectors;
42  
43  import com.google.common.collect.ImmutableMap;
44  import com.google.common.collect.Maps;
45  import com.puppycrawl.tools.checkstyle.LocalizedMessage.Utf8Control;
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 {@link Class} type object.
87       * @return default module configuration for the given {@link 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 {@link Configuration} instance.
97       * @return {@link Checker} instance based on the given {@link 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 {@link Configuration} instance.
143      * @return {@link DefaultConfiguration} for the {@link TreeWalker}
144      *     based on the given {@link 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 {@link Configuration} instance.
161      * @return {@link DefaultConfiguration} for the given {@link 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.getViolations());
243         violationsWithoutFilters.addAll(testInputConfiguration.getFilteredViolations());
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.getViolations());
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.getXmlConfiguration();
268         verifyViolations(xmlConfig, filePath, testInputConfiguration.getViolations());
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.getViolations(), actualViolations);
311         assertWithMessage("Violations for %s differ.", filePath)
312             .that(actualViolations)
313             .containsExactlyElementsIn(expected);
314     }
315 
316     /**
317      * Performs verification of two files with their given file paths using specified
318      * configuration of one file only. Also performs verification of the config specified
319      * in the input file. This method needs to be implemented when two given files need to be
320      * checked through a single check only.
321      *
322      * @param filePath1 file path of first file to verify
323      * @param filePath2 file path of second file to verify
324      * @param expected an array of expected messages
325      * @throws Exception if exception occurs during verification process
326      */
327     protected final void verifyWithInlineConfigParser(String filePath1,
328                                                       String filePath2,
329                                                       String... expected)
330             throws Exception {
331         final TestInputConfiguration testInputConfiguration1 =
332                 InlineConfigParser.parse(filePath1);
333         final DefaultConfiguration parsedConfig =
334                 testInputConfiguration1.createConfiguration();
335         final TestInputConfiguration testInputConfiguration2 =
336                 InlineConfigParser.parse(filePath2);
337         verifyViolations(parsedConfig, filePath1, testInputConfiguration1.getViolations());
338         verifyViolations(parsedConfig, filePath2, testInputConfiguration2.getViolations());
339         verify(createChecker(parsedConfig),
340                 new File[] {new File(filePath1), new File(filePath2)},
341                 filePath1,
342                 expected);
343     }
344 
345     /**
346      * Performs verification of two files with their given file paths.
347      * using specified configuration of one file only. Also performs
348      * verification of the config specified in the input file. This method
349      * needs to be implemented when two given files need to be
350      * checked through a single check only.
351      *
352      * @param filePath1 file path of first file to verify
353      * @param filePath2 file path of first file to verify
354      * @param expectedFromFile1 list of expected message
355      * @param expectedFromFile2 list of expected message
356      * @throws Exception if exception occurs during verification process
357      */
358     protected final void verifyWithInlineConfigParser(String filePath1,
359                                                       String filePath2,
360                                                       List<String> expectedFromFile1,
361                                                       List<String> expectedFromFile2)
362             throws Exception {
363         final TestInputConfiguration testInputConfiguration = InlineConfigParser.parse(filePath1);
364         final DefaultConfiguration parsedConfig = testInputConfiguration.createConfiguration();
365         final TestInputConfiguration testInputConfiguration2 = InlineConfigParser.parse(filePath2);
366         final DefaultConfiguration parsedConfig2 = testInputConfiguration.createConfiguration();
367         final File[] inputs = {new File(filePath1), new File(filePath2)};
368         verifyViolations(parsedConfig, filePath1, testInputConfiguration.getViolations());
369         verifyViolations(parsedConfig2, filePath2, testInputConfiguration2.getViolations());
370         verify(createChecker(parsedConfig), inputs, ImmutableMap.of(
371             filePath1, expectedFromFile1,
372             filePath2, expectedFromFile2));
373     }
374 
375     /**
376      * Verifies the target file against the configuration specified in a separate configuration
377      * file.
378      * This method is intended for use cases when the configuration is stored in one file and the
379      * content to verify is stored in another file.
380      *
381      * @param fileWithConfig file path of the configuration file
382      * @param targetFile file path of the target file to be verified
383      * @param expected an array of expected messages
384      * @throws Exception if an exception occurs during verification process
385      */
386     protected final void verifyWithInlineConfigParserSeparateConfigAndTarget(String fileWithConfig,
387                                                                              String targetFile,
388                                                                              String... expected)
389             throws Exception {
390         final TestInputConfiguration testInputConfiguration1 =
391                 InlineConfigParser.parse(fileWithConfig);
392         final DefaultConfiguration parsedConfig =
393                 testInputConfiguration1.createConfiguration();
394         final List<TestInputViolation> inputViolations =
395                 InlineConfigParser.getViolationsFromInputFile(targetFile);
396         final List<String> actualViolations = getActualViolationsForFile(parsedConfig, targetFile);
397         verifyViolations(targetFile, inputViolations, actualViolations);
398         assertWithMessage("Violations for %s differ.", targetFile)
399                 .that(actualViolations)
400                 .containsExactlyElementsIn(expected);
401     }
402 
403     /**
404      * Performs verification of the file with the given file path using specified configuration
405      * and the array of expected messages. Also performs verification of the config with filters
406      * specified in the input file.
407      *
408      * @param fileWithConfig file path of the configuration file.
409      * @param targetFilePath file path of the target file to be verified.
410      * @param expectedUnfiltered an array of expected unfiltered config.
411      * @param expectedFiltered an array of expected config with filters.
412      * @throws Exception if exception occurs during verification process.
413      */
414     protected final void verifyFilterWithInlineConfigParserSeparateConfigAndTarget(
415             String fileWithConfig,
416             String targetFilePath,
417             String[] expectedUnfiltered,
418             String... expectedFiltered)
419             throws Exception {
420         final TestInputConfiguration testInputConfiguration =
421                 InlineConfigParser.parseWithFilteredViolations(fileWithConfig);
422         final DefaultConfiguration configWithoutFilters =
423                 testInputConfiguration.createConfigurationWithoutFilters();
424         final List<TestInputViolation> violationsWithoutFilters = new ArrayList<>(
425                 InlineConfigParser.getFilteredViolationsFromInputFile(targetFilePath));
426         violationsWithoutFilters.addAll(
427                 InlineConfigParser.getViolationsFromInputFile(targetFilePath));
428         Collections.sort(violationsWithoutFilters);
429         verifyViolations(configWithoutFilters, targetFilePath, violationsWithoutFilters);
430         verify(configWithoutFilters, targetFilePath, expectedUnfiltered);
431         final DefaultConfiguration configWithFilters =
432                 testInputConfiguration.createConfiguration();
433         final List<TestInputViolation> violationsWithFilters =
434                 InlineConfigParser.getViolationsFromInputFile(targetFilePath);
435         verifyViolations(configWithFilters, targetFilePath, violationsWithFilters);
436         verify(configWithFilters, targetFilePath, expectedFiltered);
437     }
438 
439     /**
440      * Performs verification of the file with the given file path using specified configuration
441      * and the array expected messages. Also performs verification of the config specified in
442      * input file
443      *
444      * @param filePath file path to verify.
445      * @param expected an array of expected messages.
446      * @throws Exception if exception occurs during verification process.
447      */
448     protected void verifyWithInlineConfigParserTwice(String filePath, String... expected)
449             throws Exception {
450         final TestInputConfiguration testInputConfiguration =
451                 InlineConfigParser.parse(filePath);
452         final DefaultConfiguration parsedConfig =
453                 testInputConfiguration.createConfiguration();
454         verifyViolations(parsedConfig, filePath, testInputConfiguration.getViolations());
455         verify(parsedConfig, filePath, expected);
456     }
457 
458     /**
459      * Verifies logger output using the inline configuration parser.
460      * Expects an input file with configuration and violations, and a report file with expected
461      * output.
462      *
463      * @param inputFile path to the file with configuration and violations
464      * @param expectedReportFile path to the expected logger report file
465      * @param logger logger to test
466      * @param outputStream output stream where the logger writes its actual output
467      * @throws Exception if an exception occurs during verification
468      */
469     protected void verifyWithInlineConfigParserAndLogger(String inputFile,
470                                                          String expectedReportFile,
471                                                          AuditListener logger,
472                                                          ByteArrayOutputStream outputStream)
473             throws Exception {
474         final TestInputConfiguration testInputConfiguration =
475                 InlineConfigParser.parse(inputFile);
476         final DefaultConfiguration parsedConfig =
477                 testInputConfiguration.createConfiguration();
478         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
479         final String basePath = Path.of("").toAbsolutePath().toString();
480 
481         final Checker checker = createChecker(parsedConfig);
482         checker.setBasedir(basePath);
483         checker.addListener(logger);
484         checker.process(filesToCheck);
485 
486         verifyContent(expectedReportFile, outputStream);
487     }
488 
489     /**
490      * Verifies logger output using the inline configuration parser for default logger.
491      * Expects an input file with configuration and violations, and expected output file.
492      * Uses full Checker configuration.
493      *
494      * @param inputFile path to the file with configuration and violations
495      * @param expectedOutputFile path to the expected info stream output file
496      * @param logger logger to test
497      * @param outputStream where the logger writes its actual info stream output
498      * @throws Exception if an exception occurs during verification
499      */
500     protected final void verifyWithInlineConfigParserAndDefaultLogger(String inputFile,
501                                                               String expectedOutputFile,
502                                                               AuditListener logger,
503                                                               ByteArrayOutputStream outputStream)
504             throws Exception {
505         final TestInputConfiguration testInputConfiguration =
506                 InlineConfigParser.parseWithXmlHeader(inputFile);
507         final Configuration parsedConfig =
508                 testInputConfiguration.getXmlConfiguration();
509         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
510         final String basePath = Path.of("").toAbsolutePath().toString();
511 
512         final Checker checker = createChecker(parsedConfig);
513         checker.setBasedir(basePath);
514         checker.addListener(logger);
515         checker.process(filesToCheck);
516 
517         verifyCleanedMessageContent(expectedOutputFile, outputStream, basePath);
518     }
519 
520     /**
521      * Verifies logger output using the inline configuration parser for default logger.
522      * Expects an input file with configuration and violations, and separate expected output files
523      * for info and error streams.
524      * Uses full Checker configuration.
525      *
526      * @param inputFile path to the file with configuration and violations
527      * @param expectedInfoFile path to the expected info stream output file
528      * @param expectedErrorFile path to the expected error stream output file
529      * @param logger logger to test
530      * @param infoStream where the logger writes its actual info stream output
531      * @param errorStream where the logger writes its actual error stream output
532      * @throws Exception if an exception occurs during verification
533      * @noinspection MethodWithTooManyParameters
534      * @noinspectionreason MethodWithTooManyParameters - Method requires a lot of parameters to
535      *                     verify the default logger output.
536      */
537     protected final void verifyWithInlineConfigParserAndDefaultLogger(String inputFile,
538                                                          String expectedInfoFile,
539                                                          String expectedErrorFile,
540                                                          AuditListener logger,
541                                                          ByteArrayOutputStream infoStream,
542                                                          ByteArrayOutputStream errorStream)
543             throws Exception {
544         final TestInputConfiguration testInputConfiguration =
545                 InlineConfigParser.parseWithXmlHeader(inputFile);
546         final Configuration parsedConfig =
547                 testInputConfiguration.getXmlConfiguration();
548         final List<File> filesToCheck = Collections.singletonList(new File(inputFile));
549         final String basePath = Path.of("").toAbsolutePath().toString();
550 
551         final Checker checker = createChecker(parsedConfig);
552         checker.setBasedir(basePath);
553         checker.addListener(logger);
554         checker.process(filesToCheck);
555 
556         verifyContent(expectedInfoFile, infoStream);
557         verifyCleanedMessageContent(expectedErrorFile, errorStream, basePath);
558     }
559 
560     /**
561      * Performs verification of the file with the given file name. Uses specified configuration.
562      * Expected messages are represented by the array of strings.
563      * This implementation uses overloaded
564      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
565      *
566      * @param config configuration.
567      * @param fileName file name to verify.
568      * @param expected an array of expected messages.
569      * @throws Exception if exception occurs during verification process.
570      */
571     protected final void verify(Configuration config, String fileName, String... expected)
572             throws Exception {
573         verify(createChecker(config), fileName, fileName, expected);
574     }
575 
576     /**
577      * Performs verification of the file with the given file name.
578      * Uses provided {@link Checker} instance.
579      * Expected messages are represented by the array of strings.
580      * This implementation uses overloaded
581      * {@link AbstractModuleTestSupport#verify(Checker, String, String, String...)} method inside.
582      *
583      * @param checker {@link Checker} instance.
584      * @param fileName file name to verify.
585      * @param expected an array of expected messages.
586      * @throws Exception if exception occurs during verification process.
587      */
588     protected void verify(Checker checker, String fileName, String... expected)
589             throws Exception {
590         verify(checker, fileName, fileName, expected);
591     }
592 
593     /**
594      * Performs verification of the given files.
595      *
596      * @param checker {@link Checker} instance
597      * @param processedFiles files to process.
598      * @param expectedViolations a map of expected violations per files.
599      * @throws Exception if exception occurs during verification process.
600      */
601     protected final void verify(Checker checker,
602                           File[] processedFiles,
603                           Map<String, List<String>> expectedViolations)
604             throws Exception {
605         stream.flush();
606         stream.reset();
607         final List<File> theFiles = new ArrayList<>();
608         Collections.addAll(theFiles, processedFiles);
609         checker.process(theFiles);
610 
611         // process each of the lines
612         final Map<String, List<String>> actualViolations = getActualViolations();
613         final Map<String, List<String>> realExpectedViolations =
614                 Maps.filterValues(expectedViolations, input -> !input.isEmpty());
615 
616         assertWithMessage("Files with expected violations and actual violations differ.")
617             .that(actualViolations.keySet())
618             .isEqualTo(realExpectedViolations.keySet());
619 
620         realExpectedViolations.forEach((fileName, violationList) -> {
621             assertWithMessage("Violations for %s differ.", fileName)
622                 .that(actualViolations.get(fileName))
623                 .containsExactlyElementsIn(violationList);
624         });
625 
626         checker.destroy();
627     }
628 
629     /**
630      * Performs verification of the file with the given file name.
631      * Uses provided {@link Checker} instance.
632      * Expected messages are represented by the array of strings.
633      * This implementation uses overloaded
634      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
635      *
636      * @param checker {@link Checker} instance.
637      * @param processedFilename file name to verify.
638      * @param messageFileName message file name.
639      * @param expected an array of expected messages.
640      * @throws Exception if exception occurs during verification process.
641      */
642     protected final void verify(Checker checker,
643                           String processedFilename,
644                           String messageFileName,
645                           String... expected)
646             throws Exception {
647         verify(checker,
648                 new File[] {new File(processedFilename)},
649                 messageFileName, expected);
650     }
651 
652     /**
653      *  Performs verification of the given files against the array of
654      *  expected messages using the provided {@link Checker} instance.
655      *
656      *  @param checker {@link Checker} instance.
657      *  @param processedFiles list of files to verify.
658      *  @param messageFileName message file name.
659      *  @param expected an array of expected messages.
660      *  @throws Exception if exception occurs during verification process.
661      */
662     protected void verify(Checker checker,
663                           File[] processedFiles,
664                           String messageFileName,
665                           String... expected)
666             throws Exception {
667         final Map<String, List<String>> expectedViolations = new HashMap<>();
668         expectedViolations.put(messageFileName, Arrays.asList(expected));
669         verify(checker, processedFiles, expectedViolations);
670     }
671 
672     /**
673      * Runs 'verifyWithInlineConfigParser' with limited stack size and time duration.
674      *
675      * @param fileName file name to verify.
676      * @param expected an array of expected messages.
677      * @throws Exception if exception occurs during verification process.
678      */
679     protected final void verifyWithLimitedResources(String fileName, String... expected)
680             throws Exception {
681         TestUtil.getResultWithLimitedResources(() -> {
682             verifyWithInlineConfigParser(fileName, expected);
683             return null;
684         });
685     }
686 
687     /**
688      * Runs 'verifyWithInlineConfigParser' with limited stack size suitable for XPath-based
689      * checks, allowing Saxon's XPath engine to initialize while still detecting stack
690      * overflows caused by deep AST traversal.
691      *
692      * @param fileName file name to verify.
693      * @param expected an array of expected messages.
694      * @throws Exception if exception occurs during verification process.
695      */
696     protected final void verifyWithLimitedXpathResources(String fileName, String... expected)
697             throws Exception {
698         TestUtil.runWithLimitedXpathResources(() -> {
699             verifyWithInlineConfigParser(fileName, expected);
700             return null;
701         });
702     }
703 
704     /**
705      * Executes given config on a list of files only. Does not verify violations.
706      *
707      * @param config check configuration
708      * @param filenames names of files to process
709      * @throws Exception if there is a problem during checker configuration
710      */
711     protected final void execute(Configuration config, String... filenames) throws Exception {
712         final Checker checker = createChecker(config);
713         final List<File> files = Arrays.stream(filenames)
714                 .map(File::new)
715                 .toList();
716         checker.process(files);
717         checker.destroy();
718     }
719 
720     /**
721      * Executes given config on a list of files only. Does not verify violations.
722      *
723      * @param checker check configuration
724      * @param filenames names of files to process
725      * @throws Exception if there is a problem during checker configuration
726      */
727     protected static void execute(Checker checker, String... filenames) throws Exception {
728         final List<File> files = Arrays.stream(filenames)
729                 .map(File::new)
730                 .toList();
731         checker.process(files);
732         checker.destroy();
733     }
734 
735     /**
736      * Performs verification of violation lines.
737      *
738      * @param config parsed config.
739      * @param file file path.
740      * @param testInputViolations List of TestInputViolation objects.
741      * @throws Exception if exception occurs during verification process.
742      */
743     private void verifyViolations(Configuration config,
744                                   String file,
745                                   List<TestInputViolation> testInputViolations)
746             throws Exception {
747         final List<String> actualViolations = getActualViolationsForFile(config, file);
748         final List<Integer> actualViolationLines = actualViolations.stream()
749                 .map(violation -> violation.substring(0, violation.indexOf(':')))
750                 .map(Integer::valueOf)
751                 .toList();
752         final List<Integer> expectedViolationLines = testInputViolations.stream()
753                 .map(TestInputViolation::getLineNo)
754                 .toList();
755         assertWithMessage("Violation lines for %s differ.", file)
756                 .that(actualViolationLines)
757                 .isEqualTo(expectedViolationLines);
758         for (int index = 0; index < actualViolations.size(); index++) {
759             assertWithMessage("Actual and expected violations differ.")
760                     .that(actualViolations.get(index))
761                     .matches(testInputViolations.get(index).toRegex());
762         }
763     }
764 
765     /**
766      * Performs verification of violation lines.
767      *
768      * @param file file path.
769      * @param testInputViolations List of TestInputViolation objects.
770      * @param actualViolations for a file
771      */
772     private static void verifyViolations(String file,
773                                   List<TestInputViolation> testInputViolations,
774                                   List<String> actualViolations) {
775         final List<Integer> actualViolationLines = actualViolations.stream()
776                 .map(violation -> violation.substring(0, violation.indexOf(':')))
777                 .map(Integer::valueOf)
778                 .toList();
779         final List<Integer> expectedViolationLines = testInputViolations.stream()
780                 .map(TestInputViolation::getLineNo)
781                 .toList();
782         assertWithMessage("Violation lines for %s differ.", file)
783                 .that(actualViolationLines)
784                 .isEqualTo(expectedViolationLines);
785         for (int index = 0; index < actualViolations.size(); index++) {
786             assertWithMessage("Actual and expected violations differ.")
787                     .that(actualViolations.get(index))
788                     .matches(testInputViolations.get(index).toRegex());
789         }
790     }
791 
792     /**
793      * Verifies that the logger's actual output matches the expected report file.
794      *
795      * @param expectedOutputFile path to the expected logger report file
796      * @param outputStream output stream containing the actual logger output
797      * @throws IOException if an exception occurs while reading the file
798      */
799     private static void verifyContent(
800             String expectedOutputFile,
801             ByteArrayOutputStream outputStream) throws IOException {
802         final String expectedContent = readFile(expectedOutputFile);
803         final String actualContent =
804                 toLfLineEnding(outputStream.toString(StandardCharsets.UTF_8));
805         assertWithMessage("Content should match")
806                 .that(actualContent)
807                 .isEqualTo(expectedContent);
808     }
809 
810     /**
811      * Verifies that the logger output matches the expected report file content,
812      * keeping only severity-tagged lines (e.g. [ERROR], [WARN], [INFO]) or lines containing
813      * "Starting audit..." or "Audit done".
814      *
815      * <p>
816      * This method strips:
817      * <ul>
818      *   <li>any stack trace lines from exception outputs (i.e. lines not starting with a severity
819      *   tag),</li>
820      *   <li>any absolute {@code basePath} prefixes in the message content.</li>
821      * </ul>
822      * The result is compared with expected output that includes only severity-tagged lines.
823      *
824      * @param expectedOutputFile path to a file that contains the expected first line
825      * @param outputStream output stream containing the actual logger output
826      * @param basePath absolute path prefix to strip before comparison
827      * @throws IOException if an exception occurs while reading the file
828      */
829     private static void verifyCleanedMessageContent(
830             String expectedOutputFile,
831             ByteArrayOutputStream outputStream,
832             String basePath) throws IOException {
833         final String expectedContent = readFile(expectedOutputFile);
834         final String rawActualContent =
835                 toLfLineEnding(outputStream.toString(StandardCharsets.UTF_8));
836 
837         final String cleanedActualContent = rawActualContent.lines()
838                 .filter(line -> {
839                     return line.startsWith("[")
840                             || line.contains("Starting audit...")
841                             || line.contains("Audit done.");
842                 })
843                 .map(line -> line.replace(basePath, ""))
844                 .map(line -> line.replace('\\', '/'))
845                 .collect(Collectors.joining("\n", "", "\n"));
846 
847         assertWithMessage("Content should match")
848                 .that(cleanedActualContent)
849                 .isEqualTo(expectedContent);
850     }
851 
852     /**
853      * Tests the file with the check config.
854      *
855      * @param config check configuration.
856      * @param file input file path.
857      * @return list of actual violations.
858      * @throws Exception if exception occurs during verification process.
859      */
860     private List<String> getActualViolationsForFile(Configuration config,
861                                                     String file) throws Exception {
862         stream.flush();
863         stream.reset();
864         final List<File> files = Collections.singletonList(new File(file));
865         final Checker checker = createChecker(config);
866         checker.process(files);
867         final Map<String, List<String>> actualViolations =
868                 getActualViolations();
869         checker.destroy();
870         return actualViolations.getOrDefault(file, new ArrayList<>());
871     }
872 
873     /**
874      * Returns the actual violations for each file that has been checked against {@link Checker}.
875      * Each file is mapped to their corresponding violation messages. Reads input stream for these
876      * messages using instance of {@link InputStreamReader}.
877      *
878      * @return a {@link Map} object containing file names and the corresponding violation messages.
879      * @throws IOException exception can occur when reading input stream.
880      */
881     private Map<String, List<String>> getActualViolations() throws IOException {
882         // process each of the lines
883         try (ByteArrayInputStream inputStream =
884                 new ByteArrayInputStream(stream.toByteArray());
885             LineNumberReader lnr = new LineNumberReader(
886                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
887             final Map<String, List<String>> actualViolations = new HashMap<>();
888             for (String line = lnr.readLine(); line != null;
889                  line = lnr.readLine()) {
890                 if ("Audit done.".equals(line) || line.contains("at com")) {
891                     break;
892                 }
893                 // have at least 2 characters before the splitting colon,
894                 // to not split after the drive letter on Windows
895                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
896                 final String actualViolationFileName = actualViolation[0];
897                 final String actualViolationMessage = actualViolation[1];
898 
899                 actualViolations
900                         .computeIfAbsent(actualViolationFileName, key -> new ArrayList<>())
901                         .add(actualViolationMessage);
902             }
903 
904             return actualViolations;
905         }
906     }
907 
908     /**
909      * Gets the check message 'as is' from appropriate 'messages.properties'
910      * file.
911      *
912      * @param messageKey the key of message in 'messages.properties' file.
913      * @param arguments  the arguments of message in 'messages.properties' file.
914      * @return The message of the check with the arguments applied.
915      */
916     protected final String getCheckMessage(String messageKey, Object... arguments) {
917         return internalGetCheckMessage(getMessageBundle(), messageKey, arguments);
918     }
919 
920     /**
921      * Gets the check message 'as is' from appropriate 'messages.properties'
922      * file.
923      *
924      * @param clazz the related check class.
925      * @param messageKey the key of message in 'messages.properties' file.
926      * @param arguments the arguments of message in 'messages.properties' file.
927      * @return The message of the check with the arguments applied.
928      */
929     protected static String getCheckMessage(
930             Class<?> clazz, String messageKey, Object... arguments) {
931         return internalGetCheckMessage(getMessageBundle(clazz.getName()), messageKey, arguments);
932     }
933 
934     /**
935      * Gets the check message 'as is' from appropriate 'messages.properties'
936      * file.
937      *
938      * @param messageBundle the bundle name.
939      * @param messageKey the key of message in 'messages.properties' file.
940      * @param arguments the arguments of message in 'messages.properties' file.
941      * @return The message of the check with the arguments applied.
942      */
943     private static String internalGetCheckMessage(
944             String messageBundle, String messageKey, Object... arguments) {
945         final ResourceBundle resourceBundle = ResourceBundle.getBundle(
946                 messageBundle,
947                 Locale.ROOT,
948                 Thread.currentThread().getContextClassLoader(),
949                 new Utf8Control());
950         final String pattern = resourceBundle.getString(messageKey);
951         final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
952         return formatter.format(arguments);
953     }
954 
955     /**
956      * Returns message bundle for a class specified by its class name.
957      *
958      * @return a string of message bundles for the class using class name.
959      */
960     private String getMessageBundle() {
961         final String className = getClass().getName();
962         return getMessageBundle(className);
963     }
964 
965     /**
966      * Returns message bundles for a class by providing class name.
967      *
968      * @param className name of the class.
969      * @return message bundles containing package name.
970      */
971     private static String getMessageBundle(String className) {
972         final String messageBundle;
973         final String messages = "messages";
974         final int endIndex = className.lastIndexOf('.');
975         final Map<String, String> messageBundleMappings = new HashMap<>();
976         messageBundleMappings.put("SeverityMatchFilterExamplesTest",
977                 "com.puppycrawl.tools.checkstyle.checks.naming.messages");
978 
979         if (endIndex < 0) {
980             messageBundle = messages;
981         }
982         else {
983             final String packageName = className.substring(0, endIndex);
984             if ("com.puppycrawl.tools.checkstyle.filters".equals(packageName)) {
985                 messageBundle = messageBundleMappings.get(className.substring(endIndex + 1));
986             }
987             else {
988                 messageBundle = packageName + "." + messages;
989             }
990         }
991         return messageBundle;
992     }
993 
994     /**
995      * Remove suppressed violation messages from actual violation messages.
996      *
997      * @param actualViolations actual violation messages
998      * @param suppressedViolations suppressed violation messages
999      * @return an array of actual violation messages minus suppressed violation messages
1000      */
1001     protected static String[] removeSuppressed(String[] actualViolations,
1002                                                String... suppressedViolations) {
1003         final List<String> actualViolationsList =
1004             Arrays.stream(actualViolations).collect(Collectors.toCollection(ArrayList::new));
1005         actualViolationsList.removeAll(Arrays.asList(suppressedViolations));
1006         return actualViolationsList.toArray(CommonUtil.EMPTY_STRING_ARRAY);
1007     }
1008 
1009 }