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