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.IOException;
25  import java.io.PrintWriter;
26  import java.io.Serial;
27  import java.util.List;
28  
29  import org.junit.jupiter.api.Test;
30  
31  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
32  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
33  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
34  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
35  import com.puppycrawl.tools.checkstyle.api.Violation;
36  import com.puppycrawl.tools.checkstyle.internal.utils.CloseAndFlushTestByteArrayOutputStream;
37  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
38  
39  /**
40   * Enter a description of class XMLLoggerTest.java.
41   */
42  // -@cs[AbbreviationAsWordInName] Test should be named as its main class.
43  public class XMLLoggerTest extends AbstractXmlTestSupport {
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/xmllogger";
56      }
57  
58      @Test
59      public void testEncode()
60              throws IOException {
61          final XMLLogger test = new XMLLogger(outStream, OutputStreamOptions.NONE);
62          assertWithMessage("should be able to create XMLLogger without issue")
63              .that(test)
64              .isNotNull();
65          final String[][] encodings = {
66              {"<", "&lt;"},
67              {">", "&gt;"},
68              {"'", "&apos;"},
69              {"\"", "&quot;"},
70              {"&", "&amp;"},
71              {"&lt;", "&amp;lt;"},
72              {"abc;", "abc;"},
73              {"&#0;", "&amp;#0;"},
74              {"&#0", "&amp;#0"},
75              {"&#X0;", "&amp;#X0;"},
76              {"\u0001", "#x1;"},
77              {"\u0080", "#x80;"},
78          };
79          for (String[] encoding : encodings) {
80              final String encoded = XMLLogger.encode(encoding[0]);
81              assertWithMessage("\"" + encoding[0] + "\"")
82                  .that(encoded)
83                  .isEqualTo(encoding[1]);
84          }
85          outStream.close();
86      }
87  
88      @Test
89      public void testIsReference()
90              throws IOException {
91          final XMLLogger test = new XMLLogger(outStream, OutputStreamOptions.NONE);
92          assertWithMessage("should be able to create XMLLogger without issue")
93              .that(test)
94              .isNotNull();
95          final String[] references = {
96              "&#0;",
97              "&#x0;",
98              "&lt;",
99              "&gt;",
100             "&apos;",
101             "&quot;",
102             "&amp;",
103         };
104         for (String reference : references) {
105             assertWithMessage("reference: " + reference)
106                     .that(XMLLogger.isReference(reference))
107                     .isTrue();
108         }
109         final String[] noReferences = {
110             "&",
111             "&;",
112             "&#;",
113             "&#a;",
114             "&#X0;",
115             "&#x;",
116             "&#xg;",
117             "ramp;",
118             "ref",
119         };
120         for (String noReference : noReferences) {
121             assertWithMessage("no reference: " + noReference)
122                     .that(XMLLogger.isReference(noReference))
123                     .isFalse();
124         }
125 
126         outStream.close();
127     }
128 
129     /**
130      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this test
131      * relies on a custom OutputStream to assert close() call behavior.
132      */
133     @Test
134     public void testCloseStream()
135             throws Exception {
136         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
137         logger.auditStarted(null);
138         logger.auditFinished(null);
139 
140         assertWithMessage("Invalid close count")
141             .that(outStream.getCloseCount())
142             .isEqualTo(1);
143 
144         verifyXml(getPath("ExpectedXMLLoggerEmpty.xml"), outStream);
145     }
146 
147     /**
148      * Cannot use verifyWithInlineConfigParserAndXmlLogger because it requires
149      * a custom stream to count close() calls.
150      *
151      * @throws Exception throws exception
152      */
153     @Test
154     public void testNoCloseStream()
155             throws Exception {
156         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.NONE);
157         logger.auditStarted(null);
158         logger.auditFinished(null);
159 
160         assertWithMessage("Invalid close count")
161             .that(outStream.getCloseCount())
162             .isEqualTo(0);
163 
164         outStream.close();
165         verifyXml(getPath("ExpectedXMLLoggerEmpty.xml"), outStream);
166     }
167 
168     @Test
169         public void testFileStarted() throws Exception {
170         verifyWithInlineConfigParserAndXmlLogger(
171             "InputXMLLoggerFileStarted.java",
172             "ExpectedXMLLoggerFileStarted.xml");
173     }
174 
175     @Test
176     public void testFileFinished()
177             throws Exception {
178         verifyWithInlineConfigParserAndXmlLogger(
179             "InputXMLLoggerFileFinished.java",
180             "ExpectedXMLLoggerFileFinished.xml");
181     }
182 
183     @Test
184     public void testAddError() throws Exception {
185         verifyWithInlineConfigParserAndXmlLogger("InputXMLLoggerAddError.java",
186                 "ExpectedXMLLoggerAddError.xml");
187     }
188 
189     /**
190      * This test cannot use the standard input/expected XML approach
191      * because it requires an AuditEvent with a null fileName.
192      */
193     @Test
194     public void testAddErrorWithNullFileName() throws Exception {
195         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
196         logger.auditStarted(null);
197         final Violation violation =
198                 new Violation(1, 1,
199                         "messages.properties", "key", null, SeverityLevel.ERROR, null,
200                         getClass(), null);
201         final AuditEvent ev = new AuditEvent(this, null, violation);
202         logger.addError(ev);
203         logger.auditFinished(null);
204         verifyXml(getPath("ExpectedXMLLoggerErrorNullFileName.xml"), outStream,
205                 violation.getViolation());
206     }
207 
208     @Test
209     public void testAddErrorModuleId() throws Exception {
210         final String inputFile = "InputXMLLoggerErrorModuleId.java";
211         final String expectedXmlReport = "ExpectedXMLLoggerErrorModuleId.xml";
212         verifyWithInlineConfigParserAndXmlLogger(inputFile, expectedXmlReport);
213     }
214 
215     @Test
216     public void testAddErrorEmptyModuleId() throws Exception {
217         final String inputFile = "InputXMLLoggerErrorEmptyModuleId.java";
218         final String expectedXmlReport = "ExpectedXMLLoggerErrorEmptyModuleId.xml";
219         verifyWithInlineConfigParserAndXmlLogger(inputFile, expectedXmlReport);
220     }
221 
222     @Test
223     public void testAddErrorWithAndWithoutModuleId() throws Exception {
224         final String inputFile = "InputXMLLoggerErrorWithAndWithoutModuleId.java";
225         final String expectedXmlReport = "ExpectedXMLLoggerErrorWithAndWithoutModuleId.xml";
226         verifyWithInlineConfigParserAndXmlLogger(inputFile, expectedXmlReport);
227     }
228 
229     @Test
230     public void testAddErrorWithEncodedMessage() throws Exception {
231         final String inputFileWithConfig = "InputXMLLoggerEncodedMessage.java";
232         final String expectedXmlReport = "ExpectedXMLLoggerEncodedMessage.xml";
233         verifyWithInlineConfigParserAndXmlLogger(inputFileWithConfig, expectedXmlReport);
234     }
235 
236     @Test
237     public void testAddErrorOnZeroColumns() throws Exception {
238         verifyWithInlineConfigParserAndXmlLogger("InputXMLLoggerErrorOnZeroColumn.java",
239                 "ExpectedXMLLoggerErrorZeroColumn.xml");
240     }
241 
242     @Test
243     public void testAddIgnored() throws Exception {
244         final String inputFile = "InputXMLLoggerIgnored.java";
245         final String expectedXmlReport = "ExpectedXMLLoggerIgnored.xml";
246         verifyWithInlineConfigParserAndXmlLogger(inputFile, expectedXmlReport);
247     }
248 
249     @Test
250     public void testAddException()
251             throws Exception {
252         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
253         logger.auditStarted(null);
254         final Violation violation =
255             new Violation(1, 1,
256                 "messages.properties", null, null, null, getClass(), null);
257         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
258         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
259         logger.auditFinished(null);
260         verifyXml(getPath("ExpectedXMLLoggerException.xml"), outStream);
261         assertWithMessage("Invalid close count")
262             .that(outStream.getCloseCount())
263             .isEqualTo(1);
264     }
265 
266     /**
267      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this test
268      * requires an AuditEvent with a null file name and a custom OutputStream
269      * to verify close() behavior.
270      */
271     @Test
272     public void testAddExceptionWithNullFileName()
273             throws Exception {
274         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
275         logger.auditStarted(null);
276         final Violation violation =
277                 new Violation(1, 1,
278                         "messages.properties", null, null, null, getClass(), null);
279         final AuditEvent ev = new AuditEvent(this, null, violation);
280         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
281         logger.auditFinished(null);
282         verifyXml(getPath("ExpectedXMLLoggerExceptionNullFileName.xml"), outStream);
283         assertWithMessage("Invalid close count")
284             .that(outStream.getCloseCount())
285             .isEqualTo(1);
286     }
287 
288     @Test
289     public void testAddExceptionAfterFileStarted()
290             throws Exception {
291         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
292         logger.auditStarted(null);
293 
294         final AuditEvent fileStartedEvent = new AuditEvent(this, "Test.java");
295         logger.fileStarted(fileStartedEvent);
296 
297         final Violation violation =
298                 new Violation(1, 1,
299                         "messages.properties", null, null, null, getClass(), null);
300         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
301         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
302 
303         logger.fileFinished(ev);
304         logger.auditFinished(null);
305         verifyXml(getPath("ExpectedXMLLoggerException2.xml"), outStream);
306         assertWithMessage("Invalid close count")
307             .that(outStream.getCloseCount())
308             .isEqualTo(1);
309     }
310 
311     @Test
312     public void testAddExceptionBeforeFileFinished()
313             throws Exception {
314         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
315         logger.auditStarted(null);
316         final Violation violation =
317                 new Violation(1, 1,
318                         "messages.properties", null, null, null, getClass(), null);
319         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
320         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
321         final AuditEvent fileFinishedEvent = new AuditEvent(this, "Test.java");
322         logger.fileFinished(fileFinishedEvent);
323         logger.auditFinished(null);
324         verifyXml(getPath("ExpectedXMLLoggerException3.xml"), outStream);
325         assertWithMessage("Invalid close count")
326             .that(outStream.getCloseCount())
327             .isEqualTo(1);
328     }
329 
330     @Test
331     public void testAddExceptionBetweenFileStartedAndFinished()
332             throws Exception {
333         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
334         logger.auditStarted(null);
335         final Violation violation =
336                 new Violation(1, 1,
337                         "messages.properties", null, null, null, getClass(), null);
338         final AuditEvent fileStartedEvent = new AuditEvent(this, "Test.java");
339         logger.fileStarted(fileStartedEvent);
340         final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
341         logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
342         final AuditEvent fileFinishedEvent = new AuditEvent(this, "Test.java");
343         logger.fileFinished(fileFinishedEvent);
344         logger.auditFinished(null);
345         verifyXml(getPath("ExpectedXMLLoggerException2.xml"), outStream);
346         assertWithMessage("Invalid close count")
347             .that(outStream.getCloseCount())
348             .isEqualTo(1);
349     }
350 
351     @Test
352     public void testAuditFinishedWithoutFileFinished() throws Exception {
353         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
354         logger.auditStarted(null);
355         final AuditEvent fileStartedEvent = new AuditEvent(this, "Test.java");
356         logger.fileStarted(fileStartedEvent);
357 
358         final Violation violation =
359                 new Violation(1, 1,
360                         "messages.properties", "key", null, SeverityLevel.ERROR, null,
361                         getClass(), null);
362         final AuditEvent errorEvent = new AuditEvent(this, "Test.java", violation);
363         logger.addError(errorEvent);
364 
365         logger.fileFinished(errorEvent);
366         logger.auditFinished(null);
367         verifyXml(getPath("ExpectedXMLLoggerError.xml"), outStream, violation.getViolation());
368     }
369 
370     @Test
371     public void testFileOpenTag()
372             throws Exception {
373         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
374         logger.auditStarted(null);
375         final AuditEvent ev = new AuditEvent(this, "Test&.java");
376         logger.fileFinished(ev);
377         logger.auditFinished(null);
378         verifyXml(getPath("ExpectedXMLLoggerSpecialName.xml"),
379                 outStream, "<file name=" + "Test&amp;.java" + ">");
380 
381     }
382 
383     @Test
384     public void testVerifyLogger()
385             throws Exception {
386         final String inputFileWithConfig = "InputXMLLogger.java";
387         final String xmlReport = "ExpectedXMLLoggerWithChecker.xml";
388         verifyWithInlineConfigParserAndXmlLogger(inputFileWithConfig, xmlReport);
389     }
390 
391     @Test
392     public void testVerifyLoggerWithMultipleInput()
393             throws Exception {
394         final String inputFileWithConfig = "InputXMLLogger.java";
395         final String expectedXmlReport = "ExpectedXMLLoggerDuplicatedFile.xml";
396         verifyWithInlineConfigParserAndXmlLogger(
397                 inputFileWithConfig,
398                 expectedXmlReport,
399                 List.of(inputFileWithConfig, inputFileWithConfig));
400     }
401 
402     /**
403      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this is a
404      * pure unit test that validates constructor error handling and exception
405      * messaging, which cannot be exercised through Checker execution.
406      */
407     @Test
408     public void testNullOutputStreamOptions() {
409         try {
410             final XMLLogger logger = new XMLLogger(outStream,
411                     (OutputStreamOptions) null);
412             // assert required to calm down eclipse's 'The allocated object is never used' violation
413             assertWithMessage("Null instance")
414                 .that(logger)
415                 .isNotNull();
416             assertWithMessage("Exception was expected").fail();
417         }
418         catch (IllegalArgumentException exception) {
419             assertWithMessage("Invalid error message")
420                 .that(exception.getMessage())
421                 .isEqualTo("Parameter outputStreamOptions can not be null");
422         }
423     }
424 
425     /**
426      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this test
427      * directly exercises XMLLogger lifecycle behavior (finishLocalSetup) and
428      * does not depend on Checker execution or XML output.
429      */
430     @Test
431     public void testFinishLocalSetup() {
432         final XMLLogger logger = new XMLLogger(outStream, OutputStreamOptions.CLOSE);
433         logger.finishLocalSetup();
434         logger.auditStarted(null);
435         logger.auditFinished(null);
436         assertWithMessage("instance should not be null")
437             .that(logger)
438             .isNotNull();
439     }
440 
441     /**
442      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this test
443      * verifies constructor wiring and internal state (closeStream flag),
444      * which are not exposed through the inline verifier.
445      * We keep this test for 100% coverage. Until #12873.
446      */
447     @Test
448     public void testCtorWithTwoParametersCloseStreamOptions() {
449         final XMLLogger logger = new XMLLogger(outStream, AutomaticBean.OutputStreamOptions.CLOSE);
450         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
451 
452         assertWithMessage("closeStream should be true")
453                 .that(closeStream)
454                 .isTrue();
455     }
456 
457     /**
458      * Cannot use verifyWithInlineConfigParserAndXmlLogger because this test
459      * verifies constructor wiring and internal state (closeStream flag),
460      * which are not exposed through the inline verifier.
461      * We keep this test for 100% coverage. Until #12873.
462      */
463     @Test
464     public void testCtorWithTwoParametersNoneStreamOptions() {
465         final XMLLogger logger = new XMLLogger(outStream, AutomaticBean.OutputStreamOptions.NONE);
466         final boolean closeStream = TestUtil.getInternalState(logger, "closeStream", Boolean.class);
467 
468         assertWithMessage("closeStream should be false")
469                 .that(closeStream)
470                 .isFalse();
471     }
472 
473     private static final class TestException extends RuntimeException {
474         @Serial
475         private static final long serialVersionUID = 1L;
476 
477         private TestException(String msg, Throwable cause) {
478             super(msg, cause);
479         }
480 
481         @Override
482         public void printStackTrace(PrintWriter printWriter) {
483             printWriter.print("stackTrace\r\nexample");
484         }
485 
486     }
487 
488 }