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      * Checker.process(...) obtains file paths from the current runtime File API, and
274      * Checker.fireErrors(...) passes those paths through relativizePathWithCatch(...)
275      * / CommonUtil.relativizePath(...). On Linux CI, File.getAbsolutePath() cannot produce
276      * a native "C:\\..." Windows absolute path, so this Windows-specific case is manual.
277      */
278     @Test
279     public void testAddErrorWithAbsoluteWindowsPath() 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 =
288                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
289         logger.fileStarted(ev);
290         logger.addError(ev);
291         logger.fileFinished(ev);
292         logger.auditFinished(null);
293         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
294     }
295 
296     /**
297      * VerifyWithInlineConfigParserAndLogger relies on Checker.process(...), where path strings
298      * are OS-dependent and produced from File semantics before Checker.fireErrors(...) calls
299      * relativizePathWithCatch(...) / CommonUtil.relativizePath(...). A Linux run will not emit
300      * ".\\Test.java", so this test must construct the AuditEvent manually.
301      */
302     @Test
303     public void testAddErrorWithRelativeWindowsPath() throws IOException {
304         final SarifLogger logger = new SarifLogger(outStream,
305                 OutputStreamOptions.CLOSE);
306         logger.auditStarted(null);
307         final Violation violation =
308                 new Violation(1, 1,
309                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
310                         getClass(), "found an error");
311         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
312         logger.fileStarted(ev);
313         logger.addError(ev);
314         logger.fileFinished(ev);
315         logger.auditFinished(null);
316         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
317     }
318 
319     /**
320      * A file name can contain a double quote on POSIX filesystems, and that character is not
321      * produced by the normal Checker.process(...) file-auditing flow, so the event is built
322      * manually to assert the rendered URI stays inside its JSON string instead of breaking out.
323      */
324     @Test
325     public void testAddErrorWithQuoteInPath() throws IOException {
326         final SarifLogger logger = new SarifLogger(outStream,
327                 OutputStreamOptions.CLOSE);
328         logger.auditStarted(null);
329         final Violation violation =
330                 new Violation(1, 1,
331                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
332                         getClass(), "found an error");
333         final AuditEvent ev = new AuditEvent(this, "Test\".java", violation);
334         logger.fileStarted(ev);
335         logger.addError(ev);
336         logger.fileFinished(ev);
337         logger.auditFinished(null);
338         verifyContent(getPath("ExpectedSarifLoggerQuoteInPath.sarif"), outStream);
339     }
340 
341     /**
342      * We keep this test for 100% coverage. Until #12873.
343      */
344     @Test
345     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
346         final OutputStream infoStream = new ByteArrayOutputStream();
347         final SarifLogger logger = new SarifLogger(infoStream,
348                 AutomaticBean.OutputStreamOptions.CLOSE);
349         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
350 
351         assertWithMessage("closeStream should be true")
352                 .that(closeStream)
353                 .isTrue();
354     }
355 
356     /**
357      * We keep this test for 100% coverage. Until #12873.
358      */
359     @Test
360     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
361         final OutputStream infoStream = new ByteArrayOutputStream();
362         final SarifLogger logger = new SarifLogger(infoStream,
363                 AutomaticBean.OutputStreamOptions.NONE);
364         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
365 
366         assertWithMessage("closeStream should be false")
367                 .that(closeStream)
368                 .isFalse();
369     }
370 
371     /**
372      * This test can't use verifyWithInlineConfigParserAndLogger because it
373      * checks for an exception during the logger's creation. Since the constructor
374      * fails immediately with a null parameter, we never get a logger instance
375      * to use in the inline verification process.
376      */
377     @Test
378     public void testNullOutputStreamOptions() {
379         final IllegalArgumentException exception =
380                 getExpectedThrowable(IllegalArgumentException.class, () -> {
381                     final SarifLogger logger =
382                             new SarifLogger(outStream, (OutputStreamOptions) null);
383                     // assert required to calm down eclipse's
384                     // 'The allocated object is never used' violation
385                     assertWithMessage("Null instance")
386                         .that(logger)
387                         .isNotNull();
388                 }, "Exception was expected");
389         assertWithMessage("Invalid error message")
390             .that(exception.getMessage())
391             .isEqualTo("Parameter outputStreamOptions can not be null");
392     }
393 
394     @Test
395     public void testCloseStream() throws Exception {
396         final String inputFile = "InputSarifLoggerEmpty.java";
397         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
398         final SarifLogger logger = new SarifLogger(outStream,
399                 OutputStreamOptions.CLOSE);
400 
401         verifyWithInlineConfigParserAndLogger(
402                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
403 
404         assertWithMessage("Invalid close count")
405             .that(outStream.getCloseCount())
406             .isEqualTo(1);
407     }
408 
409     @Test
410     public void testNoCloseStream() throws Exception {
411         final String inputFile = "InputSarifLoggerEmpty.java";
412         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
413         final SarifLogger logger = new SarifLogger(outStream,
414                 OutputStreamOptions.NONE);
415 
416         verifyWithInlineConfigParserAndLogger(
417                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
418 
419         assertWithMessage("Invalid close count")
420             .that(outStream.getCloseCount())
421             .isEqualTo(0);
422         assertWithMessage("Invalid flush count")
423             .that(outStream.getFlushCount())
424             .isEqualTo(1);
425     }
426 
427     @Test
428     public void testFinishLocalSetup() throws Exception {
429         final String inputFile = "InputSarifLoggerEmpty.java";
430         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
431         final SarifLogger logger = new SarifLogger(outStream,
432                 OutputStreamOptions.CLOSE);
433 
434         logger.finishLocalSetup();
435 
436         verifyWithInlineConfigParserAndLogger(
437                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
438     }
439 
440     /**
441      * SarifLogger.readResource(String) is a static utility method that loads
442      * classpath resources directly. Passing an invalid name triggers an IOException
443      * that is not reachable through the normal Checker.process(...) and
444      * Checker.fireErrors(...) execution flow, so this test must call the method
445      * directly rather than using verifyWithInlineConfigParserAndLogger.
446      */
447     @Test
448     public void testReadResourceWithInvalidName() {
449         final IOException exception =
450                 getExpectedThrowable(IOException.class, () -> {
451                     SarifLogger.readResource("random");
452                 }, "Exception expected");
453         assertWithMessage("Exception message must match")
454             .that(exception.getMessage())
455             .isEqualTo("Cannot find the resource random");
456     }
457 
458     @Test
459     public void testMultipleRules() throws Exception {
460         final String inputFile = "InputSarifLoggerMultipleRules.java";
461         final String expectedReportFile = "ExpectedSarifLoggerMultipleRules.sarif";
462         final SarifLogger logger = new SarifLogger(outStream,
463                 OutputStreamOptions.CLOSE);
464 
465         verifyWithInlineConfigParserAndLogger(
466                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
467     }
468 
469     @Test
470     public void testModuleIdSuffix() throws Exception {
471         final String inputFile = "InputSarifLoggerModuleId.java";
472         final String expectedReportFile = "ExpectedSarifLoggerModuleId.sarif";
473         final SarifLogger logger = new SarifLogger(outStream,
474                 OutputStreamOptions.CLOSE);
475 
476         verifyWithInlineConfigParserAndLogger(
477                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
478     }
479 
480     @Test
481     public void testMultipleMessageStrings() throws Exception {
482         final String inputFile = "InputSarifLoggerMultipleMessages.java";
483         final String expectedReportFile = "ExpectedSarifLoggerMultipleMessages.sarif";
484         final SarifLogger logger = new SarifLogger(outStream,
485                 OutputStreamOptions.CLOSE);
486 
487         verifyWithInlineConfigParserAndLogger(
488                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
489     }
490 
491     /**
492      * This test injects ruleMetadata via reflection to force ClassNotFoundException handling
493      * during logger.auditFinished(...). That reflective state mutation is outside the normal
494      * Checker.process(...) and Checker.fireErrors(...) helper flow.
495      */
496     @Test
497     public void testGetMessagesWithClassNotFoundException() throws Exception {
498         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
499 
500         final ModuleDetails fakeModule = new ModuleDetails();
501         fakeModule.setName("FakeCheck");
502         fakeModule.setFullQualifiedName("com.fake.NonExistentCheck");
503         fakeModule.setDescription("Fake check for testing");
504         fakeModule.addToViolationMessages("fake.message.key");
505 
506         @SuppressWarnings("unchecked")
507         final Map<Object, ModuleDetails> ruleMetadata =
508                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
509         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
510         final Object ruleKey =
511                 TestUtil.instantiate(ruleKeyClass, "com.fake.NonExistentCheck", null);
512         ruleMetadata.put(ruleKey, fakeModule);
513 
514         logger.auditFinished(null);
515 
516         verifyContent(getPath("ExpectedSarifLoggerClassNotFoundException.sarif"), outStream);
517     }
518 
519     /**
520      * This test uses reflection to inject ruleMetadata and trigger MissingResourceException
521      * handling in logger.auditFinished(...). That reflective path is not reachable through
522      * the standard Checker.process(...) / Checker.fireErrors(...) execution.
523      */
524     @Test
525     public void testGetMessagesWithMissingResourceException() throws Exception {
526         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
527 
528         final ModuleDetails fakeModule = new ModuleDetails();
529         fakeModule.setName("SarifLoggerTest");
530         fakeModule.setFullQualifiedName("com.puppycrawl.tools.checkstyle.SarifLoggerTest");
531         fakeModule.setDescription("Test class without resource bundle");
532         fakeModule.addToViolationMessages("nonexistent.message.key");
533 
534         @SuppressWarnings("unchecked")
535         final Map<Object, ModuleDetails> ruleMetadata =
536                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
537         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
538         final Object ruleKey =
539                 TestUtil.instantiate(ruleKeyClass,
540                         "com.puppycrawl.tools.checkstyle.SarifLoggerTest", null);
541         ruleMetadata.put(ruleKey, fakeModule);
542 
543         logger.auditFinished(null);
544 
545         verifyContent(getPath("ExpectedSarifLoggerMissingResourceException.sarif"), outStream);
546     }
547 
548     /**
549      * Tests that all severity levels (ERROR, WARNING, INFO, IGNORE) are correctly
550      * rendered in SARIF output to kill the switch statement mutation in renderSeverityLevel.
551      * This test uses realistic input with inline configuration rather than manual mocking
552      * to ensure proper integration testing.
553      *
554      * @throws Exception if an error occurs during verification
555      */
556     @Test
557     public void testRenderSeverityLevelAllLevels() throws Exception {
558         final String inputFile = "InputSarifLoggerAllSeverityLevels.java";
559         final String expectedReportFile = "ExpectedSarifLoggerAllSeverityLevels.sarif";
560         final SarifLogger logger = new SarifLogger(outStream,
561                 OutputStreamOptions.CLOSE);
562 
563         verifyWithInlineConfigParserAndLogger(
564                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
565     }
566 
567     private static void verifyContent(
568             String expectedOutputFile,
569             ByteArrayOutputStream actualOutputStream) throws IOException {
570         final String expectedContent = readFile(expectedOutputFile);
571         final String actualContent =
572                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
573         assertWithMessage("sarif content should match")
574             .that(actualContent)
575             .isEqualTo(expectedContent);
576     }
577 
578     private static final class TestException extends RuntimeException {
579         @Serial
580         private static final long serialVersionUID = 1L;
581 
582         private TestException(String msg, Throwable cause) {
583             super(msg, cause);
584         }
585 
586         @Override
587         public void printStackTrace(PrintWriter printWriter) {
588             printWriter.print("stackTrace\nexample");
589         }
590     }
591 }