View Javadoc
1   /*
2    * Copyright (c) 2011-2025 Yegor Bugayenko
3    *
4    * All rights reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions
8    * are met: 1) Redistributions of source code must retain the above
9    * copyright notice, this list of conditions and the following
10   * disclaimer. 2) Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following
12   * disclaimer in the documentation and/or other materials provided
13   * with the distribution. 3) Neither the name of the Qulice.com nor
14   * the names of its contributors may be used to endorse or promote
15   * products derived from this software without specific prior written
16   * permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
20   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29   * OF THE POSSIBILITY OF SUCH DAMAGE.
30   */
31  package com.qulice.checkstyle;
32  
33  import com.puppycrawl.tools.checkstyle.Checker;
34  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
35  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
36  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
37  import com.puppycrawl.tools.checkstyle.api.AuditListener;
38  import java.io.File;
39  import java.nio.charset.StandardCharsets;
40  import java.util.ArrayList;
41  import java.util.LinkedList;
42  import java.util.List;
43  import java.util.Properties;
44  import java.util.stream.Stream;
45  import org.apache.commons.io.IOUtils;
46  import org.cactoos.text.Joined;
47  import org.hamcrest.MatcherAssert;
48  import org.hamcrest.Matchers;
49  import org.junit.jupiter.params.ParameterizedTest;
50  import org.junit.jupiter.params.provider.MethodSource;
51  import org.mockito.Mockito;
52  import org.mockito.invocation.InvocationOnMock;
53  import org.mockito.stubbing.Answer;
54  import org.xml.sax.InputSource;
55  
56  /**
57   * Integration test case for all checkstyle checks.
58   * @since 0.3
59   */
60  final class ChecksTest {
61  
62      /**
63       * Test checkstyle for true negative.
64       * @param dir Directory where test scripts are located.
65       * @throws Exception If something goes wrong
66       */
67      @ParameterizedTest
68      @MethodSource("checks")
69      @SuppressWarnings("PMD.JUnitAssertionsShouldIncludeMessage")
70      void testCheckstyleTruePositive(final String dir) throws Exception {
71          final AuditListener listener = Mockito.mock(AuditListener.class);
72          final Collector collector = new ChecksTest.Collector();
73          Mockito.doAnswer(collector).when(listener)
74              .addError(Mockito.any(AuditEvent.class));
75          this.check(dir, "/Invalid.java", listener);
76          final String[] violations = IOUtils.toString(
77              this.getClass().getResourceAsStream(
78                  String.format("%s/violations.txt", dir)
79              ),
80              StandardCharsets.UTF_8
81              ).split("\n");
82          for (final String line : violations) {
83              final String[] sectors = line.split(":");
84              final Integer pos = Integer.valueOf(sectors[0]);
85              final String needle = sectors[1].trim();
86              MatcherAssert.assertThat(
87                  collector.has(pos, needle),
88                  Matchers.describedAs(
89                      String.format(
90                          "Line no.%d ('%s') not reported by %s: '%s'",
91                          pos,
92                          needle,
93                          dir,
94                          collector.summary()
95                      ),
96                      Matchers.is(true)
97                  )
98              );
99          }
100         MatcherAssert.assertThat(
101             collector.eventCount(),
102             Matchers.describedAs(
103                 String.format(
104                     "Got more violations that expected for directory %s (%s)",
105                     dir, collector.summary()
106                 ),
107                 Matchers.equalTo(violations.length)
108             )
109         );
110     }
111 
112     /**
113      * Test checkstyle for true negative.
114      * @param dir Directory where test scripts are located.
115      * @throws Exception If something goes wrong
116      */
117     @ParameterizedTest
118     @MethodSource("checks")
119     void testCheckstyleTrueNegative(final String dir) throws Exception {
120         final AuditListener listener = Mockito.mock(AuditListener.class);
121         final Collector collector = new ChecksTest.Collector();
122         Mockito.doAnswer(collector).when(listener)
123             .addError(Mockito.any(AuditEvent.class));
124         this.check(dir, "/Valid.java", listener);
125         MatcherAssert.assertThat(
126             "Log should be empty for valid files",
127             collector.summary(),
128             Matchers.equalTo("")
129         );
130         Mockito.verify(listener, Mockito.times(0))
131             .addError(Mockito.any(AuditEvent.class));
132     }
133 
134     /**
135      * Check one file.
136      * @param dir Directory where test scripts are located.
137      * @param name The name of the check
138      * @param listener The listener
139      * @throws Exception If something goes wrong inside
140      */
141     private void check(
142         final String dir, final String name, final AuditListener listener
143     ) throws Exception {
144         final Checker checker = new Checker();
145         final InputSource src = new InputSource(
146             this.getClass().getResourceAsStream(
147                 String.format("%s/config.xml", dir)
148             )
149         );
150         checker.setModuleClassLoader(
151             Thread.currentThread().getContextClassLoader()
152         );
153         checker.configure(
154             ConfigurationLoader.loadConfiguration(
155                 src,
156                 new PropertiesExpander(new Properties()),
157                 ConfigurationLoader.IgnoredModulesOptions.OMIT
158             )
159         );
160         final List<File> files = new ArrayList<>(0);
161         files.add(
162             new File(
163                 this.getClass().getResource(
164                     String.format("%s%s", dir, name)
165                 ).getFile()
166             )
167         );
168         checker.addListener(listener);
169         checker.process(files);
170         checker.destroy();
171     }
172 
173     /**
174      * Returns full list of checks.
175      * @return The list
176      */
177     @SuppressWarnings("PMD.UnusedPrivateMethod")
178     private static Stream<String> checks() {
179         return Stream.of(
180             "MethodsOrderCheck",
181             "MultilineJavadocTagsCheck",
182             "StringLiteralsConcatenationCheck",
183             "EmptyLinesCheck",
184             "ImportCohesionCheck",
185             "BracketsStructureCheck",
186             "CurlyBracketsStructureCheck",
187             "JavadocLocationCheck",
188             "JavadocParameterOrderCheck",
189             "MethodBodyCommentsCheck",
190             "RequireThisCheck",
191             "ProtectedMethodInFinalClassCheck",
192             "NoJavadocForOverriddenMethodsCheck",
193             "NonStaticMethodCheck",
194             "ConstantUsageCheck",
195             "JavadocEmptyLineCheck",
196             "JavadocTagsCheck",
197             "ProhibitNonFinalClassesCheck",
198             "QualifyInnerClassCheck",
199             "CommentCheck",
200             "ProhibitUnusedPrivateConstructorCheck"
201         ).map(s -> String.format("ChecksTest/%s", s));
202     }
203 
204     /**
205      * Mocked collector of checkstyle events.
206      *
207      * @since 0.1
208      */
209     private static final class Collector implements Answer<Object> {
210 
211         /**
212          * List of events received.
213          */
214         private final List<AuditEvent> events = new LinkedList<>();
215 
216         @Override
217         public Object answer(final InvocationOnMock invocation) {
218             this.events.add((AuditEvent) invocation.getArguments()[0]);
219             return null;
220         }
221 
222         /**
223          * How many messages do we have?
224          * @return Amount of messages reported
225          */
226         public int eventCount() {
227             return this.events.size();
228         }
229 
230         /**
231          * Do we have this message for this line?
232          * @param line The number of the line
233          * @param msg The message we're looking for
234          * @return This message was reported for the give line?
235          */
236         public boolean has(final Integer line, final String msg) {
237             boolean has = false;
238             for (final AuditEvent event : this.events) {
239                 if (event.getLine() == line && event.getMessage().equals(msg)) {
240                     has = true;
241                     break;
242                 }
243             }
244             return has;
245         }
246 
247         /**
248          * Returns full summary.
249          * @return The test summary of all events
250          */
251         public String summary() {
252             final List<String> msgs = new LinkedList<>();
253             for (final AuditEvent event : this.events) {
254                 msgs.add(
255                     String.format(
256                         "%s:%s",
257                         event.getLine(),
258                         event.getMessage()
259                     )
260                 );
261             }
262             return new Joined("; ", msgs).toString();
263         }
264     }
265 
266 }