View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.google.checkstyle.test.base;
21  
22  import static org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertTrue;
24  
25  import java.io.BufferedReader;
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.IOException;
31  import java.io.InputStreamReader;
32  import java.io.LineNumberReader;
33  import java.nio.charset.StandardCharsets;
34  import java.text.MessageFormat;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.Properties;
41  import java.util.Set;
42  import java.util.regex.Pattern;
43  
44  import com.puppycrawl.tools.checkstyle.Checker;
45  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
46  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
47  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
48  import com.puppycrawl.tools.checkstyle.TreeWalker;
49  import com.puppycrawl.tools.checkstyle.api.AbstractViolationReporter;
50  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
51  import com.puppycrawl.tools.checkstyle.api.Configuration;
52  import com.puppycrawl.tools.checkstyle.internal.utils.BriefUtLogger;
53  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
54  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
55  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtils;
56  
57  public abstract class AbstractModuleTestSupport extends AbstractPathTestSupport {
58  
59      /**
60       * Enum to specify options for checker creation.
61       */
62      public enum ModuleCreationOption {
63          /**
64           * Points that the module configurations
65           * has to be added under {@link TreeWalker}.
66           */
67          IN_TREEWALKER,
68          /**
69           * Points that checker will be created as
70           * a root of default configuration.
71           */
72          IN_CHECKER
73      }
74  
75      private static final Pattern WARN_PATTERN = CommonUtils
76              .createPattern(".*[ ]*//[ ]*warn[ ]*|/[*]\\s?warn\\s?[*]/");
77  
78      private static final String XML_NAME = "/google_checks.xml";
79  
80      private static Configuration configuration;
81  
82      private static Set<Class<?>> checkstyleModules;
83  
84      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
85  
86      /**
87       * Returns test logger.
88       * @return logger test logger
89       */
90      public final BriefUtLogger getBriefUtLogger() {
91          return new BriefUtLogger(stream);
92      }
93  
94      /**
95       * Returns {@link Configuration} based on Google's checks xml-configuration (google_checks.xml).
96       * This implementation uses {@link ConfigurationLoader} in order to load configuration
97       * from xml-file.
98       * @return {@link Configuration} based on Google's checks xml-configuration (google_checks.xml).
99       * @throws CheckstyleException if exception occurs during configuration loading.
100      */
101     protected static Configuration getConfiguration() throws CheckstyleException {
102         if (configuration == null) {
103             configuration = ConfigurationLoader.loadConfiguration(XML_NAME, new PropertiesExpander(
104                     System.getProperties()));
105         }
106 
107         return configuration;
108     }
109 
110     /**
111      * Creates {@link DefaultConfiguration} instance for the given module class.
112      * @param clazz module class.
113      * @return {@link DefaultConfiguration} instance.
114      */
115     private static DefaultConfiguration createModuleConfig(Class<?> clazz) {
116         return new DefaultConfiguration(clazz.getName());
117     }
118 
119     /**
120      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
121      * @param moduleConfig {@link Configuration} instance.
122      * @return {@link Checker} instance based on the given {@link Configuration} instance.
123      * @throws Exception if an exception occurs during checker configuration.
124      */
125     public final Checker createChecker(Configuration moduleConfig)
126             throws Exception {
127         if (checkstyleModules == null) {
128             checkstyleModules = CheckUtil.getCheckstyleModules();
129         }
130 
131         final String name = moduleConfig.getName();
132         ModuleCreationOption moduleCreationOption = ModuleCreationOption.IN_CHECKER;
133 
134         for (Class<?> moduleClass : checkstyleModules) {
135             if (moduleClass.getSimpleName().equals(name)
136                     || moduleClass.getSimpleName().equals(name + "Check")) {
137                 if (ModuleReflectionUtils.isCheckstyleTreeWalkerCheck(moduleClass)
138                         || ModuleReflectionUtils.isTreeWalkerFilterModule(moduleClass)) {
139                     moduleCreationOption = ModuleCreationOption.IN_TREEWALKER;
140                 }
141                 break;
142             }
143         }
144 
145         return createChecker(moduleConfig, moduleCreationOption);
146     }
147 
148     /**
149      * Creates {@link Checker} instance based on specified {@link Configuration}.
150      * @param moduleConfig {@link Configuration} instance.
151      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
152      *                                                  under {@link TreeWalker}.
153      * @return {@link Checker} instance.
154      * @throws Exception if an exception occurs during checker configuration.
155      */
156     protected final Checker createChecker(Configuration moduleConfig,
157                                     ModuleCreationOption moduleCreationOption)
158             throws Exception {
159         final DefaultConfiguration dc;
160 
161         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
162             dc = createTreeWalkerConfig(moduleConfig);
163         }
164         else {
165             dc = createRootConfig(moduleConfig);
166         }
167 
168         final Checker checker = new Checker();
169         // make sure the tests always run with English error messages
170         // so the tests don't fail in supported locales like German
171         final Locale locale = Locale.ENGLISH;
172         checker.setLocaleCountry(locale.getCountry());
173         checker.setLocaleLanguage(locale.getLanguage());
174         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
175         checker.configure(dc);
176         checker.addListener(getBriefUtLogger());
177         return checker;
178     }
179 
180     /**
181      * Creates {@link DefaultConfiguration} or the {@link Checker}.
182      * based on the given {@link Configuration}.
183      * @param config {@link Configuration} instance.
184      * @return {@link DefaultConfiguration} for the {@link Checker}.
185      */
186     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
187         final DefaultConfiguration dc =
188                 new DefaultConfiguration("configuration");
189         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
190         // make sure that the tests always run with this charset
191         dc.addAttribute("charset", "iso-8859-1");
192         dc.addChild(twConf);
193         twConf.addChild(config);
194         return dc;
195     }
196 
197     /**
198      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
199      * @param config {@link Configuration} instance.
200      * @return {@link DefaultConfiguration} for the given {@link Configuration} instance.
201      */
202     protected static DefaultConfiguration createRootConfig(Configuration config) {
203         final DefaultConfiguration dc = new DefaultConfiguration("root");
204         dc.addChild(config);
205         return dc;
206     }
207 
208     /**
209      * Performs verification of the file with given file name. Uses specified configuration.
210      * Expected messages are represented by the array of strings, warning line numbers are
211      * represented by the array of integers.
212      * This implementation uses overloaded
213      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String[], Integer...)}
214      * method inside.
215      * @param config configuration.
216      * @param fileName file name to verify.
217      * @param expected an array of expected messages.
218      * @param warnsExpected an array of expected warning numbers.
219      * @throws Exception if exception occurs during verification process.
220      */
221     protected final void verify(Configuration config, String fileName, String[] expected,
222             Integer... warnsExpected) throws Exception {
223         verify(createChecker(config),
224                 new File[] {new File(fileName)},
225                 fileName, expected, warnsExpected);
226     }
227 
228     /**
229      * Performs verification of files. Uses provided {@link Checker} instance.
230      * @param checker {@link Checker} instance.
231      * @param processedFiles files to process.
232      * @param messageFileName message file name.
233      * @param expected an array of expected messages.
234      * @param warnsExpected an array of expected warning line numbers.
235      * @throws Exception if exception occurs during verification process.
236      */
237     protected final void verify(Checker checker,
238             File[] processedFiles,
239             String messageFileName,
240             String[] expected,
241             Integer... warnsExpected)
242             throws Exception {
243         stream.flush();
244         final List<File> theFiles = new ArrayList<>();
245         Collections.addAll(theFiles, processedFiles);
246         final List<Integer> theWarnings = new ArrayList<>();
247         Collections.addAll(theWarnings, warnsExpected);
248         final int errs = checker.process(theFiles);
249 
250         // process each of the lines
251         final ByteArrayInputStream inputStream =
252                 new ByteArrayInputStream(stream.toByteArray());
253         try (LineNumberReader lnr = new LineNumberReader(
254                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
255 
256             int previousLineNumber = 0;
257             for (int i = 0; i < expected.length; i++) {
258                 final String expectedResult = messageFileName + ":" + expected[i];
259                 final String actual = lnr.readLine();
260                 assertEquals("error message " + i, expectedResult, actual);
261 
262                 String parseInt = removeDeviceFromPathOnWindows(actual);
263                 parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
264                 parseInt = parseInt.substring(0, parseInt.indexOf(':'));
265                 final int lineNumber = Integer.parseInt(parseInt);
266                 assertTrue("input file is expected to have a warning comment on line number "
267                         + lineNumber, previousLineNumber == lineNumber
268                             || theWarnings.remove((Integer) lineNumber));
269                 previousLineNumber = lineNumber;
270             }
271 
272             assertEquals("unexpected output: " + lnr.readLine(),
273                     expected.length, errs);
274             assertEquals("unexpected warnings " + theWarnings, 0, theWarnings.size());
275         }
276 
277         checker.destroy();
278     }
279 
280     /**
281      * Gets the check message 'as is' from appropriate 'messages.properties'
282      * file.
283      *
284      * @param aClass The package the message is located in.
285      * @param messageKey the key of message in 'messages.properties' file.
286      * @param arguments  the arguments of message in 'messages.properties' file.
287      * @return The message of the check with the arguments applied.
288      */
289     protected static String getCheckMessage(Class<? extends AbstractViolationReporter> aClass,
290             String messageKey, Object... arguments) {
291         String checkMessage;
292         try {
293             final Properties pr = new Properties();
294             pr.load(aClass.getResourceAsStream("messages.properties"));
295             final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
296                     Locale.ROOT);
297             checkMessage = formatter.format(arguments);
298         }
299         catch (IOException ex) {
300             checkMessage = null;
301         }
302         return checkMessage;
303     }
304 
305     /**
306      * Gets the check message 'as is' from appropriate 'messages.properties' file.
307      * @param messages The map of messages to scan.
308      * @param messageKey the key of message in 'messages.properties' file.
309      * @param arguments the arguments of message in 'messages.properties' file.
310      * @return The message of the check with the arguments applied.
311      */
312     protected static String getCheckMessage(Map<String, String> messages, String messageKey,
313             Object... arguments) {
314         String checkMessage = null;
315         for (Map.Entry<String, String> entry : messages.entrySet()) {
316             if (messageKey.equals(entry.getKey())) {
317                 final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT);
318                 checkMessage = formatter.format(arguments);
319                 break;
320             }
321         }
322         return checkMessage;
323     }
324 
325     /**
326      * Returns {@link Configuration} instance for the given module name.
327      * This implementation uses {@link AbstractModuleTestSupport#getConfiguration()} method inside.
328      * @param moduleName module name.
329      * @return {@link Configuration} instance for the given module name.
330      * @throws CheckstyleException if exception occurs during configuration loading.
331      */
332     protected static Configuration getModuleConfig(String moduleName) throws CheckstyleException {
333         return getModuleConfig(moduleName, null);
334     }
335 
336     /**
337      * Returns {@link Configuration} instance for the given module name.
338      * This implementation uses {@link AbstractModuleTestSupport#getConfiguration()} method inside.
339      * @param moduleName module name.
340      * @param moduleId module id.
341      * @return {@link Configuration} instance for the given module name.
342      * @throws CheckstyleException if exception occurs during configuration loading.
343      */
344     protected static Configuration getModuleConfig(String moduleName, String moduleId)
345             throws CheckstyleException {
346         final Configuration result;
347         final List<Configuration> configs = getModuleConfigs(moduleName);
348         if (configs.size() == 1) {
349             result = configs.get(0);
350         }
351         else if (moduleId == null) {
352             throw new IllegalStateException("multiple instances of the same Module are detected");
353         }
354         else {
355             result = configs.stream().filter(conf -> {
356                 try {
357                     return conf.getAttribute("id").equals(moduleId);
358                 }
359                 catch (CheckstyleException ex) {
360                     throw new IllegalStateException("problem to get ID attribute from " + conf, ex);
361                 }
362             })
363             .findFirst().orElseGet(null);
364         }
365 
366         return result;
367     }
368 
369     /**
370      * Returns a list of all {@link Configuration} instances for the given module name.
371      * This implementation uses {@link AbstractModuleTestSupport#getConfiguration()} method inside.
372      * @param moduleName module name.
373      * @return {@link Configuration} instance for the given module name.
374      * @throws CheckstyleException if exception occurs during configuration loading.
375      */
376     protected static List<Configuration> getModuleConfigs(String moduleName)
377             throws CheckstyleException {
378         final List<Configuration> result = new ArrayList<>();
379         for (Configuration currentConfig : getConfiguration().getChildren()) {
380             if ("TreeWalker".equals(currentConfig.getName())) {
381                 for (Configuration moduleConfig : currentConfig.getChildren()) {
382                     if (moduleName.equals(moduleConfig.getName())) {
383                         result.add(moduleConfig);
384                     }
385                 }
386             }
387             else if (moduleName.equals(currentConfig.getName())) {
388                 result.add(currentConfig);
389             }
390         }
391         return result;
392     }
393 
394     private static String removeDeviceFromPathOnWindows(String path) {
395         String fixedPath = path;
396         final String os = System.getProperty("os.name", "Unix");
397         if (os.startsWith("Windows")) {
398             fixedPath = path.substring(path.indexOf(':') + 1);
399         }
400         return fixedPath;
401     }
402 
403     /**
404      * Returns an array of integers which represents the warning line numbers in the file
405      * with the given file name.
406      * @param fileName file name.
407      * @return an array of integers which represents the warning line numbers.
408      * @throws IOException if I/O exception occurs while reading the file.
409      */
410     protected Integer[] getLinesWithWarn(String fileName) throws IOException {
411         final List<Integer> result = new ArrayList<>();
412         try (BufferedReader br = new BufferedReader(new InputStreamReader(
413                 new FileInputStream(fileName), StandardCharsets.UTF_8))) {
414             int lineNumber = 1;
415             while (true) {
416                 final String line = br.readLine();
417                 if (line == null) {
418                     break;
419                 }
420                 if (WARN_PATTERN.matcher(line).find()) {
421                     result.add(lineNumber);
422                 }
423                 lineNumber++;
424             }
425         }
426         return result.toArray(new Integer[result.size()]);
427     }
428 }