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.nio.charset.StandardCharsets;
29  
30  import org.junit.jupiter.api.Test;
31  
32  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
33  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
34  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
35  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
36  import com.puppycrawl.tools.checkstyle.api.Violation;
37  import com.puppycrawl.tools.checkstyle.internal.utils.CloseAndFlushTestByteArrayOutputStream;
38  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
39  
40  public class SarifLoggerTest extends AbstractModuleTestSupport {
41  
42      /**
43       * Output stream to hold the test results. The IntelliJ IDEA issues the AutoCloseableResource
44       * warning here, so it needs to be suppressed. The {@code ByteArrayOutputStream} does not hold
45       * any resources that need to be released.
46       */
47      private final CloseAndFlushTestByteArrayOutputStream outStream =
48          new CloseAndFlushTestByteArrayOutputStream();
49  
50      @Override
51      protected String getPackageLocation() {
52          return "com/puppycrawl/tools/checkstyle/sariflogger";
53      }
54  
55      /**
56       * Executes the common logging steps for SARIF logging tests.
57       * This method mimic of natural execution at is done by Checker.
58       *
59       * <p>
60       * It starts file auditing, adds an error, and completes the audit process.
61       * </p>
62       *
63       * @param instance  the current instance of {@code SarifLoggerTest}, used for context
64       * @param logger    the {@code SarifLogger} instance to log errors
65       * @param fileName  the name of the file being audited
66       * @param violation the {@code Violation} object containing error details
67       */
68      public final void executeLogger(
69          SarifLoggerTest instance, SarifLogger logger, String fileName, Violation violation) {
70          final AuditEvent event = new AuditEvent(this, "Test.java", violation);
71          logger.fileStarted(event);
72          logger.addError(event);
73          logger.fileFinished(event);
74          logger.auditFinished(null);
75      }
76  
77      @Test
78      public void testEscape() {
79          final String[][] encodings = {
80              {"\"", "\\\""},
81              {"\\", "\\\\"},
82              {"\b", "\\b"},
83              {"\f", "\\f"},
84              {"\n", "\\n"},
85              {"\r", "\\r"},
86              {"\t", "\\t"},
87              {"/", "\\/"},
88              {"\u0010", "\\u0010"},
89              {"\u001E", "\\u001E"},
90              {"\u001F", "\\u001F"},
91              {" ", " "},
92              {"bar1234", "bar1234"},
93          };
94          for (String[] encoding : encodings) {
95              final String encoded = SarifLogger.escape(encoding[0]);
96              assertWithMessage("\"" + encoding[0] + "\"")
97                  .that(encoded)
98                  .isEqualTo(encoding[1]);
99          }
100     }
101 
102     @Test
103     public void testSingleError() throws Exception {
104         final String inputFile = "InputSarifLoggerSingleError.java";
105         final String expectedReportFile = "ExpectedSarifLoggerSingleError.sarif";
106         final SarifLogger logger = new SarifLogger(outStream,
107                 OutputStreamOptions.CLOSE);
108 
109         verifyWithInlineConfigParserAndLogger(
110                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
111     }
112 
113     @Test
114     public void testAddErrorAtColumn1() throws Exception {
115         final SarifLogger logger = new SarifLogger(outStream,
116                 OutputStreamOptions.CLOSE);
117         final String inputFile = "InputSarifLoggerSingleErrorColumn1.java";
118         final String expectedOutput = "ExpectedSarifLoggerSingleErrorColumn1.sarif";
119         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
120                 getPath(expectedOutput), logger, outStream);
121     }
122 
123     @Test
124     public void testAddErrorAtColumn0() throws Exception {
125         final String inputFile = "InputSarifLoggerErrorColumn0.java";
126         final String expectedReportFile = "ExpectedSarifLoggerSingleErrorColumn0.sarif";
127         final SarifLogger logger = new SarifLogger(outStream,
128                 OutputStreamOptions.CLOSE);
129 
130         verifyWithInlineConfigParserAndLogger(
131                 getPath(inputFile), getPath(expectedReportFile), logger, outStream);
132     }
133 
134     @Test
135     public void testAddErrorWithWarningLevel() throws Exception {
136         final SarifLogger logger = new SarifLogger(outStream,
137                 OutputStreamOptions.CLOSE);
138         final String inputFile = "InputSarifLoggerSingleWarning.java";
139         final String expectedOutput = "ExpectedSarifLoggerSingleWarning.sarif";
140         verifyWithInlineConfigParserAndLogger(getPath(inputFile),
141                 getPath(expectedOutput), logger, outStream);
142     }
143 
144     @Test
145     public void testAddErrors() throws IOException {
146         final SarifLogger logger = new SarifLogger(outStream,
147                 OutputStreamOptions.CLOSE);
148         logger.auditStarted(null);
149         final Violation violation =
150                 new Violation(1, 1,
151                         "messages.properties", "ruleId", null, SeverityLevel.INFO, null,
152                         getClass(), "found an error");
153         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
154         final Violation violation2 =
155                 new Violation(1, 1,
156                         "messages.properties", "ruleId2", null, SeverityLevel.IGNORE, null,
157                         getClass(), "found another error");
158         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
159         logger.fileStarted(ev);
160         logger.addError(ev);
161         logger.fileFinished(ev);
162         logger.fileStarted(ev2);
163         logger.addError(ev2);
164         logger.fileFinished(ev2);
165         logger.auditFinished(null);
166         verifyContent(getPath("ExpectedSarifLoggerDoubleError.sarif"), outStream);
167     }
168 
169     @Test
170     public void testAddException() throws IOException {
171         final SarifLogger logger = new SarifLogger(outStream,
172                 OutputStreamOptions.CLOSE);
173         logger.auditStarted(null);
174         final Violation message =
175                 new Violation(1, 1,
176                         "messages.properties", "null", null, null,
177                         getClass(), "found an error");
178         final AuditEvent ev = new AuditEvent(this, null, message);
179         logger.fileStarted(ev);
180         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
181         logger.fileFinished(ev);
182         logger.auditFinished(null);
183         verifyContent(getPath("ExpectedSarifLoggerSingleException.sarif"), outStream);
184     }
185 
186     @Test
187     public void testAddExceptions() throws IOException {
188         final SarifLogger logger = new SarifLogger(outStream,
189                 OutputStreamOptions.CLOSE);
190         logger.auditStarted(null);
191         final Violation violation =
192                 new Violation(1, 1,
193                         "messages.properties", "null", null, null,
194                         getClass(), "found an error");
195         final AuditEvent ev = new AuditEvent(this, null, violation);
196         final Violation violation2 =
197                 new Violation(1, 1,
198                         "messages.properties", "null", null, null,
199                         getClass(), "found an error");
200         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
201         logger.fileStarted(ev);
202         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
203         logger.fileFinished(ev);
204         logger.fileStarted(ev2);
205         logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
206         logger.fileFinished(ev);
207         logger.auditFinished(null);
208         verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
209     }
210 
211     @Test
212     public void testLineOnly() throws IOException {
213         final SarifLogger logger = new SarifLogger(outStream,
214             OutputStreamOptions.CLOSE);
215         logger.auditStarted(null);
216         final Violation violation =
217             new Violation(1, 0,
218                 "messages.properties", "ruleId", null, null,
219                 getClass(), "found an error");
220         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
221         logger.fileStarted(ev);
222         logger.addError(ev);
223         logger.fileFinished(ev);
224         logger.auditFinished(null);
225         verifyContent(getPath("ExpectedSarifLoggerLineOnly.sarif"), outStream);
226     }
227 
228     @Test
229     public void testEmpty() throws IOException {
230         final SarifLogger logger = new SarifLogger(outStream,
231                 OutputStreamOptions.CLOSE);
232         logger.auditStarted(null);
233         final Violation violation =
234                 new Violation(1, 1,
235                         "messages.properties", "null", null, null,
236                         getClass(), "found an error");
237         final AuditEvent ev = new AuditEvent(this, null, violation);
238         logger.fileStarted(ev);
239         logger.fileFinished(ev);
240         logger.auditFinished(null);
241         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
242     }
243 
244     @Test
245     public void testAddErrorWithSpaceInPath() throws IOException {
246         final SarifLogger logger = new SarifLogger(outStream,
247                 OutputStreamOptions.CLOSE);
248         logger.auditStarted(null);
249         final Violation violation =
250                 new Violation(1, 1,
251                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
252                         getClass(), "found an error");
253         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test 2.java", violation);
254         logger.fileStarted(ev);
255         logger.addError(ev);
256         logger.fileFinished(ev);
257         logger.auditFinished(null);
258         verifyContent(getPath("ExpectedSarifLoggerSpaceInPath.sarif"), outStream);
259     }
260 
261     @Test
262     public void testAddErrorWithAbsoluteLinuxPath() throws IOException {
263         final SarifLogger logger = new SarifLogger(outStream,
264                 OutputStreamOptions.CLOSE);
265         logger.auditStarted(null);
266         final Violation violation =
267                 new Violation(1, 1,
268                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
269                         getClass(), "found an error");
270         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test.java", violation);
271         logger.fileStarted(ev);
272         logger.addError(ev);
273         logger.fileFinished(ev);
274         logger.auditFinished(null);
275         verifyContent(getPath("ExpectedSarifLoggerAbsoluteLinuxPath.sarif"), outStream);
276     }
277 
278     @Test
279     public void testAddErrorWithRelativeLinuxPath() 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, "./Test.java", violation);
288         logger.fileStarted(ev);
289         logger.addError(ev);
290         logger.fileFinished(ev);
291         logger.auditFinished(null);
292         verifyContent(getPath("ExpectedSarifLoggerRelativeLinuxPath.sarif"), outStream);
293     }
294 
295     @Test
296     public void testAddErrorWithAbsoluteWindowsPath() throws IOException {
297         final SarifLogger logger = new SarifLogger(outStream,
298                 OutputStreamOptions.CLOSE);
299         logger.auditStarted(null);
300         final Violation violation =
301                 new Violation(1, 1,
302                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
303                         getClass(), "found an error");
304         final AuditEvent ev =
305                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
306         logger.fileStarted(ev);
307         logger.addError(ev);
308         logger.fileFinished(ev);
309         logger.auditFinished(null);
310         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
311     }
312 
313     @Test
314     public void testAddErrorWithRelativeWindowsPath() throws IOException {
315         final SarifLogger logger = new SarifLogger(outStream,
316                 OutputStreamOptions.CLOSE);
317         logger.auditStarted(null);
318         final Violation violation =
319                 new Violation(1, 1,
320                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
321                         getClass(), "found an error");
322         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
323         logger.fileStarted(ev);
324         logger.addError(ev);
325         logger.fileFinished(ev);
326         logger.auditFinished(null);
327         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
328     }
329 
330     /**
331      * We keep this test for 100% coverage. Until #12873.
332      */
333     @Test
334     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
335         final OutputStream infoStream = new ByteArrayOutputStream();
336         final SarifLogger logger = new SarifLogger(infoStream,
337                 AutomaticBean.OutputStreamOptions.CLOSE);
338         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream");
339 
340         assertWithMessage("closeStream should be true")
341                 .that(closeStream)
342                 .isTrue();
343     }
344 
345     /**
346      * We keep this test for 100% coverage. Until #12873.
347      */
348     @Test
349     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
350         final OutputStream infoStream = new ByteArrayOutputStream();
351         final SarifLogger logger = new SarifLogger(infoStream,
352                 AutomaticBean.OutputStreamOptions.NONE);
353         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream");
354 
355         assertWithMessage("closeStream should be false")
356                 .that(closeStream)
357                 .isFalse();
358     }
359 
360     @Test
361     public void testNullOutputStreamOptions() {
362         try {
363             final SarifLogger logger = new SarifLogger(outStream, (OutputStreamOptions) null);
364             // assert required to calm down eclipse's 'The allocated object is never used' violation
365             assertWithMessage("Null instance")
366                 .that(logger)
367                 .isNotNull();
368             assertWithMessage("Exception was expected").fail();
369         }
370         catch (IllegalArgumentException | IOException exception) {
371             assertWithMessage("Invalid error message")
372                 .that(exception.getMessage())
373                 .isEqualTo("Parameter outputStreamOptions can not be null");
374         }
375     }
376 
377     @Test
378     public void testCloseStream() throws IOException {
379         final SarifLogger logger = new SarifLogger(outStream,
380                 OutputStreamOptions.CLOSE);
381         logger.auditStarted(null);
382         logger.auditFinished(null);
383 
384         assertWithMessage("Invalid close count")
385             .that(outStream.getCloseCount())
386             .isEqualTo(1);
387 
388         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
389     }
390 
391     @Test
392     public void testNoCloseStream() throws IOException {
393         final SarifLogger logger = new SarifLogger(outStream,
394                 OutputStreamOptions.NONE);
395         logger.auditStarted(null);
396         logger.auditFinished(null);
397 
398         assertWithMessage("Invalid close count")
399             .that(outStream.getCloseCount())
400             .isEqualTo(0);
401         assertWithMessage("Invalid flush count")
402             .that(outStream.getFlushCount())
403             .isEqualTo(1);
404 
405         outStream.close();
406         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
407     }
408 
409     @Test
410     public void testFinishLocalSetup() throws IOException {
411         final SarifLogger logger = new SarifLogger(outStream,
412                 OutputStreamOptions.CLOSE);
413         logger.finishLocalSetup();
414         logger.auditStarted(null);
415         logger.auditFinished(null);
416         assertWithMessage("instance should not be null")
417             .that(logger)
418             .isNotNull();
419     }
420 
421     @Test
422     public void testReadResourceWithInvalidName() {
423         try {
424             SarifLogger.readResource("random");
425             assertWithMessage("Exception expected").fail();
426         }
427         catch (IOException exception) {
428             assertWithMessage("Exception message must match")
429                 .that(exception.getMessage())
430                 .isEqualTo("Cannot find the resource random");
431         }
432     }
433 
434     private static void verifyContent(
435             String expectedOutputFile,
436             ByteArrayOutputStream actualOutputStream) throws IOException {
437         final String expectedContent = readFile(expectedOutputFile);
438         final String actualContent =
439                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
440         assertWithMessage("sarif content should match")
441             .that(actualContent)
442             .isEqualTo(expectedContent);
443     }
444 
445     private static final class TestException extends RuntimeException {
446 
447         private static final long serialVersionUID = 1L;
448 
449         private TestException(String msg, Throwable cause) {
450             super(msg, cause);
451         }
452 
453         @Override
454         public void printStackTrace(PrintWriter printWriter) {
455             printWriter.print("stackTrace\nexample");
456         }
457     }
458 }