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