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