1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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 org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertTrue;
24  
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStreamReader;
30  import java.io.LineNumberReader;
31  import java.nio.charset.StandardCharsets;
32  import java.text.MessageFormat;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collections;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.ResourceBundle;
41  import java.util.stream.Collectors;
42  
43  import com.google.common.collect.MapDifference;
44  import com.google.common.collect.Maps;
45  import com.puppycrawl.tools.checkstyle.api.Configuration;
46  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
47  import com.puppycrawl.tools.checkstyle.internal.utils.BriefUtLogger;
48  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
49  
50  public abstract class AbstractModuleTestSupport extends AbstractPathTestSupport {
51  
52      /**
53       * Enum to specify options for checker creation.
54       */
55      public enum ModuleCreationOption {
56  
57          /**
58           * Points that the module configurations
59           * has to be added under {@link TreeWalker}.
60           */
61          IN_TREEWALKER,
62          /**
63           * Points that checker will be created as
64           * a root of default configuration.
65           */
66          IN_CHECKER,
67  
68      }
69  
70      private static final String ROOT_MODULE_NAME = "root";
71  
72      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
73  
74      /**
75       * Returns log stream.
76       * @return stream log stream
77       */
78      public ByteArrayOutputStream getStream() {
79          return stream;
80      }
81  
82      /**
83       * Returns test logger.
84       * @return logger for tests
85       */
86      public final BriefUtLogger getBriefUtLogger() {
87          return new BriefUtLogger(stream);
88      }
89  
90      protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
91          return new DefaultConfiguration(clazz.getName());
92      }
93  
94      /**
95       * Creates {@link Checker} instance based on the given {@link Configuration} instance.
96       * @param moduleConfig {@link Configuration} instance.
97       * @return {@link Checker} instance based on the given {@link Configuration} instance.
98       * @throws Exception if an exception occurs during checker configuration.
99       */
100     public final Checker createChecker(Configuration moduleConfig)
101             throws Exception {
102         ModuleCreationOption moduleCreationOption = ModuleCreationOption.IN_CHECKER;
103 
104         final String moduleName = moduleConfig.getName();
105         if (!ROOT_MODULE_NAME.equals(moduleName)) {
106             try {
107                 final Class<?> moduleClass = Class.forName(moduleName);
108                 if (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(moduleClass)
109                         || ModuleReflectionUtil.isTreeWalkerFilterModule(moduleClass)) {
110                     moduleCreationOption = ModuleCreationOption.IN_TREEWALKER;
111                 }
112             }
113             catch (ClassNotFoundException ignore) {
114                 // ignore exception, assume it is not part of TreeWalker
115             }
116         }
117 
118         return createChecker(moduleConfig, moduleCreationOption);
119     }
120 
121     /**
122      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
123      * @param moduleConfig {@link Configuration} instance.
124      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
125 *                                              under {@link TreeWalker}.
126      * @return {@link Checker} instance based on the given {@link Configuration} instance.
127      * @throws Exception if an exception occurs during checker configuration.
128      */
129     public final Checker createChecker(Configuration moduleConfig,
130                                  ModuleCreationOption moduleCreationOption)
131             throws Exception {
132         final Checker checker = new Checker();
133         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
134 
135         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
136             final Configuration dc = createTreeWalkerConfig(moduleConfig);
137             checker.configure(dc);
138         }
139         else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())) {
140             checker.configure(moduleConfig);
141         }
142         else {
143             final Configuration dc = createRootConfig(moduleConfig);
144             checker.configure(dc);
145         }
146         checker.addListener(new BriefUtLogger(stream));
147         return checker;
148     }
149 
150     /**
151      * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
152      * based on the given {@link Configuration} instance.
153      * @param config {@link Configuration} instance.
154      * @return {@link DefaultConfiguration} for the {@link TreeWalker}
155      *     based on the given {@link Configuration} instance.
156      */
157     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
158         final DefaultConfiguration dc =
159                 new DefaultConfiguration("configuration");
160         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
161         // make sure that the tests always run with this charset
162         dc.addAttribute("charset", StandardCharsets.UTF_8.name());
163         dc.addChild(twConf);
164         twConf.addChild(config);
165         return dc;
166     }
167 
168     /**
169      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
170      * @param config {@link Configuration} instance.
171      * @return {@link DefaultConfiguration} for the given {@link Configuration} instance.
172      */
173     protected static DefaultConfiguration createRootConfig(Configuration config) {
174         final DefaultConfiguration dc = new DefaultConfiguration(ROOT_MODULE_NAME);
175         if (config != null) {
176             dc.addChild(config);
177         }
178         return dc;
179     }
180 
181     /**
182      * Returns canonical path for the file with the given file name.
183      * The path is formed base on the non-compilable resources location.
184      * This implementation uses 'src/test/resources-noncompilable/com/puppycrawl/tools/checkstyle/'
185      * as a non-compilable resource location.
186      * @param filename file name.
187      * @return canonical path for the file with the given file name.
188      * @throws IOException if I/O exception occurs while forming the path.
189      */
190     protected final String getNonCompilablePath(String filename) throws IOException {
191         return new File("src/test/resources-noncompilable/" + getPackageLocation() + "/"
192                 + filename).getCanonicalPath();
193     }
194 
195     /**
196      * Returns URI-representation of the path for the given file name.
197      * The path is formed base on the root location.
198      * This implementation uses 'src/test/resources/com/puppycrawl/tools/checkstyle/'
199      * as a root location.
200      * @param filename file name.
201      * @return URI-representation of the path for the file with the given file name.
202      */
203     protected final String getUriString(String filename) {
204         return new File("src/test/resources/" + getPackageLocation() + "/" + filename).toURI()
205                 .toString();
206     }
207 
208     /**
209      * Performs verification of the file with the given file name. Uses specified configuration.
210      * Expected messages are represented by the array of strings.
211      * This implementation uses overloaded
212      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
213      * @param aConfig configuration.
214      * @param fileName file name to verify.
215      * @param expected an array of expected messages.
216      * @throws Exception if exception occurs during verification process.
217      */
218     protected final void verify(Configuration aConfig, String fileName, String... expected)
219             throws Exception {
220         verify(createChecker(aConfig), fileName, fileName, expected);
221     }
222 
223     /**
224      * Performs verification of the file with the given file name.
225      * Uses provided {@link Checker} instance.
226      * Expected messages are represented by the array of strings.
227      * This implementation uses overloaded
228      * {@link AbstractModuleTestSupport#verify(Checker, String, String, String...)} method inside.
229      * @param checker {@link Checker} instance.
230      * @param fileName file name to verify.
231      * @param expected an array of expected messages.
232      * @throws Exception if exception occurs during verification process.
233      */
234     protected void verify(Checker checker, String fileName, String... expected)
235             throws Exception {
236         verify(checker, fileName, fileName, expected);
237     }
238 
239     /**
240      * Performs verification of the file with the given file name.
241      * Uses provided {@link Checker} instance.
242      * Expected messages are represented by the array of strings.
243      * This implementation uses overloaded
244      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String...)} method inside.
245      * @param checker {@link Checker} instance.
246      * @param processedFilename file name to verify.
247      * @param messageFileName message file name.
248      * @param expected an array of expected messages.
249      * @throws Exception if exception occurs during verification process.
250      */
251     protected final void verify(Checker checker,
252                           String processedFilename,
253                           String messageFileName,
254                           String... expected)
255             throws Exception {
256         verify(checker,
257                 new File[] {new File(processedFilename)},
258                 messageFileName, expected);
259     }
260 
261     /**
262      *  We keep two verify methods with separate logic only for convenience of debugging.
263      *  We have minimum amount of multi-file test cases.
264      *  @param checker {@link Checker} instance.
265      *  @param processedFiles list of files to verify.
266      *  @param messageFileName message file name.
267      *  @param expected an array of expected messages.
268      *  @throws Exception if exception occurs during verification process.
269      */
270     protected void verify(Checker checker,
271                           File[] processedFiles,
272                           String messageFileName,
273                           String... expected)
274             throws Exception {
275         stream.flush();
276         stream.reset();
277         final List<File> theFiles = new ArrayList<>();
278         Collections.addAll(theFiles, processedFiles);
279         final int errs = checker.process(theFiles);
280 
281         // process each of the lines
282         try (ByteArrayInputStream inputStream =
283                 new ByteArrayInputStream(stream.toByteArray());
284             LineNumberReader lnr = new LineNumberReader(
285                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
286             final List<String> actuals = lnr.lines().limit(expected.length)
287                     .sorted().collect(Collectors.toList());
288             Arrays.sort(expected);
289 
290             for (int i = 0; i < expected.length; i++) {
291                 final String expectedResult = messageFileName + ":" + expected[i];
292                 assertEquals("error message " + i, expectedResult, actuals.get(i));
293             }
294 
295             assertEquals("unexpected output: " + lnr.readLine(),
296                     expected.length, errs);
297         }
298 
299         checker.destroy();
300     }
301 
302     /**
303      * Performs verification of the given files.
304      * @param checker {@link Checker} instance
305      * @param processedFiles files to process.
306      * @param expectedViolations a map of expected violations per files.
307      * @throws Exception if exception occurs during verification process.
308      */
309     protected final void verify(Checker checker,
310                           File[] processedFiles,
311                           Map<String, List<String>> expectedViolations)
312             throws Exception {
313         stream.flush();
314         stream.reset();
315         final List<File> theFiles = new ArrayList<>();
316         Collections.addAll(theFiles, processedFiles);
317         final int errs = checker.process(theFiles);
318 
319         // process each of the lines
320         final Map<String, List<String>> actualViolations = getActualViolations(errs);
321         final Map<String, List<String>> realExpectedViolations =
322                 Maps.filterValues(expectedViolations, input -> !input.isEmpty());
323         final MapDifference<String, List<String>> violationDifferences =
324                 Maps.difference(realExpectedViolations, actualViolations);
325 
326         final Map<String, List<String>> missingViolations =
327                 violationDifferences.entriesOnlyOnLeft();
328         final Map<String, List<String>> unexpectedViolations =
329                 violationDifferences.entriesOnlyOnRight();
330         final Map<String, MapDifference.ValueDifference<List<String>>> differingViolations =
331                 violationDifferences.entriesDiffering();
332 
333         final StringBuilder message = new StringBuilder(256);
334         if (!missingViolations.isEmpty()) {
335             message.append("missing violations: ").append(missingViolations);
336         }
337         if (!unexpectedViolations.isEmpty()) {
338             if (message.length() > 0) {
339                 message.append('\n');
340             }
341             message.append("unexpected violations: ").append(unexpectedViolations);
342         }
343         if (!differingViolations.isEmpty()) {
344             if (message.length() > 0) {
345                 message.append('\n');
346             }
347             message.append("differing violations: ").append(differingViolations);
348         }
349 
350         assertTrue(message.toString(),
351                 missingViolations.isEmpty()
352                         && unexpectedViolations.isEmpty()
353                         && differingViolations.isEmpty());
354 
355         checker.destroy();
356     }
357 
358     private Map<String, List<String>> getActualViolations(int errorCount) throws IOException {
359         // process each of the lines
360         try (ByteArrayInputStream inputStream =
361                 new ByteArrayInputStream(stream.toByteArray());
362             LineNumberReader lnr = new LineNumberReader(
363                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
364             final Map<String, List<String>> actualViolations = new HashMap<>();
365             for (String line = lnr.readLine(); line != null && lnr.getLineNumber() <= errorCount;
366                  line = lnr.readLine()) {
367                 // have at least 2 characters before the splitting colon,
368                 // to not split after the drive letter on windows
369                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
370                 final String actualViolationFileName = actualViolation[0];
371                 final String actualViolationMessage = actualViolation[1];
372 
373                 List<String> actualViolationsPerFile =
374                         actualViolations.get(actualViolationFileName);
375                 if (actualViolationsPerFile == null) {
376                     actualViolationsPerFile = new ArrayList<>();
377                     actualViolations.put(actualViolationFileName, actualViolationsPerFile);
378                 }
379                 actualViolationsPerFile.add(actualViolationMessage);
380             }
381 
382             return actualViolations;
383         }
384     }
385 
386     /**
387      * Gets the check message 'as is' from appropriate 'messages.properties'
388      * file.
389      *
390      * @param messageKey the key of message in 'messages.properties' file.
391      * @param arguments  the arguments of message in 'messages.properties' file.
392      * @return The message of the check with the arguments applied.
393      */
394     protected final String getCheckMessage(String messageKey, Object... arguments) {
395         return internalGetCheckMessage(getMessageBundle(), messageKey, arguments);
396     }
397 
398     /**
399      * Gets the check message 'as is' from appropriate 'messages.properties'
400      * file.
401      *
402      * @param clazz the related check class.
403      * @param messageKey the key of message in 'messages.properties' file.
404      * @param arguments the arguments of message in 'messages.properties' file.
405      * @return The message of the check with the arguments applied.
406      */
407     protected static String getCheckMessage(
408             Class<?> clazz, String messageKey, Object... arguments) {
409         return internalGetCheckMessage(getMessageBundle(clazz.getName()), messageKey, arguments);
410     }
411 
412     /**
413      * Gets the check message 'as is' from appropriate 'messages.properties'
414      * file.
415      *
416      * @param messageBundle the bundle name.
417      * @param messageKey the key of message in 'messages.properties' file.
418      * @param arguments the arguments of message in 'messages.properties' file.
419      * @return The message of the check with the arguments applied.
420      */
421     private static String internalGetCheckMessage(
422             String messageBundle, String messageKey, Object... arguments) {
423         final ResourceBundle resourceBundle = ResourceBundle.getBundle(
424                 messageBundle,
425                 Locale.getDefault(),
426                 Thread.currentThread().getContextClassLoader(),
427                 new LocalizedMessage.Utf8Control());
428         final String pattern = resourceBundle.getString(messageKey);
429         final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
430         return formatter.format(arguments);
431     }
432 
433     private String getMessageBundle() {
434         final String className = getClass().getName();
435         return getMessageBundle(className);
436     }
437 
438     private static String getMessageBundle(String className) {
439         final String messageBundle;
440         final String messages = "messages";
441         final int endIndex = className.lastIndexOf('.');
442         if (endIndex < 0) {
443             messageBundle = messages;
444         }
445         else {
446             final String packageName = className.substring(0, endIndex);
447             messageBundle = packageName + "." + messages;
448         }
449         return messageBundle;
450     }
451 
452 }