//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.google.checkstyle.test.base; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.regex.Pattern; import com.puppycrawl.tools.checkstyle.BriefUtLogger; import com.puppycrawl.tools.checkstyle.Checker; import com.puppycrawl.tools.checkstyle.ConfigurationLoader; import com.puppycrawl.tools.checkstyle.DefaultConfiguration; import com.puppycrawl.tools.checkstyle.PropertiesExpander; import com.puppycrawl.tools.checkstyle.TreeWalker; import com.puppycrawl.tools.checkstyle.api.AbstractViolationReporter; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; import com.puppycrawl.tools.checkstyle.api.Configuration; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; public class BaseCheckTestSupport { private static final Pattern WARN_PATTERN = CommonUtils .createPattern(".*[ ]*//[ ]*warn[ ]*|/[*]\\s?warn\\s?[*]/"); private static final String XML_NAME = "/google_checks.xml"; private static Configuration configuration; protected final ByteArrayOutputStream stream = new ByteArrayOutputStream(); /** * Returns {@link Configuration} based on Google's checks xml-configuration (google_checks.xml). * This implementation uses {@link ConfigurationLoader} in order to load configuration * from xml-file. * @return {@link Configuration} based on Google's checks xml-configuration (google_checks.xml). * @throws CheckstyleException if exception occurs during configuration loading. */ protected static Configuration getConfiguration() throws CheckstyleException { if (configuration == null) { configuration = ConfigurationLoader.loadConfiguration(XML_NAME, new PropertiesExpander( System.getProperties())); } return configuration; } /** * Creates {@link DefaultConfiguration} instance for the given check class. * @param clazz check class. * @return {@link DefaultConfiguration} instance. */ private static DefaultConfiguration createCheckConfig(Class<?> clazz) { return new DefaultConfiguration(clazz.getName()); } /** * Creates {@link Checker} instance based on specified {@link Configuration}. * @param checkConfig {@link Configuration} instance. * @return {@link Checker} instance. * @throws CheckstyleException if an exception occurs during checker configuration. */ protected Checker createChecker(Configuration checkConfig) throws Exception { final DefaultConfiguration dc = createCheckerConfig(checkConfig); final Checker checker = new Checker(); // make sure the tests always run with English error messages // so the tests don't fail in supported locales like German final Locale locale = Locale.ENGLISH; checker.setLocaleCountry(locale.getCountry()); checker.setLocaleLanguage(locale.getLanguage()); checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader()); checker.configure(dc); checker.addListener(new BriefUtLogger(stream)); return checker; } /** * Creates {@link DefaultConfiguration} or the {@link Checker}. * based on the given {@link Configuration}. * @param config {@link Configuration} instance. * @return {@link DefaultConfiguration} for the {@link Checker}. */ protected DefaultConfiguration createCheckerConfig(Configuration config) { final DefaultConfiguration dc = new DefaultConfiguration("configuration"); final DefaultConfiguration twConf = createCheckConfig(TreeWalker.class); // make sure that the tests always run with this charset dc.addAttribute("charset", "iso-8859-1"); dc.addChild(twConf); twConf.addChild(config); return dc; } /** * Returns canonical path for the file with the given file name. * The path is formed based on the specific root location. * This implementation uses 'src/it/resources/com/google/checkstyle/test/' as a root location. * @param fileName file name. * @return canonical path for the the file with the given file name. * @throws IOException if I/O exception occurs while forming the path. */ protected String getPath(String fileName) throws IOException { return new File("src/it/resources/com/google/checkstyle/test/" + fileName) .getCanonicalPath(); } /** * Performs verification of the file with given file name. Uses specified configuration. * Expected messages are represented by the array of strings, warning line numbers are * represented by the array of integers. * This implementation uses overloaded * {@link BaseCheckTestSupport#verify(Checker, File[], String, String[], Integer...)} method * inside. * @param config configuration. * @param fileName file name to verify. * @param expected an array of expected messages. * @param warnsExpected an array of expected warning numbers. * @throws Exception if exception occurs during verification process. */ protected void verify(Configuration config, String fileName, String[] expected, Integer... warnsExpected) throws Exception { verify(createChecker(config), new File[] {new File(fileName)}, fileName, expected, warnsExpected); } /** * Performs verification of files. Uses provided {@link Checker} instance. * @param checker {@link Checker} instance. * @param processedFiles files to process. * @param messageFileName message file name. * @param expected an array of expected messages. * @param warnsExpected an array of expected warning line numbers. * @throws Exception if exception occurs during verification process. */ protected void verify(Checker checker, File[] processedFiles, String messageFileName, String[] expected, Integer... warnsExpected) throws Exception { stream.flush(); final List<File> theFiles = new ArrayList<>(); Collections.addAll(theFiles, processedFiles); final List<Integer> theWarnings = new ArrayList<>(); Collections.addAll(theWarnings, warnsExpected); final int errs = checker.process(theFiles); // process each of the lines final ByteArrayInputStream inputStream = new ByteArrayInputStream(stream.toByteArray()); try (LineNumberReader lnr = new LineNumberReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { int previousLineNumber = 0; for (int i = 0; i < expected.length; i++) { final String expectedResult = messageFileName + ":" + expected[i]; final String actual = lnr.readLine(); assertEquals("error message " + i, expectedResult, actual); String parseInt = removeDeviceFromPathOnWindows(actual); parseInt = parseInt.substring(parseInt.indexOf(':') + 1); parseInt = parseInt.substring(0, parseInt.indexOf(':')); final int lineNumber = Integer.parseInt(parseInt); assertTrue("input file is expected to have a warning comment on line number " + lineNumber, previousLineNumber == lineNumber || theWarnings.remove((Integer) lineNumber)); previousLineNumber = lineNumber; } assertEquals("unexpected output: " + lnr.readLine(), expected.length, errs); assertEquals("unexpected warnings " + theWarnings, 0, theWarnings.size()); } checker.destroy(); } /** * Gets the check message 'as is' from appropriate 'messages.properties' * file. * * @param messageKey the key of message in 'messages.properties' file. * @param arguments the arguments of message in 'messages.properties' file. */ protected String getCheckMessage(Class<? extends AbstractViolationReporter> aClass, String messageKey, Object... arguments) { String checkMessage; try { final Properties pr = new Properties(); pr.load(aClass.getResourceAsStream("messages.properties")); final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey), Locale.ROOT); checkMessage = formatter.format(arguments); } catch (IOException ex) { checkMessage = null; } return checkMessage; } /** * Gets the check message 'as is' from appropriate 'messages.properties' file. * @param messageKey the key of message in 'messages.properties' file. * @param arguments the arguments of message in 'messages.properties' file. */ protected String getCheckMessage(Map<String, String> messages, String messageKey, Object... arguments) { String checkMessage = null; for (Map.Entry<String, String> entry : messages.entrySet()) { if (messageKey.equals(entry.getKey())) { final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT); checkMessage = formatter.format(arguments); break; } } return checkMessage; } /** * Returns {@link Configuration} instance for the given check name. * This implementation uses {@link BaseCheckTestSupport#getConfiguration()} method inside. * @param checkName check name. * @return {@link Configuration} instance for the given check name. * @throws CheckstyleException if exception occurs during configuration loading. */ protected static Configuration getCheckConfig(String checkName) throws CheckstyleException { final Configuration result; final List<Configuration> configs = getCheckConfigs(checkName); if (configs.size() == 1) { result = configs.get(0); } else { throw new IllegalStateException("multiple instances of the same Check are detected"); } return result; } /** * Returns {@link Configuration} instance for the given check name. * This implementation uses {@link BaseCheckTestSupport#getConfiguration()} method inside. * @param checkName check name. * @return {@link Configuration} instance for the given check name. * @throws CheckstyleException if exception occurs during configuration loading. */ protected static Configuration getCheckConfig(String checkName, String checkId) throws CheckstyleException { final Configuration result; final List<Configuration> configs = getCheckConfigs(checkName); if (configs.size() == 1) { result = configs.get(0); } else { result = configs.stream().filter(conf -> { try { return conf.getAttribute("id").equals(checkId); } catch (CheckstyleException ex) { throw new IllegalStateException("problem to get ID attribute from " + conf, ex); } }) .findFirst().orElseGet(null); } return result; } /** * Returns a list of all {@link Configuration} instances for the given check name. * This implementation uses {@link BaseCheckTestSupport#getConfiguration()} method inside. * @param checkName check name. * @return {@link Configuration} instance for the given check name. * @throws CheckstyleException if exception occurs during configuration loading. */ protected static List<Configuration> getCheckConfigs(String checkName) throws CheckstyleException { final List<Configuration> result = new ArrayList<>(); for (Configuration currentConfig : getConfiguration().getChildren()) { if ("TreeWalker".equals(currentConfig.getName())) { for (Configuration checkConfig : currentConfig.getChildren()) { if (checkName.equals(checkConfig.getName())) { result.add(checkConfig); } } } else if (checkName.equals(currentConfig.getName())) { result.add(currentConfig); } } return result; } private static String removeDeviceFromPathOnWindows(String path) { String fixedPath = path; final String os = System.getProperty("os.name", "Unix"); if (os.startsWith("Windows")) { fixedPath = path.substring(path.indexOf(':') + 1); } return fixedPath; } /** * Returns an array of integers which represents the warning line numbers in the file * with the given file name. * @param fileName file name. * @return an array of integers which represents the warning line numbers. * @throws IOException if I/O exception occurs while reading the file. */ protected Integer[] getLinesWithWarn(String fileName) throws IOException { final List<Integer> result = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(fileName), StandardCharsets.UTF_8))) { int lineNumber = 1; while (true) { final String line = br.readLine(); if (line == null) { break; } if (WARN_PATTERN.matcher(line).find()) { result.add(lineNumber); } lineNumber++; } } return result.toArray(new Integer[result.size()]); } }