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