View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStream;
27  import java.io.PrintWriter;
28  import java.io.Serial;
29  import java.nio.charset.StandardCharsets;
30  import java.util.Map;
31  
32  import org.junit.jupiter.api.Test;
33  
34  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
35  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
36  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
37  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
38  import com.puppycrawl.tools.checkstyle.api.Violation;
39  import com.puppycrawl.tools.checkstyle.internal.utils.CloseAndFlushTestByteArrayOutputStream;
40  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
41  import com.puppycrawl.tools.checkstyle.meta.ModuleDetails;
42  
43  public class SarifLoggerTest extends AbstractModuleTestSupport {
44  
45      /**
46       * Output stream to hold the test results. The IntelliJ IDEA issues the AutoCloseableResource
47       * warning here, so it needs to be suppressed. The {@code ByteArrayOutputStream} does not hold
48       * any resources that need to be released.
49       */
50      private final CloseAndFlushTestByteArrayOutputStream outStream =
51          new CloseAndFlushTestByteArrayOutputStream();
52  
53      @Override
54      public String getPackageLocation() {
55          return "com/puppycrawl/tools/checkstyle/sariflogger";
56      }
57  
58      /**
59       * Executes the common logging steps for SARIF logging tests.
60       * This method mimic of natural execution at is done by Checker.
61       *
62       * <p>
63       * It starts file auditing, adds an error, and completes the audit process.
64       * </p>
65       *
66       * @param instance  the current instance of {@code SarifLoggerTest}, used for context
67       * @param logger    the {@code SarifLogger} instance to log errors
68       * @param fileName  the name of the file being audited
69       * @param violation the {@code Violation} object containing error details
70       */
71      public final void executeLogger(
72          SarifLoggerTest instance, SarifLogger logger, String fileName, Violation violation) {
73          final AuditEvent event = new AuditEvent(this, "Test.java", violation);
74          logger.fileStarted(event);
75          logger.addError(event);
76          logger.fileFinished(event);
77          logger.auditFinished(null);
78      }
79  
80      @Test
81      public void testEscape() {
82          final String[][] encodings = {
83              {"\"", "\\\""},
84              {"\\", "\\\\"},
85              {"\b", "\\b"},
86              {"\f", "\\f"},
87              {"\n", "\\n"},
88              {"\r", "\\r"},
89              {"\t", "\\t"},
90              {"/", "\\/"},
91              {"\u0010", "\\u0010"},
92              {"\u001E", "\\u001E"},
93              {"\u001F", "\\u001F"},
94              {" ", " "},
95              {"bar1234", "bar1234"},
96          };
97          for (String[] encoding : encodings) {
98              final String encoded = SarifLogger.escape(encoding[0]);
99              assertWithMessage("\"%s\"", encoding[0])
100                 .that(encoded)
101                 .isEqualTo(encoding[1]);
102         }
103     }
104 
105     @Test
106     public void testSingleError() throws Exception {
107         final String inputFile = "InputSarifLoggerSingleError.java";
108         final String expectedReportFile = "ExpectedSarifLoggerSingleError.sarif";
109         final SarifLogger logger = new SarifLogger(outStream,
110                 OutputStreamOptions.CLOSE);
111 
112         verifyWithInlineConfigParserAndLogger(
113                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
114     }
115 
116     @Test
117     public void testAddErrorAtColumn1() throws Exception {
118         final SarifLogger logger = new SarifLogger(outStream,
119                 OutputStreamOptions.CLOSE);
120         final String inputFile = "InputSarifLoggerSingleErrorColumn1.java";
121         final String expectedOutput = "ExpectedSarifLoggerSingleErrorColumn1.sarif";
122         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
123                 getPath(expectedOutput), logger, outStream);
124     }
125 
126     @Test
127     public void testAddErrorAtColumn0() throws Exception {
128         final String inputFile = "InputSarifLoggerErrorColumn0.java";
129         final String expectedReportFile = "ExpectedSarifLoggerSingleErrorColumn0.sarif";
130         final SarifLogger logger = new SarifLogger(outStream,
131                 OutputStreamOptions.CLOSE);
132 
133         verifyWithInlineConfigParserAndLogger(
134                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
135     }
136 
137     @Test
138     public void testAddErrorWithWarningLevel() throws Exception {
139         final SarifLogger logger = new SarifLogger(outStream,
140                 OutputStreamOptions.CLOSE);
141         final String inputFile = "InputSarifLoggerSingleWarning.java";
142         final String expectedOutput = "ExpectedSarifLoggerSingleWarning.sarif";
143         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
144                 getPath(expectedOutput), logger, outStream);
145     }
146 
147     @Test
148     public void testDoubleError() throws Exception {
149         final SarifLogger logger =
150                 new SarifLogger(outStream, OutputStreamOptions.CLOSE);
151 
152         verifyWithInlineConfigParserAndLogger(
153                 getPath("InputSarifLoggerDoubleError.java"),
154                 getPath("ExpectedSarifLoggerDoubleError.sarif"),
155                 logger,
156                 outStream
157         );
158     }
159 
160     @Test
161     public void testAddException() throws IOException {
162         final SarifLogger logger = new SarifLogger(outStream,
163                 OutputStreamOptions.CLOSE);
164         logger.auditStarted(null);
165         final Violation message =
166                 new Violation(1, 1,
167                         "messages.properties", "null", null, null,
168                         getClass(), "found an error");
169         final AuditEvent ev = new AuditEvent(this, null, message);
170         logger.fileStarted(ev);
171         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
172         logger.fileFinished(ev);
173         logger.auditFinished(null);
174         verifyContent(getPath("ExpectedSarifLoggerSingleException.sarif"), outStream);
175     }
176 
177     @Test
178     public void testAddExceptions() throws IOException {
179         final SarifLogger logger = new SarifLogger(outStream,
180                 OutputStreamOptions.CLOSE);
181         logger.auditStarted(null);
182         final Violation violation =
183                 new Violation(1, 1,
184                         "messages.properties", "null", null, null,
185                         getClass(), "found an error");
186         final AuditEvent ev = new AuditEvent(this, null, violation);
187         final Violation violation2 =
188                 new Violation(1, 1,
189                         "messages.properties", "null", null, null,
190                         getClass(), "found an error");
191         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
192         logger.fileStarted(ev);
193         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
194         logger.fileFinished(ev);
195         logger.fileStarted(ev2);
196         logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
197         logger.fileFinished(ev);
198         logger.auditFinished(null);
199         verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
200     }
201 
202     @Test
203     public void testLineOnly() throws Exception {
204         final String inputFile = "InputSarifLoggerLineOnly.java";
205         final String expectedReportFile = "ExpectedSarifLoggerLineOnly.sarif";
206         final SarifLogger logger = new SarifLogger(outStream,
207             OutputStreamOptions.CLOSE);
208 
209         verifyWithInlineConfigParserAndLogger(
210                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
211         );
212     }
213 
214     @Test
215     public void testEmpty() throws Exception {
216         final String inputFile = "InputSarifLoggerEmpty.java";
217         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
218         final SarifLogger logger = new SarifLogger(outStream,
219                 OutputStreamOptions.CLOSE);
220 
221         verifyWithInlineConfigParserAndLogger(
222                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
223         );
224     }
225 
226     @Test
227     public void testAddErrorWithSpaceInPath() throws Exception {
228         final String inputFile = "InputSarifLogger Space.java";
229         final String expectedReportFile = "ExpectedSarifLoggerSpaceInPath.sarif";
230         final SarifLogger logger = new SarifLogger(outStream,
231                 OutputStreamOptions.CLOSE);
232 
233         verifyWithInlineConfigParserAndLogger(
234                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
235     }
236 
237     @Test
238     public void testAddErrorWithAbsoluteLinuxPath() throws Exception {
239         final String inputFile = "InputSarifLoggerAbsoluteLinuxPath.java";
240         final String expectedReportFile = "ExpectedSarifLoggerAbsoluteLinuxPath.sarif";
241         final SarifLogger logger = new SarifLogger(outStream,
242                 OutputStreamOptions.CLOSE);
243         verifyWithInlineConfigParserAndLogger(
244                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
245     }
246 
247     @Test
248     public void testAddErrorWithRelativeLinuxPath() throws IOException {
249         final SarifLogger logger = new SarifLogger(outStream,
250                 OutputStreamOptions.CLOSE);
251         logger.auditStarted(null);
252         final Violation violation =
253                 new Violation(1, 1,
254                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
255                         getClass(), "found an error");
256         final AuditEvent ev = new AuditEvent(this, "./Test.java", violation);
257         logger.fileStarted(ev);
258         logger.addError(ev);
259         logger.fileFinished(ev);
260         logger.auditFinished(null);
261         verifyContent(getPath("ExpectedSarifLoggerRelativeLinuxPath.sarif"), outStream);
262     }
263 
264     @Test
265     public void testAddErrorWithAbsoluteWindowsPath() throws IOException {
266         final SarifLogger logger = new SarifLogger(outStream,
267                 OutputStreamOptions.CLOSE);
268         logger.auditStarted(null);
269         final Violation violation =
270                 new Violation(1, 1,
271                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
272                         getClass(), "found an error");
273         final AuditEvent ev =
274                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
275         logger.fileStarted(ev);
276         logger.addError(ev);
277         logger.fileFinished(ev);
278         logger.auditFinished(null);
279         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
280     }
281 
282     @Test
283     public void testAddErrorWithRelativeWindowsPath() throws IOException {
284         final SarifLogger logger = new SarifLogger(outStream,
285                 OutputStreamOptions.CLOSE);
286         logger.auditStarted(null);
287         final Violation violation =
288                 new Violation(1, 1,
289                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
290                         getClass(), "found an error");
291         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
292         logger.fileStarted(ev);
293         logger.addError(ev);
294         logger.fileFinished(ev);
295         logger.auditFinished(null);
296         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
297     }
298 
299     /**
300      * We keep this test for 100% coverage. Until #12873.
301      */
302     @Test
303     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
304         final OutputStream infoStream = new ByteArrayOutputStream();
305         final SarifLogger logger = new SarifLogger(infoStream,
306                 AutomaticBean.OutputStreamOptions.CLOSE);
307         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
308 
309         assertWithMessage("closeStream should be true")
310                 .that(closeStream)
311                 .isTrue();
312     }
313 
314     /**
315      * We keep this test for 100% coverage. Until #12873.
316      */
317     @Test
318     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
319         final OutputStream infoStream = new ByteArrayOutputStream();
320         final SarifLogger logger = new SarifLogger(infoStream,
321                 AutomaticBean.OutputStreamOptions.NONE);
322         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
323 
324         assertWithMessage("closeStream should be false")
325                 .that(closeStream)
326                 .isFalse();
327     }
328 
329     @Test
330     public void testNullOutputStreamOptions() {
331         try {
332             final SarifLogger logger = new SarifLogger(outStream, (OutputStreamOptions) null);
333             // assert required to calm down eclipse's 'The allocated object is never used' violation
334             assertWithMessage("Null instance")
335                 .that(logger)
336                 .isNotNull();
337             assertWithMessage("Exception was expected").fail();
338         }
339         catch (IllegalArgumentException | IOException exception) {
340             assertWithMessage("Invalid error message")
341                 .that(exception.getMessage())
342                 .isEqualTo("Parameter outputStreamOptions can not be null");
343         }
344     }
345 
346     @Test
347     public void testCloseStream() throws Exception {
348         final String inputFile = "InputSarifLoggerEmpty.java";
349         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
350         final SarifLogger logger = new SarifLogger(outStream,
351                 OutputStreamOptions.CLOSE);
352 
353         verifyWithInlineConfigParserAndLogger(
354                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
355 
356         assertWithMessage("Invalid close count")
357             .that(outStream.getCloseCount())
358             .isEqualTo(1);
359     }
360 
361     @Test
362     public void testNoCloseStream() throws IOException {
363         final SarifLogger logger = new SarifLogger(outStream,
364                 OutputStreamOptions.NONE);
365         logger.auditStarted(null);
366         logger.auditFinished(null);
367 
368         assertWithMessage("Invalid close count")
369             .that(outStream.getCloseCount())
370             .isEqualTo(0);
371         assertWithMessage("Invalid flush count")
372             .that(outStream.getFlushCount())
373             .isEqualTo(1);
374 
375         outStream.close();
376         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
377     }
378 
379     @Test
380     public void testFinishLocalSetup() throws IOException {
381         final SarifLogger logger = new SarifLogger(outStream,
382                 OutputStreamOptions.CLOSE);
383         logger.finishLocalSetup();
384         logger.auditStarted(null);
385         logger.auditFinished(null);
386         assertWithMessage("instance should not be null")
387             .that(logger)
388             .isNotNull();
389     }
390 
391     @Test
392     public void testReadResourceWithInvalidName() {
393         try {
394             SarifLogger.readResource("random");
395             assertWithMessage("Exception expected").fail();
396         }
397         catch (IOException exception) {
398             assertWithMessage("Exception message must match")
399                 .that(exception.getMessage())
400                 .isEqualTo("Cannot find the resource random");
401         }
402     }
403 
404     @Test
405     public void testMultipleRules() throws Exception {
406         final String inputFile = "InputSarifLoggerMultipleRules.java";
407         final String expectedReportFile = "ExpectedSarifLoggerMultipleRules.sarif";
408         final SarifLogger logger = new SarifLogger(outStream,
409                 OutputStreamOptions.CLOSE);
410 
411         verifyWithInlineConfigParserAndLogger(
412                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
413     }
414 
415     @Test
416     public void testModuleIdSuffix() throws Exception {
417         final String inputFile = "InputSarifLoggerModuleId.java";
418         final String expectedReportFile = "ExpectedSarifLoggerModuleId.sarif";
419         final SarifLogger logger = new SarifLogger(outStream,
420                 OutputStreamOptions.CLOSE);
421 
422         verifyWithInlineConfigParserAndLogger(
423                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
424     }
425 
426     @Test
427     public void testMultipleMessageStrings() throws Exception {
428         final String inputFile = "InputSarifLoggerMultipleMessages.java";
429         final String expectedReportFile = "ExpectedSarifLoggerMultipleMessages.sarif";
430         final SarifLogger logger = new SarifLogger(outStream,
431                 OutputStreamOptions.CLOSE);
432 
433         verifyWithInlineConfigParserAndLogger(
434                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
435     }
436 
437     @Test
438     public void testEscapedMessageText() throws Exception {
439         final String inputFile = "InputSarifLoggerEscapedMessage.java";
440         final String expectedReportFile = "ExpectedSarifLoggerEscapedMessage.sarif";
441         final SarifLogger logger = new SarifLogger(outStream,
442                 OutputStreamOptions.CLOSE);
443 
444         verifyWithInlineConfigParserAndLogger(
445                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
446     }
447 
448     /**
449      * Tests {@code ClassNotFoundException} handling in {@code getMessages} method.
450      * Uses reflection to inject fake module metadata with non-existent class
451      * to trigger the exception. Real checkstyle modules cannot be used as
452      * all are on the classpath during testing.
453      */
454     @Test
455     public void testGetMessagesWithClassNotFoundException() throws Exception {
456         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
457 
458         final ModuleDetails fakeModule = new ModuleDetails();
459         fakeModule.setName("FakeCheck");
460         fakeModule.setFullQualifiedName("com.fake.NonExistentCheck");
461         fakeModule.setDescription("Fake check for testing");
462         fakeModule.addToViolationMessages("fake.message.key");
463 
464         @SuppressWarnings("unchecked")
465         final Map<Object, ModuleDetails> ruleMetadata =
466                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
467         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
468         final Object ruleKey =
469                 TestUtil.instantiate(ruleKeyClass, "com.fake.NonExistentCheck", null);
470         ruleMetadata.put(ruleKey, fakeModule);
471 
472         logger.auditFinished(null);
473 
474         verifyContent(getPath("ExpectedSarifLoggerClassNotFoundException.sarif"), outStream);
475     }
476 
477     /**
478      * Tests {@code MissingResourceException} handling in {@code getMessages} method.
479      * Uses reflection to inject fake module metadata pointing to a test class
480      * without messages.properties to trigger the exception. Real checkstyle modules
481      * cannot be used as all have proper resource bundles during testing.
482      */
483     @Test
484     public void testGetMessagesWithMissingResourceException() throws Exception {
485         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
486 
487         final ModuleDetails fakeModule = new ModuleDetails();
488         fakeModule.setName("SarifLoggerTest");
489         fakeModule.setFullQualifiedName("com.puppycrawl.tools.checkstyle.SarifLoggerTest");
490         fakeModule.setDescription("Test class without resource bundle");
491         fakeModule.addToViolationMessages("nonexistent.message.key");
492 
493         @SuppressWarnings("unchecked")
494         final Map<Object, ModuleDetails> ruleMetadata =
495                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
496         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
497         final Object ruleKey =
498                 TestUtil.instantiate(ruleKeyClass,
499                         "com.puppycrawl.tools.checkstyle.SarifLoggerTest", null);
500         ruleMetadata.put(ruleKey, fakeModule);
501 
502         logger.auditFinished(null);
503 
504         verifyContent(getPath("ExpectedSarifLoggerMissingResourceException.sarif"), outStream);
505     }
506 
507     /**
508      * Tests that all severity levels (ERROR, WARNING, INFO, IGNORE) are correctly
509      * rendered in SARIF output to kill the switch statement mutation in renderSeverityLevel.
510      * This test uses realistic input with inline configuration rather than manual mocking
511      * to ensure proper integration testing.
512      *
513      * @throws Exception if an error occurs during verification
514      */
515     @Test
516     public void testRenderSeverityLevelAllLevels() throws Exception {
517         final String inputFile = "InputSarifLoggerAllSeverityLevels.java";
518         final String expectedReportFile = "ExpectedSarifLoggerAllSeverityLevels.sarif";
519         final SarifLogger logger = new SarifLogger(outStream,
520                 OutputStreamOptions.CLOSE);
521 
522         verifyWithInlineConfigParserAndLogger(
523                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
524     }
525 
526     private static void verifyContent(
527             String expectedOutputFile,
528             ByteArrayOutputStream actualOutputStream) throws IOException {
529         final String expectedContent = readFile(expectedOutputFile);
530         final String actualContent =
531                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
532         assertWithMessage("sarif content should match")
533             .that(actualContent)
534             .isEqualTo(expectedContent);
535     }
536 
537     private static final class TestException extends RuntimeException {
538         @Serial
539         private static final long serialVersionUID = 1L;
540 
541         private TestException(String msg, Throwable cause) {
542             super(msg, cause);
543         }
544 
545         @Override
546         public void printStackTrace(PrintWriter printWriter) {
547             printWriter.print("stackTrace\nexample");
548         }
549     }
550 }