View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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      protected 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("\"" + 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 String inputFile = "InputSarifLoggerDoubleError.java";
150         final String expectedReportFile = "ExpectedSarifLoggerDoubleError.sarif";
151         final SarifLogger logger = new SarifLogger(outStream,
152                 OutputStreamOptions.CLOSE);
153 
154         verifyWithInlineConfigParserAndLogger(
155                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
156     }
157 
158     @Test
159     public void testAddException() throws IOException {
160         final SarifLogger logger = new SarifLogger(outStream,
161                 OutputStreamOptions.CLOSE);
162         logger.auditStarted(null);
163         final Violation message =
164                 new Violation(1, 1,
165                         "messages.properties", "null", null, null,
166                         getClass(), "found an error");
167         final AuditEvent ev = new AuditEvent(this, null, message);
168         logger.fileStarted(ev);
169         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
170         logger.fileFinished(ev);
171         logger.auditFinished(null);
172         verifyContent(getPath("ExpectedSarifLoggerSingleException.sarif"), outStream);
173     }
174 
175     @Test
176     public void testAddExceptions() throws IOException {
177         final SarifLogger logger = new SarifLogger(outStream,
178                 OutputStreamOptions.CLOSE);
179         logger.auditStarted(null);
180         final Violation violation =
181                 new Violation(1, 1,
182                         "messages.properties", "null", null, null,
183                         getClass(), "found an error");
184         final AuditEvent ev = new AuditEvent(this, null, violation);
185         final Violation violation2 =
186                 new Violation(1, 1,
187                         "messages.properties", "null", null, null,
188                         getClass(), "found an error");
189         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
190         logger.fileStarted(ev);
191         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
192         logger.fileFinished(ev);
193         logger.fileStarted(ev2);
194         logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
195         logger.fileFinished(ev);
196         logger.auditFinished(null);
197         verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
198     }
199 
200     @Test
201     public void testLineOnly() throws Exception {
202         final String inputFile = "InputSarifLoggerLineOnly.java";
203         final String expectedReportFile = "ExpectedSarifLoggerLineOnly.sarif";
204         final SarifLogger logger = new SarifLogger(outStream,
205             OutputStreamOptions.CLOSE);
206 
207         verifyWithInlineConfigParserAndLogger(
208                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
209         );
210     }
211 
212     @Test
213     public void testEmpty() throws Exception {
214         final String inputFile = "InputSarifLoggerEmpty.java";
215         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
216         final SarifLogger logger = new SarifLogger(outStream,
217                 OutputStreamOptions.CLOSE);
218 
219         verifyWithInlineConfigParserAndLogger(
220                 getPath(inputFile), getPath(expectedReportFile), logger, outStream
221         );
222     }
223 
224     @Test
225     public void testAddErrorWithSpaceInPath() throws IOException {
226         final SarifLogger logger = new SarifLogger(outStream,
227                 OutputStreamOptions.CLOSE);
228         logger.auditStarted(null);
229         final Violation violation =
230                 new Violation(1, 1,
231                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
232                         getClass(), "found an error");
233         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test 2.java", violation);
234         logger.fileStarted(ev);
235         logger.addError(ev);
236         logger.fileFinished(ev);
237         logger.auditFinished(null);
238         verifyContent(getPath("ExpectedSarifLoggerSpaceInPath.sarif"), outStream);
239     }
240 
241     @Test
242     public void testAddErrorWithAbsoluteLinuxPath() throws IOException {
243         final SarifLogger logger = new SarifLogger(outStream,
244                 OutputStreamOptions.CLOSE);
245         logger.auditStarted(null);
246         final Violation violation =
247                 new Violation(1, 1,
248                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
249                         getClass(), "found an error");
250         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test.java", violation);
251         logger.fileStarted(ev);
252         logger.addError(ev);
253         logger.fileFinished(ev);
254         logger.auditFinished(null);
255         verifyContent(getPath("ExpectedSarifLoggerAbsoluteLinuxPath.sarif"), outStream);
256     }
257 
258     @Test
259     public void testAddErrorWithRelativeLinuxPath() throws IOException {
260         final SarifLogger logger = new SarifLogger(outStream,
261                 OutputStreamOptions.CLOSE);
262         logger.auditStarted(null);
263         final Violation violation =
264                 new Violation(1, 1,
265                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
266                         getClass(), "found an error");
267         final AuditEvent ev = new AuditEvent(this, "./Test.java", violation);
268         logger.fileStarted(ev);
269         logger.addError(ev);
270         logger.fileFinished(ev);
271         logger.auditFinished(null);
272         verifyContent(getPath("ExpectedSarifLoggerRelativeLinuxPath.sarif"), outStream);
273     }
274 
275     @Test
276     public void testAddErrorWithAbsoluteWindowsPath() throws IOException {
277         final SarifLogger logger = new SarifLogger(outStream,
278                 OutputStreamOptions.CLOSE);
279         logger.auditStarted(null);
280         final Violation violation =
281                 new Violation(1, 1,
282                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
283                         getClass(), "found an error");
284         final AuditEvent ev =
285                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
286         logger.fileStarted(ev);
287         logger.addError(ev);
288         logger.fileFinished(ev);
289         logger.auditFinished(null);
290         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
291     }
292 
293     @Test
294     public void testAddErrorWithRelativeWindowsPath() throws IOException {
295         final SarifLogger logger = new SarifLogger(outStream,
296                 OutputStreamOptions.CLOSE);
297         logger.auditStarted(null);
298         final Violation violation =
299                 new Violation(1, 1,
300                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
301                         getClass(), "found an error");
302         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
303         logger.fileStarted(ev);
304         logger.addError(ev);
305         logger.fileFinished(ev);
306         logger.auditFinished(null);
307         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
308     }
309 
310     /**
311      * We keep this test for 100% coverage. Until #12873.
312      */
313     @Test
314     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
315         final OutputStream infoStream = new ByteArrayOutputStream();
316         final SarifLogger logger = new SarifLogger(infoStream,
317                 AutomaticBean.OutputStreamOptions.CLOSE);
318         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
319 
320         assertWithMessage("closeStream should be true")
321                 .that(closeStream)
322                 .isTrue();
323     }
324 
325     /**
326      * We keep this test for 100% coverage. Until #12873.
327      */
328     @Test
329     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
330         final OutputStream infoStream = new ByteArrayOutputStream();
331         final SarifLogger logger = new SarifLogger(infoStream,
332                 AutomaticBean.OutputStreamOptions.NONE);
333         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
334 
335         assertWithMessage("closeStream should be false")
336                 .that(closeStream)
337                 .isFalse();
338     }
339 
340     @Test
341     public void testNullOutputStreamOptions() {
342         try {
343             final SarifLogger logger = new SarifLogger(outStream, (OutputStreamOptions) null);
344             // assert required to calm down eclipse's 'The allocated object is never used' violation
345             assertWithMessage("Null instance")
346                 .that(logger)
347                 .isNotNull();
348             assertWithMessage("Exception was expected").fail();
349         }
350         catch (IllegalArgumentException | IOException exception) {
351             assertWithMessage("Invalid error message")
352                 .that(exception.getMessage())
353                 .isEqualTo("Parameter outputStreamOptions can not be null");
354         }
355     }
356 
357     @Test
358     public void testCloseStream() throws Exception {
359         final String inputFile = "InputSarifLoggerEmpty.java";
360         final String expectedReportFile = "ExpectedSarifLoggerEmpty.sarif";
361         final SarifLogger logger = new SarifLogger(outStream,
362                 OutputStreamOptions.CLOSE);
363 
364         verifyWithInlineConfigParserAndLogger(
365                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
366 
367         assertWithMessage("Invalid close count")
368             .that(outStream.getCloseCount())
369             .isEqualTo(1);
370     }
371 
372     @Test
373     public void testNoCloseStream() throws IOException {
374         final SarifLogger logger = new SarifLogger(outStream,
375                 OutputStreamOptions.NONE);
376         logger.auditStarted(null);
377         logger.auditFinished(null);
378 
379         assertWithMessage("Invalid close count")
380             .that(outStream.getCloseCount())
381             .isEqualTo(0);
382         assertWithMessage("Invalid flush count")
383             .that(outStream.getFlushCount())
384             .isEqualTo(1);
385 
386         outStream.close();
387         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
388     }
389 
390     @Test
391     public void testFinishLocalSetup() throws IOException {
392         final SarifLogger logger = new SarifLogger(outStream,
393                 OutputStreamOptions.CLOSE);
394         logger.finishLocalSetup();
395         logger.auditStarted(null);
396         logger.auditFinished(null);
397         assertWithMessage("instance should not be null")
398             .that(logger)
399             .isNotNull();
400     }
401 
402     @Test
403     public void testReadResourceWithInvalidName() {
404         try {
405             SarifLogger.readResource("random");
406             assertWithMessage("Exception expected").fail();
407         }
408         catch (IOException exception) {
409             assertWithMessage("Exception message must match")
410                 .that(exception.getMessage())
411                 .isEqualTo("Cannot find the resource random");
412         }
413     }
414 
415     @Test
416     public void testMultipleRules() throws Exception {
417         final String inputFile = "InputSarifLoggerMultipleRules.java";
418         final String expectedReportFile = "ExpectedSarifLoggerMultipleRules.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 testModuleIdSuffix() throws Exception {
428         final String inputFile = "InputSarifLoggerModuleId.java";
429         final String expectedReportFile = "ExpectedSarifLoggerModuleId.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 testMultipleMessageStrings() throws Exception {
439         final String inputFile = "InputSarifLoggerMultipleMessages.java";
440         final String expectedReportFile = "ExpectedSarifLoggerMultipleMessages.sarif";
441         final SarifLogger logger = new SarifLogger(outStream,
442                 OutputStreamOptions.CLOSE);
443 
444         verifyWithInlineConfigParserAndLogger(
445                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
446     }
447 
448     @Test
449     public void testEscapedMessageText() throws Exception {
450         final String inputFile = "InputSarifLoggerEscapedMessage.java";
451         final String expectedReportFile = "ExpectedSarifLoggerEscapedMessage.sarif";
452         final SarifLogger logger = new SarifLogger(outStream,
453                 OutputStreamOptions.CLOSE);
454 
455         verifyWithInlineConfigParserAndLogger(
456                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
457     }
458 
459     /**
460      * Tests {@code ClassNotFoundException} handling in {@code getMessages} method.
461      * Uses reflection to inject fake module metadata with non-existent class
462      * to trigger the exception. Real checkstyle modules cannot be used as
463      * all are on the classpath during testing.
464      */
465     @Test
466     public void testGetMessagesWithClassNotFoundException() throws Exception {
467         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
468 
469         final ModuleDetails fakeModule = new ModuleDetails();
470         fakeModule.setName("FakeCheck");
471         fakeModule.setFullQualifiedName("com.fake.NonExistentCheck");
472         fakeModule.setDescription("Fake check for testing");
473         fakeModule.addToViolationMessages("fake.message.key");
474 
475         @SuppressWarnings("unchecked")
476         final Map<Object, ModuleDetails> ruleMetadata =
477                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
478         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
479         final Object ruleKey =
480                 TestUtil.instantiate(ruleKeyClass, "com.fake.NonExistentCheck", null);
481         ruleMetadata.put(ruleKey, fakeModule);
482 
483         logger.auditFinished(null);
484 
485         verifyContent(getPath("ExpectedSarifLoggerClassNotFoundException.sarif"), outStream);
486     }
487 
488     /**
489      * Tests {@code MissingResourceException} handling in {@code getMessages} method.
490      * Uses reflection to inject fake module metadata pointing to a test class
491      * without messages.properties to trigger the exception. Real checkstyle modules
492      * cannot be used as all have proper resource bundles during testing.
493      */
494     @Test
495     public void testGetMessagesWithMissingResourceException() throws Exception {
496         final SarifLogger logger = new SarifLogger(outStream, OutputStreamOptions.CLOSE);
497 
498         final ModuleDetails fakeModule = new ModuleDetails();
499         fakeModule.setName("SarifLoggerTest");
500         fakeModule.setFullQualifiedName("com.puppycrawl.tools.checkstyle.SarifLoggerTest");
501         fakeModule.setDescription("Test class without resource bundle");
502         fakeModule.addToViolationMessages("nonexistent.message.key");
503 
504         @SuppressWarnings("unchecked")
505         final Map<Object, ModuleDetails> ruleMetadata =
506                 TestUtil.getInternalState(logger, "ruleMetadata", Map.class);
507         final Class<?> ruleKeyClass = TestUtil.getInnerClassType(SarifLogger.class, "RuleKey");
508         final Object ruleKey =
509                 TestUtil.instantiate(ruleKeyClass,
510                         "com.puppycrawl.tools.checkstyle.SarifLoggerTest", null);
511         ruleMetadata.put(ruleKey, fakeModule);
512 
513         logger.auditFinished(null);
514 
515         verifyContent(getPath("ExpectedSarifLoggerMissingResourceException.sarif"), outStream);
516     }
517 
518     private static void verifyContent(
519             String expectedOutputFile,
520             ByteArrayOutputStream actualOutputStream) throws IOException {
521         final String expectedContent = readFile(expectedOutputFile);
522         final String actualContent =
523                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
524         assertWithMessage("sarif content should match")
525             .that(actualContent)
526             .isEqualTo(expectedContent);
527     }
528 
529     private static final class TestException extends RuntimeException {
530         @Serial
531         private static final long serialVersionUID = 1L;
532 
533         private TestException(String msg, Throwable cause) {
534             super(msg, cause);
535         }
536 
537         @Override
538         public void printStackTrace(PrintWriter printWriter) {
539             printWriter.print("stackTrace\nexample");
540         }
541     }
542 }