code: ///////////////////////////////////////////////////////////////////////////////////////////////
  // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
  // Copyright (C) 2001-2023 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 org.checkstyle.base;
  
  import static com.google.common.truth.Truth.assertWithMessage;
  
  import java.io.BufferedReader;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.LineNumberReader;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  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.AbstractPathTestSupport;
  import com.puppycrawl.tools.checkstyle.Checker;
  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
  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.internal.utils.BriefUtLogger;
  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
  
  public abstract class AbstractItModuleTestSupport extends AbstractPathTestSupport {
  
      /**
       * Enum to specify options for checker creation.
       */
      public enum ModuleCreationOption {
  
          /**
           * Points that the module configurations
           * has to be added under {@link TreeWalker}.
           */
          IN_TREEWALKER,
          /**
           * Points that checker will be created as
           * a root of default configuration.
           */
          IN_CHECKER,
  
      }
  
      protected static final String ROOT_MODULE_NAME = "root";
  
      private static final Pattern WARN_PATTERN = CommonUtil
              .createPattern(".* *// *warn *|/[*]\\*?\\s?warn\\s?[*]/");
  
      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
  
      /**
       * Find the module creation option to use for the module name.
       *
       * @param moduleName the module name.
       * @return the module creation option.
       */
      protected abstract ModuleCreationOption findModuleCreationOption(String moduleName);
  
      /**
       * Returns test logger.
       *
       * @return logger for tests
       */
      protected final BriefUtLogger getBriefUtLogger() {
          return new BriefUtLogger(stream);
      }
  
      /**
       * Creates a default module configuration {@link DefaultConfiguration} for a given object
       * of type {@link Class}.
       *
       * @param clazz a {@link Class} type object.
       * @return default module configuration for the given {@link Class} instance.
       */
      protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
          return new DefaultConfiguration(clazz.getName());
      }
  
      /**
       * Returns {@link Configuration} instance for the given module name pulled
       * from the {@code masterConfig}.
       *
       * @param masterConfig The master configuration to examine.
       * @param moduleName module name.
       * @param moduleId module id.
       * @return {@link Configuration} instance for the given module name.
       * @throws IllegalStateException if there is a problem retrieving the module
       *         or config.
       */
      protected static Configuration getModuleConfig(Configuration masterConfig, String moduleName,
              String moduleId) {
          final Configuration result;
          final List<Configuration> configs = getModuleConfigs(masterConfig, moduleName);
          if (configs.size() == 1) {
              result = configs.get(0);
          }
          else if (configs.isEmpty()) {
              throw new IllegalStateException("no instances of the Module was found: " + moduleName);
          }
          else if (moduleId == null) {
              throw new IllegalStateException("multiple instances of the same Module are detected");
          }
          else {
              result = configs.stream().filter(conf -> isSameModuleId(conf, moduleId))
              .findFirst()
              .orElseThrow(() -> new IllegalStateException("problem with module config"));
          }
  
          return result;
      }
  
      /**
       * Verifies if the configuration's ID matches the expected {@code moduleId}.
       *
       * @param conf The config to examine.
       * @param moduleId The module ID to match against.
       * @return {@code true} if it matches.
       * @throws IllegalStateException If there is an issue with finding the ID.
       */
      private static boolean isSameModuleId(Configuration conf, String moduleId) {
          try {
              return conf.getProperty("id").equals(moduleId);
          }
          catch (CheckstyleException ex) {
              throw new IllegalStateException("problem to get ID attribute from " + conf, ex);
          }
      }
  
      /**
       * Returns a list of all {@link Configuration} instances for the given module IDs in the
       * {@code masterConfig}.
       *
       * @param masterConfig The master configuration to pull results from.
       * @param moduleIds module IDs.
       * @return List of {@link Configuration} instances.
       * @throws CheckstyleException if there is an error with the config.
       */
      protected static List<Configuration> getModuleConfigsByIds(Configuration masterConfig,
              String... moduleIds) throws CheckstyleException {
          final List<Configuration> result = new ArrayList<>();
          for (Configuration currentConfig : masterConfig.getChildren()) {
              if ("TreeWalker".equals(currentConfig.getName())) {
                  for (Configuration moduleConfig : currentConfig.getChildren()) {
                      final String id = getProperty(moduleConfig, "id");
                      if (id != null && isIn(id, moduleIds)) {
                          result.add(moduleConfig);
                      }
                  }
              }
              else {
                  final String id = getProperty(currentConfig, "id");
                  if (id != null && isIn(id, moduleIds)) {
                      result.add(currentConfig);
                  }
              }
          }
          return result;
      }
  
      /**
       * Finds the specific property {@code name} in the {@code config}.
       *
       * @param config The configuration to examine.
       * @param name The property name to find.
       * @return The property value or {@code null} if not found.
       * @throws CheckstyleException if there is an error with the config.
       */
      private static String getProperty(Configuration config, String name)
              throws CheckstyleException {
          String result = null;
  
          if (isIn(name, config.getPropertyNames())) {
              result = config.getProperty(name);
          }
  
          return result;
      }
  
      /**
       * Finds the specific ID in a list of IDs.
       *
       * @param find The ID to find.
       * @param list The list of module IDs.
       * @return {@code true} if the ID is in the list.
       */
      private static boolean isIn(String find, String... list) {
          boolean found = false;
  
          for (String item : list) {
              if (find.equals(item)) {
                  found = true;
                  break;
              }
          }
  
          return found;
      }
  
      /**
       * Returns a list of all {@link Configuration} instances for the given
       * module name pulled from the {@code masterConfig}.
       *
       * @param masterConfig The master configuration to examine.
       * @param moduleName module name.
       * @return {@link Configuration} instance for the given module name.
       */
      private static List<Configuration> getModuleConfigs(Configuration masterConfig,
              String moduleName) {
          final List<Configuration> result = new ArrayList<>();
          for (Configuration currentConfig : masterConfig.getChildren()) {
              if ("TreeWalker".equals(currentConfig.getName())) {
                  for (Configuration moduleConfig : currentConfig.getChildren()) {
                      if (moduleName.equals(moduleConfig.getName())) {
                          result.add(moduleConfig);
                      }
                  }
              }
              else if (moduleName.equals(currentConfig.getName())) {
                  result.add(currentConfig);
              }
          }
          return result;
      }
  
      /**
       * Creates {@link Checker} instance based on the given {@link Configuration} instance.
       *
       * @param moduleConfig {@link Configuration} instance.
       * @return {@link Checker} instance based on the given {@link Configuration} instance.
       * @throws Exception if an exception occurs during checker configuration.
       */
      protected final Checker createChecker(Configuration moduleConfig)
              throws Exception {
          final String name = moduleConfig.getName();
  
          return createChecker(moduleConfig, findModuleCreationOption(name));
      }
  
      /**
       * Creates {@link Checker} instance based on the given {@link Configuration} instance.
       *
       * @param moduleConfig {@link Configuration} instance.
       * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
       *                                                  under {@link TreeWalker}.
       * @return {@link Checker} instance based on the given {@link Configuration} instance.
       * @throws Exception if an exception occurs during checker configuration.
       */
      protected final Checker createChecker(Configuration moduleConfig,
                                   ModuleCreationOption moduleCreationOption)
              throws Exception {
          final Checker checker = new Checker();
          checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
          // 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());
  
          if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
              final Configuration config = createTreeWalkerConfig(moduleConfig);
              checker.configure(config);
          }
          else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())) {
              checker.configure(moduleConfig);
          }
          else {
              final Configuration config = createRootConfig(moduleConfig);
              checker.configure(config);
          }
          checker.addListener(getBriefUtLogger());
          return checker;
      }
  
      /**
       * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
       * based on the given {@link Configuration} instance.
       *
       * @param config {@link Configuration} instance.
       * @return {@link DefaultConfiguration} for the {@link TreeWalker}
       *     based on the given {@link Configuration} instance.
       */
      protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
          final DefaultConfiguration rootConfig =
                  new DefaultConfiguration(ROOT_MODULE_NAME);
          final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
          // make sure that the tests always run with this charset
          rootConfig.addProperty("charset", StandardCharsets.UTF_8.name());
          rootConfig.addChild(twConf);
          twConf.addChild(config);
          return rootConfig;
      }
  
      /**
       * Creates {@link DefaultConfiguration} or the Checker.
       * based on the the list of {@link Configuration}.
       *
       * @param configs list of {@link Configuration} instances.
       * @return {@link DefaultConfiguration} for the Checker.
       */
      protected static DefaultConfiguration createTreeWalkerConfig(
              List<Configuration> configs) {
          DefaultConfiguration result = null;
  
          for (Configuration config : configs) {
              if (result == null) {
                  result = (DefaultConfiguration) createTreeWalkerConfig(config).getChildren()[0];
              }
              else {
                  result.addChild(config);
              }
          }
  
          return result;
      }
  
      /**
       * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
       *
       * @param config {@link Configuration} instance.
       * @return {@link DefaultConfiguration} for the given {@link Configuration} instance.
       */
      protected static DefaultConfiguration createRootConfig(Configuration config) {
          final DefaultConfiguration rootConfig = new DefaultConfiguration(ROOT_MODULE_NAME);
          rootConfig.addChild(config);
          return rootConfig;
      }
  
      /**
       * Returns canonical path for the file with the given file name.
       * The path is formed base on the non-compilable resources location.
       *
       * @param filename file name.
       * @return canonical path for the file with the given file name.
       * @throws IOException if I/O exception occurs while forming the path.
       */
      protected final String getNonCompilablePath(String filename) throws IOException {
          return new File("src/" + getResourceLocation() + "/resources-noncompilable/"
                  + getPackageLocation() + "/" + filename).getCanonicalPath();
      }
  
      /**
       * Performs verification of the file with the 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 AbstractItModuleTestSupport#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 final 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 final void verify(Checker checker,
              File[] processedFiles,
              String messageFileName,
              String[] expected,
              Integer... warnsExpected)
              throws Exception {
          stream.flush();
          stream.reset();
          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
          try (ByteArrayInputStream inputStream =
                  new ByteArrayInputStream(stream.toByteArray());
              LineNumberReader lnr = new LineNumberReader(
                  new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
              int previousLineNumber = 0;
              for (int index = 0; index < expected.length; index++) {
                  final String expectedResult = messageFileName + ":" + expected[index];
                  final String actual = lnr.readLine();
                  assertWithMessage("Error message at position %s of 'expected' does "
                          + "not match actual message", index)
                      .that(actual)
                      .isEqualTo(expectedResult);
  
                  String parseInt = removeDeviceFromPathOnWindows(actual);
                  parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
                  parseInt = parseInt.substring(0, parseInt.indexOf(':'));
                  final int lineNumber = Integer.parseInt(parseInt);
                  assertWithMessage(
                          "input file is expected to have a warning comment on line number %s",
                          lineNumber)
                      .that(previousLineNumber == lineNumber
                              || theWarnings.remove((Integer) lineNumber))
                      .isTrue();
                  previousLineNumber = lineNumber;
              }
  
              assertWithMessage("unexpected output: %s", lnr.readLine())
                  .that(errs)
                  .isEqualTo(expected.length);
              assertWithMessage("unexpected warnings %s", theWarnings)
                  .that(theWarnings)
                  .isEmpty();
          }
  
          checker.destroy();
      }
  
      /**
       * Gets the check message 'as is' from appropriate 'messages.properties'
       * file.
       *
       * @param aClass the package the message is located in.
       * @param messageKey the key of message in 'messages.properties' file.
       * @param arguments  the arguments of message in 'messages.properties' file.
       * @return The message of the check with the arguments applied.
       * @throws IOException if there is a problem loading the property file.
       */
      protected static String getCheckMessage(Class<? extends AbstractViolationReporter> aClass,
              String messageKey, Object... arguments) throws IOException {
          final Properties pr = new Properties();
          pr.load(aClass.getResourceAsStream("messages.properties"));
          final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
                  Locale.ROOT);
          return formatter.format(arguments);
      }
  
      /**
       * Gets the check message 'as is' from appropriate 'messages.properties' file.
       *
       * @param messages the map of messages to scan.
       * @param messageKey the key of message in 'messages.properties' file.
       * @param arguments the arguments of message in 'messages.properties' file.
       * @return The message of the check with the arguments applied.
       */
      protected static 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;
      }
  
      /**
       * Remove device from path string for windows path.
       *
       * @param path path to correct.
       * @return Path without device name.
       */
      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 = Files.newBufferedReader(
                  Paths.get(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[0]);
      }
  
      @Override
      protected String getResourceLocation() {
          return "it";
      }
  
  }
  