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 IOException {
115         final SarifLogger logger = new SarifLogger(outStream,
116                 OutputStreamOptions.CLOSE);
117         logger.auditStarted(null);
118         final Violation violation =
119                 new Violation(1, 1,
120                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
121                         getClass(), "found an error\ntry again");
122         executeLogger(this, logger, "Test.java", violation);
123         verifyContent(getPath("ExpectedSarifLoggerSingleErrorColumn1.sarif"), 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 IOException {
139         final SarifLogger logger = new SarifLogger(outStream,
140                 OutputStreamOptions.CLOSE);
141         logger.auditStarted(null);
142         final Violation violation =
143                 new Violation(1, 1,
144                         "messages.properties", "ruleId", null, SeverityLevel.WARNING, null,
145                         getClass(), "found an error");
146         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
147         logger.fileStarted(ev);
148         logger.addError(ev);
149         logger.fileFinished(ev);
150         logger.auditFinished(null);
151         verifyContent(getPath("ExpectedSarifLoggerSingleWarning.sarif"), outStream);
152     }
153 
154     @Test
155     public void testAddErrors() throws IOException {
156         final SarifLogger logger = new SarifLogger(outStream,
157                 OutputStreamOptions.CLOSE);
158         logger.auditStarted(null);
159         final Violation violation =
160                 new Violation(1, 1,
161                         "messages.properties", "ruleId", null, SeverityLevel.INFO, null,
162                         getClass(), "found an error");
163         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
164         final Violation violation2 =
165                 new Violation(1, 1,
166                         "messages.properties", "ruleId2", null, SeverityLevel.IGNORE, null,
167                         getClass(), "found another error");
168         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
169         logger.fileStarted(ev);
170         logger.addError(ev);
171         logger.fileFinished(ev);
172         logger.fileStarted(ev2);
173         logger.addError(ev2);
174         logger.fileFinished(ev2);
175         logger.auditFinished(null);
176         verifyContent(getPath("ExpectedSarifLoggerDoubleError.sarif"), outStream);
177     }
178 
179     @Test
180     public void testAddException() throws IOException {
181         final SarifLogger logger = new SarifLogger(outStream,
182                 OutputStreamOptions.CLOSE);
183         logger.auditStarted(null);
184         final Violation message =
185                 new Violation(1, 1,
186                         "messages.properties", "null", null, null,
187                         getClass(), "found an error");
188         final AuditEvent ev = new AuditEvent(this, null, message);
189         logger.fileStarted(ev);
190         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
191         logger.fileFinished(ev);
192         logger.auditFinished(null);
193         verifyContent(getPath("ExpectedSarifLoggerSingleException.sarif"), outStream);
194     }
195 
196     @Test
197     public void testAddExceptions() throws IOException {
198         final SarifLogger logger = new SarifLogger(outStream,
199                 OutputStreamOptions.CLOSE);
200         logger.auditStarted(null);
201         final Violation violation =
202                 new Violation(1, 1,
203                         "messages.properties", "null", null, null,
204                         getClass(), "found an error");
205         final AuditEvent ev = new AuditEvent(this, null, violation);
206         final Violation violation2 =
207                 new Violation(1, 1,
208                         "messages.properties", "null", null, null,
209                         getClass(), "found an error");
210         final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
211         logger.fileStarted(ev);
212         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
213         logger.fileFinished(ev);
214         logger.fileStarted(ev2);
215         logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
216         logger.fileFinished(ev);
217         logger.auditFinished(null);
218         verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
219     }
220 
221     @Test
222     public void testLineOnly() throws IOException {
223         final SarifLogger logger = new SarifLogger(outStream,
224             OutputStreamOptions.CLOSE);
225         logger.auditStarted(null);
226         final Violation violation =
227             new Violation(1, 0,
228                 "messages.properties", "ruleId", null, null,
229                 getClass(), "found an error");
230         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
231         logger.fileStarted(ev);
232         logger.addError(ev);
233         logger.fileFinished(ev);
234         logger.auditFinished(null);
235         verifyContent(getPath("ExpectedSarifLoggerLineOnly.sarif"), outStream);
236     }
237 
238     @Test
239     public void testEmpty() throws IOException {
240         final SarifLogger logger = new SarifLogger(outStream,
241                 OutputStreamOptions.CLOSE);
242         logger.auditStarted(null);
243         final Violation violation =
244                 new Violation(1, 1,
245                         "messages.properties", "null", null, null,
246                         getClass(), "found an error");
247         final AuditEvent ev = new AuditEvent(this, null, violation);
248         logger.fileStarted(ev);
249         logger.fileFinished(ev);
250         logger.auditFinished(null);
251         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
252     }
253 
254     @Test
255     public void testAddErrorWithSpaceInPath() throws IOException {
256         final SarifLogger logger = new SarifLogger(outStream,
257                 OutputStreamOptions.CLOSE);
258         logger.auditStarted(null);
259         final Violation violation =
260                 new Violation(1, 1,
261                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
262                         getClass(), "found an error");
263         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test 2.java", violation);
264         logger.fileStarted(ev);
265         logger.addError(ev);
266         logger.fileFinished(ev);
267         logger.auditFinished(null);
268         verifyContent(getPath("ExpectedSarifLoggerSpaceInPath.sarif"), outStream);
269     }
270 
271     @Test
272     public void testAddErrorWithAbsoluteLinuxPath() throws IOException {
273         final SarifLogger logger = new SarifLogger(outStream,
274                 OutputStreamOptions.CLOSE);
275         logger.auditStarted(null);
276         final Violation violation =
277                 new Violation(1, 1,
278                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
279                         getClass(), "found an error");
280         final AuditEvent ev = new AuditEvent(this, "/home/someuser/Code/Test.java", violation);
281         logger.fileStarted(ev);
282         logger.addError(ev);
283         logger.fileFinished(ev);
284         logger.auditFinished(null);
285         verifyContent(getPath("ExpectedSarifLoggerAbsoluteLinuxPath.sarif"), outStream);
286     }
287 
288     @Test
289     public void testAddErrorWithRelativeLinuxPath() throws IOException {
290         final SarifLogger logger = new SarifLogger(outStream,
291                 OutputStreamOptions.CLOSE);
292         logger.auditStarted(null);
293         final Violation violation =
294                 new Violation(1, 1,
295                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
296                         getClass(), "found an error");
297         final AuditEvent ev = new AuditEvent(this, "./Test.java", violation);
298         logger.fileStarted(ev);
299         logger.addError(ev);
300         logger.fileFinished(ev);
301         logger.auditFinished(null);
302         verifyContent(getPath("ExpectedSarifLoggerRelativeLinuxPath.sarif"), outStream);
303     }
304 
305     @Test
306     public void testAddErrorWithAbsoluteWindowsPath() throws IOException {
307         final SarifLogger logger = new SarifLogger(outStream,
308                 OutputStreamOptions.CLOSE);
309         logger.auditStarted(null);
310         final Violation violation =
311                 new Violation(1, 1,
312                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
313                         getClass(), "found an error");
314         final AuditEvent ev =
315                 new AuditEvent(this, "C:\\Users\\SomeUser\\Code\\Test.java", violation);
316         logger.fileStarted(ev);
317         logger.addError(ev);
318         logger.fileFinished(ev);
319         logger.auditFinished(null);
320         verifyContent(getPath("ExpectedSarifLoggerAbsoluteWindowsPath.sarif"), outStream);
321     }
322 
323     @Test
324     public void testAddErrorWithRelativeWindowsPath() throws IOException {
325         final SarifLogger logger = new SarifLogger(outStream,
326                 OutputStreamOptions.CLOSE);
327         logger.auditStarted(null);
328         final Violation violation =
329                 new Violation(1, 1,
330                         "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
331                         getClass(), "found an error");
332         final AuditEvent ev = new AuditEvent(this, ".\\Test.java", violation);
333         logger.fileStarted(ev);
334         logger.addError(ev);
335         logger.fileFinished(ev);
336         logger.auditFinished(null);
337         verifyContent(getPath("ExpectedSarifLoggerRelativeWindowsPath.sarif"), outStream);
338     }
339 
340     /**
341      * We keep this test for 100% coverage. Until #12873.
342      */
343     @Test
344     public void testCtorWithTwoParametersCloseStreamOptions() throws IOException {
345         final OutputStream infoStream = new ByteArrayOutputStream();
346         final SarifLogger logger = new SarifLogger(infoStream,
347                 AutomaticBean.OutputStreamOptions.CLOSE);
348         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream");
349 
350         assertWithMessage("closeStream should be true")
351                 .that(closeStream)
352                 .isTrue();
353     }
354 
355     /**
356      * We keep this test for 100% coverage. Until #12873.
357      */
358     @Test
359     public void testCtorWithTwoParametersNoneStreamOptions() throws IOException {
360         final OutputStream infoStream = new ByteArrayOutputStream();
361         final SarifLogger logger = new SarifLogger(infoStream,
362                 AutomaticBean.OutputStreamOptions.NONE);
363         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream");
364 
365         assertWithMessage("closeStream should be false")
366                 .that(closeStream)
367                 .isFalse();
368     }
369 
370     @Test
371     public void testNullOutputStreamOptions() {
372         try {
373             final SarifLogger logger = new SarifLogger(outStream, (OutputStreamOptions) null);
374             // assert required to calm down eclipse's 'The allocated object is never used' violation
375             assertWithMessage("Null instance")
376                 .that(logger)
377                 .isNotNull();
378             assertWithMessage("Exception was expected").fail();
379         }
380         catch (IllegalArgumentException | IOException exception) {
381             assertWithMessage("Invalid error message")
382                 .that(exception.getMessage())
383                 .isEqualTo("Parameter outputStreamOptions can not be null");
384         }
385     }
386 
387     @Test
388     public void testCloseStream() throws IOException {
389         final SarifLogger logger = new SarifLogger(outStream,
390                 OutputStreamOptions.CLOSE);
391         logger.auditStarted(null);
392         logger.auditFinished(null);
393 
394         assertWithMessage("Invalid close count")
395             .that(outStream.getCloseCount())
396             .isEqualTo(1);
397 
398         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
399     }
400 
401     @Test
402     public void testNoCloseStream() throws IOException {
403         final SarifLogger logger = new SarifLogger(outStream,
404                 OutputStreamOptions.NONE);
405         logger.auditStarted(null);
406         logger.auditFinished(null);
407 
408         assertWithMessage("Invalid close count")
409             .that(outStream.getCloseCount())
410             .isEqualTo(0);
411         assertWithMessage("Invalid flush count")
412             .that(outStream.getFlushCount())
413             .isEqualTo(1);
414 
415         outStream.close();
416         verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
417     }
418 
419     @Test
420     public void testFinishLocalSetup() throws IOException {
421         final SarifLogger logger = new SarifLogger(outStream,
422                 OutputStreamOptions.CLOSE);
423         logger.finishLocalSetup();
424         logger.auditStarted(null);
425         logger.auditFinished(null);
426         assertWithMessage("instance should not be null")
427             .that(logger)
428             .isNotNull();
429     }
430 
431     @Test
432     public void testReadResourceWithInvalidName() {
433         try {
434             SarifLogger.readResource("random");
435             assertWithMessage("Exception expected").fail();
436         }
437         catch (IOException exception) {
438             assertWithMessage("Exception message must match")
439                 .that(exception.getMessage())
440                 .isEqualTo("Cannot find the resource random");
441         }
442     }
443 
444     private static void verifyContent(
445             String expectedOutputFile,
446             ByteArrayOutputStream actualOutputStream) throws IOException {
447         final String expectedContent = readFile(expectedOutputFile);
448         final String actualContent =
449                 toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
450         assertWithMessage("sarif content should match")
451             .that(actualContent)
452             .isEqualTo(expectedContent);
453     }
454 
455     private static final class TestException extends RuntimeException {
456 
457         private static final long serialVersionUID = 1L;
458 
459         private TestException(String msg, Throwable cause) {
460             super(msg, cause);
461         }
462 
463         @Override
464         public void printStackTrace(PrintWriter printWriter) {
465             printWriter.print("stackTrace\nexample");
466         }
467     }
468 }