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  import static com.puppycrawl.tools.checkstyle.internal.utils.TestUtil.getExpectedThrowable;
24  
25  import java.io.ByteArrayOutputStream;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.io.PrintWriter;
29  import java.io.Serial;
30  import java.nio.charset.StandardCharsets;
31  import java.util.Map;
32  
33  import org.junit.jupiter.api.Test;
34  
35  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
36  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
37  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
38  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
39  import com.puppycrawl.tools.checkstyle.api.Violation;
40  import com.puppycrawl.tools.checkstyle.internal.utils.CloseAndFlushTestByteArrayOutputStream;
41  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
42  import com.puppycrawl.tools.checkstyle.meta.ModuleDetails;
43  
44  public class SarifLoggerTest extends AbstractModuleTestSupport {
45  
46      /**
47       * Output stream to hold the test results. The IntelliJ IDEA issues the AutoCloseableResource
48       * warning here, so it needs to be suppressed. The {@code ByteArrayOutputStream} does not hold
49       * any resources that need to be released.
50       */
51      private final CloseAndFlushTestByteArrayOutputStream outStream =
52          new CloseAndFlushTestByteArrayOutputStream();
53  
54      @Override
55      public String getPackageLocation() {
56          return "com/puppycrawl/tools/checkstyle/sariflogger";
57      }
58  
59      /**
60       * Executes the common logging steps for SARIF logging tests.
61       * This method mimic of natural execution at is done by Checker.
62       *
63       * <p>
64       * It starts file auditing, adds an error, and completes the audit process.
65       * </p>
66       *
67       * @param instance  the current instance of {@code SarifLoggerTest}, used for context
68       * @param logger    the {@code SarifLogger} instance to log errors
69       * @param fileName  the name of the file being audited
70       * @param violation the {@code Violation} object containing error details
71       */
72      public final void executeLogger(
73          SarifLoggerTest instance, SarifLogger logger, String fileName, Violation violation) {
74          final AuditEvent event = new AuditEvent(this, "Test.java", violation);
75          logger.fileStarted(event);
76          logger.addError(event);
77          logger.fileFinished(event);
78          logger.auditFinished(null);
79      }
80  
81      @Test
82      public void testEscape() throws Exception {
83          final String inputFile = "InputSarifLoggerEscapeAll.java";
84          final String expectedReportFile = "ExpectedSarifLoggerEscapeAll.sarif";
85          final SarifLogger logger = new SarifLogger(outStream,
86                  OutputStreamOptions.CLOSE);
87  
88          verifyWithInlineConfigParserAndLogger(
89                  getPath(inputFile), getPath(expectedReportFile), logger, outStream);
90      }
91  
92      @Test
93      public void testEscapeWithMessageKey() throws Exception {
94          final String inputFile = "InputSarifLoggerEscapeMessageKey.java";
95          final String expectedReportFile = "ExpectedSarifLoggerEscapeMessageKey.sarif";
96          final SarifLogger logger = new SarifLogger(outStream,
97                  OutputStreamOptions.CLOSE);
98  
99          verifyWithInlineConfigParserAndLogger(
100                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
101     }
102 
103     @Test
104     public void testSingleError() throws Exception {
105         final String inputFile = "InputSarifLoggerSingleError.java";
106         final String expectedReportFile = "ExpectedSarifLoggerSingleError.sarif";
107         final SarifLogger logger = new SarifLogger(outStream,
108                 OutputStreamOptions.CLOSE);
109 
110         verifyWithInlineConfigParserAndLogger(
111                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
112     }
113 
114     @Test
115     public void testAddErrorAtColumn1() throws Exception {
116         final SarifLogger logger = new SarifLogger(outStream,
117                 OutputStreamOptions.CLOSE);
118         final String inputFile = "InputSarifLoggerSingleErrorColumn1.java";
119         final String expectedOutput = "ExpectedSarifLoggerSingleErrorColumn1.sarif";
120         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
121                 getPath(expectedOutput), logger, outStream);
122     }
123 
124     @Test
125     public void testAddErrorAtColumn0() throws Exception {
126         final String inputFile = "InputSarifLoggerErrorColumn0.java";
127         final String expectedReportFile = "ExpectedSarifLoggerSingleErrorColumn0.sarif";
128         final SarifLogger logger = new SarifLogger(outStream,
129                 OutputStreamOptions.CLOSE);
130 
131         verifyWithInlineConfigParserAndLogger(
132                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
133     }
134 
135     @Test
136     public void testAddErrorWithWarningLevel() throws Exception {
137         final SarifLogger logger = new SarifLogger(outStream,
138                 OutputStreamOptions.CLOSE);
139         final String inputFile = "InputSarifLoggerSingleWarning.java";
140         final String expectedOutput = "ExpectedSarifLoggerSingleWarning.sarif";
141         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
142                 getPath(expectedOutput), logger, outStream);
143     }
144 
145     @Test
146     public void testDoubleError() throws Exception {
147         final SarifLogger logger =
148                 new SarifLogger(outStream, OutputStreamOptions.CLOSE);
149 
150         verifyWithInlineConfigParserAndLogger(
151                 getPath("InputSarifLoggerDoubleError.java"),
152                 getPath("ExpectedSarifLoggerDoubleError.sarif"),
153                 logger,
154                 outStream
155         );
156     }
157 
158     @Test
159     public void testAddException() throws Exception {
160         final SarifLogger logger = new SarifLogger(outStream,
161                 OutputStreamOptions.CLOSE);
162 
163         verifyWithInlineConfigParserAndLogger(
164                 getPath("InputSarifLoggerSingleException.java"),
165                 getPath("ExpectedSarifLoggerSingleException.sarif"),
166                 logger,
167                 outStream
168         );
169     }
170 
171     /**
172      * VerifyWithInlineConfigParserAndLogger uses Checker.process(...) and reaches
173      * logger callbacks through Checker.fireErrors(...), which triggers addError(AuditEvent).
174      * This test must call addException(AuditEvent, Throwable) directly with controlled events,
175      * including one event with a null fileName, because that exception path is not produced by
176      * the normal Checker.process(...) file-auditing flow.
177      */
178     @Test
179     public void testAddExceptions() throws IOException {
180         final SarifLogger logger = new SarifLogger(outStream,
181                 OutputStreamOptions.CLOSE);
182         logger.auditStarted(null);
183         final Violation violation =
184                 new Violation(1, 1,
185                         "messages.properties", "null", null, null,
186                         getClass(), "found an error");
187         final AuditEvent ev = new AuditEvent(this, null, violation);
188         final Violation violation2 =
189                 new Violation(1, 1,
190                         "messages.properties", "null", null, null,
191                         getClass(), "found an error");
192         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
193         logger.fileStarted(ev);
194         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
195         logger.fileFinished(ev);
196         logger.fileStarted(ev2);
197         logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
198         logger.fileFinished(ev);
199         logger.auditFinished(null);
200         verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
201     }
202 
203     @Test
204     public void testLineOnly() throws Exception {
205         final String inputFile = "InputSarifLoggerLineOnly.java";
206         final String expectedReportFile = "ExpectedSarifLoggerLineOnly.sarif";
207         final SarifLogger logger = new SarifLogger(outStream,
208             OutputStreamOptions.CLOSE);
209 
210         verifyWithInlineConfigParserAndLogger(
211                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
212         );
213     }
214 
215     @Test
216     public void testEmpty() throws Exception {
217         final String inputFile = "InputSarifLoggerEmpty.java";
218         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
219         final SarifLogger logger = new SarifLogger(outStream,
220                 OutputStreamOptions.CLOSE);
221 
222         verifyWithInlineConfigParserAndLogger(
223                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
224         );
225     }
226 
227     @Test
228     public void testAddErrorWithSpaceInPath() throws Exception {
229         final String inputFile = "InputSarifLogger Space.java";
230         final String expectedReportFile = "ExpectedSarifLoggerSpaceInPath.sarif";
231         final SarifLogger logger = new SarifLogger(outStream,
232                 OutputStreamOptions.CLOSE);
233 
234         verifyWithInlineConfigParserAndLogger(
235                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
236     }
237 
238     @Test
239     public void testAddErrorWithAbsoluteLinuxPath() throws Exception {
240         final String inputFile = "InputSarifLoggerAbsoluteLinuxPath.java";
241         final String expectedReportFile = "ExpectedSarifLoggerAbsoluteLinuxPath.sarif";
242         final SarifLogger logger = new SarifLogger(outStream,
243                 OutputStreamOptions.CLOSE);
244         verifyWithInlineConfigParserAndLogger(
245                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
246     }
247 
248     /**
249      * VerifyWithInlineConfigParserAndLogger goes through Checker.process(...), where
250      * Checker.fireErrors(...) creates AuditEvent file names from real File objects and then
251      * normalizes them via relativizePathWithCatch(...), which delegates to
252      * CommonUtil.relativizePath(...). That flow does not produce synthetic "./Test.java",
253      * so this test keeps manual AuditEvent construction.
254      */
255     @Test
256     public void testAddErrorWithRelativeLinuxPath() throws IOException {
257         final SarifLogger logger = new SarifLogger(outStream,
258                 OutputStreamOptions.CLOSE);
259         logger.auditStarted(null);
260         final Violation violation =
261                 new Violation(1, 1,
262                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
263                         getClass(), "found an error");
264         final AuditEvent ev = new AuditEvent(this, "./Test.java", violation);
265         logger.fileStarted(ev);
266         logger.addError(ev);
267         logger.fileFinished(ev);
268         logger.auditFinished(null);
269         verifyContent(getPath("ExpectedSarifLoggerRelativeLinuxPath.sarif"), outStream);
270     }
271 
272     /**
273      * The file name is taken verbatim from the analysed tree, so it may contain the literal
274      * text of a report placeholder such as {@code ${message}}. Every placeholder is filled
275      * in a single pass, so that text is kept verbatim and cannot splice the message object
276      * into the uri string.
277      */
278     @Test
279     public void testAddErrorWithPlaceholderInPath() throws IOException {
280         final SarifLogger logger = new SarifLogger(outStream,
281                 OutputStreamOptions.CLOSE);
282         logger.auditStarted(null);
283         final Violation violation =
284                 new Violation(1, 1,
285                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
286                         getClass(), "found an error");
287         final AuditEvent ev = new AuditEvent(this, "report${message}.java", violation);
288         logger.fileStarted(ev);
289         logger.addError(ev);
290         logger.fileFinished(ev);
291         logger.auditFinished(null);
292         verifyContent(getPath("ExpectedSarifLoggerPlaceholderInPath.sarif"), outStream);
293     }
294 
295     /**
296      * A violation message can carry the literal text of a placeholder such as {@code ${uri}},
297      * for instance when a check reports the text it matched. Every placeholder is filled in a
298      * single pass, so the file name is not pulled into the message text.
299      */
300     @Test
301     public void testAddErrorWithPlaceholderInMessage() throws IOException {
302         final SarifLogger logger = new SarifLogger(outStream,
303                 OutputStreamOptions.CLOSE);
304         logger.auditStarted(null);
305         final Violation violation =
306                 new Violation(1, 1,
307                         "messages.properties", "ruleId", new Object[] {"found ${uri} here"},
308                         SeverityLevel.ERROR, null, getClass(), "{0}");
309         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
310         logger.fileStarted(ev);
311         logger.addError(ev);
312         logger.fileFinished(ev);
313         logger.auditFinished(null);
314         verifyContent(getPath("ExpectedSarifLoggerPlaceholderInMessage.sarif"), outStream);
315     }
316 
317     /**
318      * Checker.process(...) obtains file paths from the current runtime File API, and
319      * Checker.fireErrors(...) passes those paths through relativizePathWithCatch(...)
320      * / CommonUtil.relativizePath(...). On Linux CI, File.getAbsolutePath() cannot produce
321      * a native "C:\\..." Windows absolute path, so this Windows-specific case is manual.
322      */
323     @Test
324     public void testAddErrorWithAbsoluteWindowsPath() throws IOException {
325         final SarifLogger logger = new SarifLogger(outStream,
326                 OutputStreamOptions.CLOSE);
327         logger.auditStarted(null);
328         final Violation violation =
329                 new Violation(1, 1,
330                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
331                         getClass(), "found an error");
332         final AuditEvent ev =
333                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
334         logger.fileStarted(ev);
335         logger.addError(ev);
336         logger.fileFinished(ev);
337         logger.auditFinished(null);
338         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
339     }
340 
341     /**
342      * VerifyWithInlineConfigParserAndLogger relies on Checker.process(...), where path strings
343      * are OS-dependent and produced from File semantics before Checker.fireErrors(...) calls
344      * relativizePathWithCatch(...) / CommonUtil.relativizePath(...). A Linux run will not emit
345      * ".\\Test.java", so this test must construct the AuditEvent manually.
346      */
347     @Test
348     public void testAddErrorWithRelativeWindowsPath() throws IOException {
349         final SarifLogger logger = new SarifLogger(outStream,
350                 OutputStreamOptions.CLOSE);
351         logger.auditStarted(null);
352         final Violation violation =
353                 new Violation(1, 1,
354                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
355                         getClass(), "found an error");
356         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
357         logger.fileStarted(ev);
358         logger.addError(ev);
359         logger.fileFinished(ev);
360         logger.auditFinished(null);
361         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
362     }
363 
364     /**
365      * A file name can contain a double quote on POSIX filesystems, and that character is not
366      * produced by the normal Checker.process(...) file-auditing flow, so the event is built
367      * manually to assert the rendered URI stays inside its JSON string instead of breaking out.
368      */
369     @Test
370     public void testAddErrorWithQuoteInPath() throws IOException {
371         final SarifLogger logger = new SarifLogger(outStream,
372                 OutputStreamOptions.CLOSE);
373         logger.auditStarted(null);
374         final Violation violation =
375                 new Violation(1, 1,
376                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
377                         getClass(), "found an error");
378         final AuditEvent ev = new AuditEvent(this, "Test\".java", violation);
379         logger.fileStarted(ev);
380         logger.addError(ev);
381         logger.fileFinished(ev);
382         logger.auditFinished(null);
383         verifyContent(getPath("ExpectedSarifLoggerQuoteInPath.sarif"), outStream);
384     }
385 
386     /**
387      * We keep this test for 100% coverage. Until #12873.
388      */
389     @Test
390     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
391         final OutputStream infoStream = new ByteArrayOutputStream();
392         final SarifLogger logger = new SarifLogger(infoStream,
393                 AutomaticBean.OutputStreamOptions.CLOSE);
394         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
395 
396         assertWithMessage("closeStream should be true")
397                 .that(closeStream)
398                 .isTrue();
399     }
400 
401     /**
402      * We keep this test for 100% coverage. Until #12873.
403      */
404     @Test
405     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
406         final OutputStream infoStream = new ByteArrayOutputStream();
407         final SarifLogger logger = new SarifLogger(infoStream,
408                 AutomaticBean.OutputStreamOptions.NONE);
409         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
410 
411         assertWithMessage("closeStream should be false")
412                 .that(closeStream)
413                 .isFalse();
414     }
415 
416     /**
417      * This test can't use verifyWithInlineConfigParserAndLogger because it
418      * checks for an exception during the logger's creation. Since the constructor
419      * fails immediately with a null parameter, we never get a logger instance
420      * to use in the inline verification process.
421      */
422     @Test
423     public void testNullOutputStreamOptions() {
424         final IllegalArgumentException exception =
425                 getExpectedThrowable(IllegalArgumentException.class, () -> {
426                     final SarifLogger logger =
427                             new SarifLogger(outStream, (OutputStreamOptions) null);
428                     // assert required to calm down eclipse's
429                     // 'The allocated object is never used' violation
430                     assertWithMessage("Null instance")
431                         .that(logger)
432                         .isNotNull();
433                 }, "Exception was expected");
434         assertWithMessage("Invalid error message")
435             .that(exception.getMessage())
436             .isEqualTo("Parameter outputStreamOptions can not be null");
437     }
438 
439     @Test
440     public void testCloseStream() throws Exception {
441         final String inputFile = "InputSarifLoggerEmpty.java";
442         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
443         final SarifLogger logger = new SarifLogger(outStream,
444                 OutputStreamOptions.CLOSE);
445 
446         verifyWithInlineConfigParserAndLogger(
447                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
448 
449         assertWithMessage("Invalid close count")
450             .that(outStream.getCloseCount())
451             .isEqualTo(1);
452     }
453 
454     @Test
455     public void testNoCloseStream() throws Exception {
456         final String inputFile = "InputSarifLoggerEmpty.java";
457         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
458         final SarifLogger logger = new SarifLogger(outStream,
459                 OutputStreamOptions.NONE);
460 
461         verifyWithInlineConfigParserAndLogger(
462                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
463 
464         assertWithMessage("Invalid close count")
465             .that(outStream.getCloseCount())
466             .isEqualTo(0);
467         assertWithMessage("Invalid flush count")
468             .that(outStream.getFlushCount())
469             .isEqualTo(1);
470     }
471 
472     @Test
473     public void testFinishLocalSetup() throws Exception {
474         final String inputFile = "InputSarifLoggerEmpty.java";
475         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
476         final SarifLogger logger = new SarifLogger(outStream,
477                 OutputStreamOptions.CLOSE);
478 
479         logger.finishLocalSetup();
480 
481         verifyWithInlineConfigParserAndLogger(
482                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
483     }
484 
485     /**
486      * SarifLogger.readResource(String) is a static utility method that loads
487      * classpath resources directly. Passing an invalid name triggers an IOException
488      * that is not reachable through the normal Checker.process(...) and
489      * Checker.fireErrors(...) execution flow, so this test must call the method
490      * directly rather than using verifyWithInlineConfigParserAndLogger.
491      */
492     @Test
493     public void testReadResourceWithInvalidName() {
494         final IOException exception =
495                 getExpectedThrowable(IOException.class, () -> {
496                     SarifLogger.readResource("random");
497                 }, "Exception expected");
498         assertWithMessage("Exception message must match")
499             .that(exception.getMessage())
500             .isEqualTo("Cannot find the resource random");
501     }
502 
503     @Test
504     public void testMultipleRules() throws Exception {
505         final String inputFile = "InputSarifLoggerMultipleRules.java";
506         final String expectedReportFile = "ExpectedSarifLoggerMultipleRules.sarif";
507         final SarifLogger logger = new SarifLogger(outStream,
508                 OutputStreamOptions.CLOSE);
509 
510         verifyWithInlineConfigParserAndLogger(
511                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
512     }
513 
514     @Test
515     public void testModuleIdSuffix() throws Exception {
516         final String inputFile = "InputSarifLoggerModuleId.java";
517         final String expectedReportFile = "ExpectedSarifLoggerModuleId.sarif";
518         final SarifLogger logger = new SarifLogger(outStream,
519                 OutputStreamOptions.CLOSE);
520 
521         verifyWithInlineConfigParserAndLogger(
522                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
523     }
524 
525     @Test
526     public void testMultipleMessageStrings() throws Exception {
527         final String inputFile = "InputSarifLoggerMultipleMessages.java";
528         final String expectedReportFile = "ExpectedSarifLoggerMultipleMessages.sarif";
529         final SarifLogger logger = new SarifLogger(outStream,
530                 OutputStreamOptions.CLOSE);
531 
532         verifyWithInlineConfigParserAndLogger(
533                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
534     }
535 
536     /**
537      * This test injects ruleMetadata via reflection to force ClassNotFoundException handling
538      * during logger.auditFinished(...). That reflective state mutation is outside the normal
539      * Checker.process(...) and Checker.fireErrors(...) helper flow.
540      */
541     @Test
542     public void testGetMessagesWithClassNotFoundException() throws Exception {
543         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
544 
545         final ModuleDetails fakeModule = new ModuleDetails();
546         fakeModule.setName("FakeCheck");
547         fakeModule.setFullQualifiedName("com.fake.NonExistentCheck");
548         fakeModule.setDescription("Fake check for testing");
549         fakeModule.addToViolationMessages("fake.message.key");
550 
551         @SuppressWarnings("unchecked")
552         final Map<Object, ModuleDetails> ruleMetadata =
553                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
554         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
555         final Object ruleKey =
556                 TestUtil.instantiate(ruleKeyClass, "com.fake.NonExistentCheck", null);
557         ruleMetadata.put(ruleKey, fakeModule);
558 
559         logger.auditFinished(null);
560 
561         verifyContent(getPath("ExpectedSarifLoggerClassNotFoundException.sarif"), outStream);
562     }
563 
564     /**
565      * This test uses reflection to inject ruleMetadata and trigger MissingResourceException
566      * handling in logger.auditFinished(...). That reflective path is not reachable through
567      * the standard Checker.process(...) / Checker.fireErrors(...) execution.
568      */
569     @Test
570     public void testGetMessagesWithMissingResourceException() throws Exception {
571         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
572 
573         final ModuleDetails fakeModule = new ModuleDetails();
574         fakeModule.setName("SarifLoggerTest");
575         fakeModule.setFullQualifiedName("com.puppycrawl.tools.checkstyle.SarifLoggerTest");
576         fakeModule.setDescription("Test class without resource bundle");
577         fakeModule.addToViolationMessages("nonexistent.message.key");
578 
579         @SuppressWarnings("unchecked")
580         final Map<Object, ModuleDetails> ruleMetadata =
581                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
582         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
583         final Object ruleKey =
584                 TestUtil.instantiate(ruleKeyClass,
585                         "com.puppycrawl.tools.checkstyle.SarifLoggerTest", null);
586         ruleMetadata.put(ruleKey, fakeModule);
587 
588         logger.auditFinished(null);
589 
590         verifyContent(getPath("ExpectedSarifLoggerMissingResourceException.sarif"), outStream);
591     }
592 
593     /**
594      * Tests that all severity levels (ERROR, WARNING, INFO, IGNORE) are correctly
595      * rendered in SARIF output to kill the switch statement mutation in renderSeverityLevel.
596      * This test uses realistic input with inline configuration rather than manual mocking
597      * to ensure proper integration testing.
598      *
599      * @throws Exception if an error occurs during verification
600      */
601     @Test
602     public void testRenderSeverityLevelAllLevels() throws Exception {
603         final String inputFile = "InputSarifLoggerAllSeverityLevels.java";
604         final String expectedReportFile = "ExpectedSarifLoggerAllSeverityLevels.sarif";
605         final SarifLogger logger = new SarifLogger(outStream,
606                 OutputStreamOptions.CLOSE);
607 
608         verifyWithInlineConfigParserAndLogger(
609                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
610     }
611 
612     private static void verifyContent(
613             String expectedOutputFile,
614             ByteArrayOutputStream actualOutputStream) throws IOException {
615         final String expectedContent = readFile(expectedOutputFile);
616         final String actualContent =
617                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
618         assertWithMessage("sarif content should match")
619             .that(actualContent)
620             .isEqualTo(expectedContent);
621     }
622 
623     private static final class TestException extends RuntimeException {
624         @Serial
625         private static final long serialVersionUID = 1L;
626 
627         private TestException(String msg, Throwable cause) {
628             super(msg, cause);
629         }
630 
631         @Override
632         public void printStackTrace(PrintWriter printWriter) {
633             printWriter.print("stackTrace\nexample");
634         }
635     }
636 
637 }