org.apache.hive.ptest.execution.conf.UnitTestPropertiesParser.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hive.ptest.execution.conf.UnitTestPropertiesParser.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.hive.ptest.execution.conf;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;

class UnitTestPropertiesParser {

    private static final Splitter VALUE_SPLITTER = Splitter.onPattern("[, ]").trimResults().omitEmptyStrings();

    // Prefix for top level properties.
    static final String PROP_PREFIX_ROOT = "unitTests";
    // Prefix used to specify module specific properties. Mainly to avoid conflicts with older unitTests properties
    static final String PROP_PREFIX_MODULE = "ut";

    static final String PROP_DIRECTORIES = "directories";
    static final String PROP_INCLUDE = "include";
    static final String PROP_EXCLUDE = "exclude";
    static final String PROP_ISOLATE = "isolate";
    static final String PROP_SKIP_BATCHING = "skipBatching";
    static final String PROP_BATCH_SIZE = "batchSize";
    static final String PROP_SUBDIR_FOR_PREFIX = "subdirForPrefix";

    static final String PROP_ONE_MODULE = "module";
    static final String PROP_MODULE_LIST = "modules";

    private final AtomicInteger batchIdCounter;

    static final int DEFAULT_PROP_BATCH_SIZE = 1;
    static final int DEFAULT_PROP_BATCH_SIZE_NOT_SPECIFIED = -1;
    static final int DEFAULT_PROP_BATCH_SIZE_INCLUDE_ALL = 0;
    static final String DEFAULT_PROP_DIRECTORIES = ".";
    static final String DEFAULT_PROP_SUBDIR_FOR_PREFIX = "target";

    static final String MODULE_NAME_TOP_LEVEL = "_root_"; // Special module for tests in the rootDir.
    static final String PREFIX_TOP_LEVEL = ".";

    private final Context unitRootContext; // Everything prefixed by ^unitTests.
    private final Context unitModuleContext; // Everything prefixed by ^ut.
    private final String testCasePropertyName;
    private final Logger logger;
    private final File sourceDirectory;
    private final FileListProvider fileListProvider;
    private final Set<String> excludedProvided; // excludedProvidedBy Framework vs excludedConfigured
    private final boolean inTest;

    @VisibleForTesting
    UnitTestPropertiesParser(Context testContext, AtomicInteger batchIdCounter, String testCasePropertyName,
            File sourceDirectory, Logger logger, FileListProvider fileListProvider, Set<String> excludedProvided,
            boolean inTest) {
        logger.info("{} created with sourceDirectory={}, testCasePropertyName={}, excludedProvide={}",
                "fileListProvider={}, inTest={}", UnitTestPropertiesParser.class.getSimpleName(), sourceDirectory,
                testCasePropertyName, excludedProvided,
                (fileListProvider == null ? "null" : fileListProvider.getClass().getSimpleName()), inTest);
        Preconditions.checkNotNull(batchIdCounter, "batchIdCounter cannot be null");
        Preconditions.checkNotNull(testContext, "testContext cannot be null");
        Preconditions.checkNotNull(testCasePropertyName, "testCasePropertyName cannot be null");
        Preconditions.checkNotNull(sourceDirectory, "sourceDirectory cannot be null");
        Preconditions.checkNotNull(logger, "logger must be specified");
        this.batchIdCounter = batchIdCounter;
        this.unitRootContext = new Context(testContext.getSubProperties(Joiner.on(".").join(PROP_PREFIX_ROOT, "")));
        this.unitModuleContext = new Context(
                testContext.getSubProperties(Joiner.on(".").join(PROP_PREFIX_MODULE, "")));
        this.sourceDirectory = sourceDirectory;
        this.testCasePropertyName = testCasePropertyName;
        this.logger = logger;
        if (excludedProvided != null) {
            this.excludedProvided = excludedProvided;
        } else {
            this.excludedProvided = new HashSet<>();
        }
        if (fileListProvider != null) {
            this.fileListProvider = fileListProvider;
        } else {
            this.fileListProvider = new DefaultFileListProvider();
        }
        this.inTest = inTest;

    }

    UnitTestPropertiesParser(Context testContext, AtomicInteger batchIdCounter, String testCasePropertyName,
            File sourceDirectory, Logger logger, Set<String> excludedProvided) {
        this(testContext, batchIdCounter, testCasePropertyName, sourceDirectory, logger, null, excludedProvided,
                false);
    }

    Collection<TestBatch> generateTestBatches() {
        try {
            return parse();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Collection<TestBatch> parse() throws IOException {

        RootConfig rootConfig = getRootConfig(unitRootContext);
        logger.info("RootConfig: " + rootConfig);

        // TODO: Set this up as a tree, instead of a flat list.
        Map<String, ModuleConfig> moduleConfigs = extractModuleConfigs();
        logger.info("ModuleConfigs: {} ", moduleConfigs);

        List<TestDir> unitTestsDirs = processPropertyDirectories();

        validateConfigs(rootConfig, moduleConfigs, unitTestsDirs);

        LinkedHashMap<String, LinkedHashSet<TestInfo>> allTests = generateFullTestSet(rootConfig, moduleConfigs,
                unitTestsDirs);

        return createTestBatches(allTests, rootConfig, moduleConfigs);
    }

    private Collection<TestBatch> createTestBatches(LinkedHashMap<String, LinkedHashSet<TestInfo>> allTests,
            RootConfig rootConfig, Map<String, ModuleConfig> moduleConfigs) {
        List<TestBatch> testBatches = new LinkedList<>();
        for (Map.Entry<String, LinkedHashSet<TestInfo>> entry : allTests.entrySet()) {
            logger.info("Creating test batches for module={}, numTests={}", entry.getKey(),
                    entry.getValue().size());
            String currentModule = entry.getKey();
            String currentPathPrefix = getPathPrefixFromModuleName(currentModule);
            int batchSize = rootConfig.batchSize;
            if (moduleConfigs.containsKey(currentModule)) {
                ModuleConfig moduleConfig = moduleConfigs.get(currentModule);
                int batchSizeModule = moduleConfig.batchSize;
                if (batchSizeModule != DEFAULT_PROP_BATCH_SIZE_NOT_SPECIFIED) {
                    batchSize = batchSizeModule;
                }
            }

            if (batchSize == DEFAULT_PROP_BATCH_SIZE_INCLUDE_ALL) {
                batchSize = Integer.MAX_VALUE;
            }
            logger.info("batchSize determined to be {} for module={}", batchSize, currentModule);

            // TODO Even out the batch sizes (i.e. 20/20/1 should be replaced by 14/14/13)
            List<String> currentList = new LinkedList<>();
            for (TestInfo testInfo : entry.getValue()) {
                if (testInfo.isIsolated || testInfo.skipBatching) {
                    UnitTestBatch unitTestBatch = new UnitTestBatch(batchIdCounter, testCasePropertyName,
                            Collections.singletonList(testInfo.testName), currentPathPrefix, !testInfo.isIsolated);
                    testBatches.add(unitTestBatch);
                } else {
                    currentList.add(testInfo.testName);
                    if (currentList.size() == batchSize) {
                        UnitTestBatch unitTestBatch = new UnitTestBatch(batchIdCounter, testCasePropertyName,
                                Collections.unmodifiableList(currentList), currentPathPrefix, true);
                        testBatches.add(unitTestBatch);
                        currentList = new LinkedList<>();
                    }
                }
            }
            if (!currentList.isEmpty()) {
                UnitTestBatch unitTestBatch = new UnitTestBatch(batchIdCounter, testCasePropertyName,
                        Collections.unmodifiableList(currentList), currentPathPrefix, true);
                testBatches.add(unitTestBatch);
            }
        }
        return testBatches;
    }

    private RootConfig getRootConfig(Context context) {
        ModuleConfig moduleConfig = getModuleConfig(context, "irrelevant", DEFAULT_PROP_BATCH_SIZE);

        String subDirForPrefix = context.getString(PROP_SUBDIR_FOR_PREFIX, DEFAULT_PROP_SUBDIR_FOR_PREFIX);
        Preconditions.checkArgument(StringUtils.isNotBlank(subDirForPrefix) && !subDirForPrefix.contains("/"));

        Context modulesContext = new Context(context.getSubProperties(Joiner.on(".").join(PROP_MODULE_LIST, "")));
        Set<String> includedModules = getProperty(modulesContext, PROP_INCLUDE);
        Set<String> excludedModules = getProperty(modulesContext, PROP_EXCLUDE);
        if (!includedModules.isEmpty() && !excludedModules.isEmpty()) {
            throw new IllegalArgumentException(String.format(
                    "%s and %s are mutually exclusive for property %s. Provided values: included=%s, excluded=%s",
                    PROP_INCLUDE, PROP_EXCLUDE, PROP_MODULE_LIST, includedModules, excludedModules));
        }

        return new RootConfig(includedModules, excludedModules, moduleConfig.include, moduleConfig.exclude,
                moduleConfig.skipBatching, moduleConfig.isolate, moduleConfig.batchSize, subDirForPrefix);
    }

    private ModuleConfig getModuleConfig(Context context, String moduleName, int defaultBatchSize) {
        Set<String> excluded = getProperty(context, PROP_EXCLUDE);
        Set<String> isolated = getProperty(context, PROP_ISOLATE);
        Set<String> included = getProperty(context, PROP_INCLUDE);
        Set<String> skipBatching = getProperty(context, PROP_SKIP_BATCHING);
        if (!included.isEmpty() && !excluded.isEmpty()) {
            throw new IllegalArgumentException(
                    String.format("Included and excluded mutually exclusive." + " Included = %s, excluded = %s",
                            included.toString(), excluded.toString()) + " for module: " + moduleName);
        }
        int batchSize = context.getInteger(PROP_BATCH_SIZE, defaultBatchSize);

        String pathPrefix = getPathPrefixFromModuleName(moduleName);

        return new ModuleConfig(moduleName, included, excluded, skipBatching, isolated, batchSize, pathPrefix);
    }

    private Set<String> getProperty(Context context, String propertyName) {
        return Sets.newHashSet(VALUE_SPLITTER.split(context.getString(propertyName, "")));
    }

    private String getPathPrefixFromModuleName(String moduleName) {
        String pathPrefix;
        if (moduleName.equals(MODULE_NAME_TOP_LEVEL)) {
            pathPrefix = PREFIX_TOP_LEVEL;
        } else {
            pathPrefix = moduleName.replace(".", "/");
        }
        return pathPrefix;
    }

    private String getModuleNameFromPathPrefix(String pathPrefix) {
        if (pathPrefix.equals(PREFIX_TOP_LEVEL)) {
            return MODULE_NAME_TOP_LEVEL;
        } else {
            pathPrefix = stripEndAndStart(pathPrefix, "/");
            pathPrefix = pathPrefix.replace("/", ".");
            // Example handling of dirs with a .
            // shims/hadoop-2.6
            //   -> moduleName=shims.hadoop-.2.6
            return pathPrefix;
        }
    }

    private String stripEndAndStart(String srcString, String stripChars) {
        srcString = StringUtils.stripEnd(srcString, stripChars);
        srcString = StringUtils.stripStart(srcString, stripChars);
        return srcString;
    }

    private Map<String, ModuleConfig> extractModuleConfigs() {
        Collection<String> modules = extractConfiguredModules();
        Map<String, ModuleConfig> result = new HashMap<>();

        for (String moduleName : modules) {
            Context moduleContext = new Context(
                    unitModuleContext.getSubProperties(Joiner.on(".").join(moduleName, "")));
            ModuleConfig moduleConfig = getModuleConfig(moduleContext, moduleName,
                    DEFAULT_PROP_BATCH_SIZE_NOT_SPECIFIED);
            logger.info("Adding moduleConfig={}", moduleConfig);
            result.put(moduleName, moduleConfig);
        }
        return result;
    }

    private Collection<String> extractConfiguredModules() {
        List<String> configuredModules = new LinkedList<>();

        Map<String, String> modulesMap = unitRootContext.getSubProperties(Joiner.on(".").join(PROP_ONE_MODULE, ""));
        for (Map.Entry<String, String> module : modulesMap.entrySet()) {
            // This is an unnecessary check, and forced configuration in the property file. Maybe
            // replace with an enforced empty value string.
            Preconditions.checkArgument(module.getKey().equals(module.getValue()));
            String moduleName = module.getKey();
            configuredModules.add(moduleName);
        }
        return configuredModules;
    }

    private List<TestDir> processPropertyDirectories() throws IOException {
        String srcDirString = sourceDirectory.getCanonicalPath();
        List<TestDir> unitTestsDirs = Lists.newArrayList();
        String propDirectoriies = unitRootContext.getString(PROP_DIRECTORIES, DEFAULT_PROP_DIRECTORIES);
        Iterable<String> propDirectoriesIterable = VALUE_SPLITTER.split(propDirectoriies);

        for (String unitTestDir : propDirectoriesIterable) {
            File unitTestParent = new File(sourceDirectory, unitTestDir);
            if (unitTestParent.isDirectory() || inTest) {
                String absUnitTestDir = unitTestParent.getCanonicalPath();

                Preconditions.checkState(absUnitTestDir.startsWith(srcDirString),
                        "Unit test dir: " + absUnitTestDir + " is not under provided src dir: " + srcDirString);
                String modulePath = absUnitTestDir.substring(srcDirString.length());

                modulePath = stripEndAndStart(modulePath, "/");

                Preconditions.checkState(!modulePath.startsWith("/"),
                        String.format("Illegal module path: [%s]", modulePath));
                if (StringUtils.isEmpty(modulePath)) {
                    modulePath = PREFIX_TOP_LEVEL;
                }
                String moduleName = getModuleNameFromPathPrefix(modulePath);
                logger.info("modulePath determined as {} for testdir={}, DerivedModuleName={}", modulePath,
                        absUnitTestDir, moduleName);

                logger.info("Adding unitTests dir [{}],[{}]", unitTestParent, moduleName);
                unitTestsDirs.add(new TestDir(unitTestParent, moduleName));
            } else {
                logger.warn("Unit test directory " + unitTestParent + " does not exist, or is a file.");
            }
        }

        return unitTestsDirs;
    }

    private void validateConfigs(RootConfig rootConfig, Map<String, ModuleConfig> moduleConfigs,
            List<TestDir> unitTestDir) {

        if (rootConfig.include.isEmpty() & rootConfig.exclude.isEmpty()) {
            // No conflicts. Module configuration is what will be used.
            // We've already verified that includes and excludes are not present at the same time for
            // individual modules.
            return;
        }

        // Validate mainly for includes / excludes working as they should.
        for (Map.Entry<String, ModuleConfig> entry : moduleConfigs.entrySet()) {
            if (rootConfig.excludedModules.contains(entry.getKey())) {
                // Don't bother validating.
                continue;
            }

            if (!rootConfig.includedModules.isEmpty() && !rootConfig.includedModules.contains(entry.getKey())) {
                // Include specified, but this module is not in the set.
                continue;
            }

            // If global contains includes, individual modules can only contain additional includes.
            if (!rootConfig.include.isEmpty() && !entry.getValue().exclude.isEmpty()) {
                throw new IllegalStateException(String.format(
                        "Global config specified includes, while module config for %s specified excludes",
                        entry.getKey()));
            }
            // If global contains excludes, individual modules can only contain additional excludes.
            if (!rootConfig.exclude.isEmpty() && !entry.getValue().include.isEmpty()) {
                throw new IllegalStateException(String.format(
                        "Global config specified excludes, while module config for %s specified includes",
                        entry.getKey()));
            }
        }
    }

    private LinkedHashMap<String, LinkedHashSet<TestInfo>> generateFullTestSet(RootConfig rootConfig,
            Map<String, ModuleConfig> moduleConfigs, List<TestDir> unitTestDirs) throws IOException {
        LinkedHashMap<String, LinkedHashSet<TestInfo>> result = new LinkedHashMap<>();

        for (TestDir unitTestDir : unitTestDirs) {
            for (File classFile : fileListProvider.listFiles(unitTestDir.path, new String[] { "class" }, true)) {
                String className = classFile.getName();

                if (className.startsWith("Test") && !className.contains("$")) {
                    String testName = className.replaceAll("\\.class$", "");
                    String pathPrefix = getPathPrefix(classFile, rootConfig.subDirForPrefix);
                    String moduleName = getModuleNameFromPathPrefix(pathPrefix);
                    logger.debug("In {}, found class {} with pathPrefix={}, moduleName={}", unitTestDir.path,
                            className, pathPrefix, moduleName);

                    ModuleConfig moduleConfig = moduleConfigs.get(moduleName);
                    if (moduleConfig == null) {
                        moduleConfig = FAKE_MODULE_CONFIG;
                    }
                    TestInfo testInfo = checkAndGetTestInfo(moduleName, pathPrefix, testName, rootConfig,
                            moduleConfig);
                    if (testInfo != null) {
                        logger.info("Adding test: " + testInfo);
                        addTestToResult(result, testInfo);
                    }
                } else {
                    logger.trace("In {}, found class {} with pathPrefix={}. Not a test", unitTestDir.path,
                            className);
                }
            }
        }
        return result;
    }

    private void addTestToResult(Map<String, LinkedHashSet<TestInfo>> result, TestInfo testInfo) {
        LinkedHashSet<TestInfo> moduleSet = result.get(testInfo.moduleName);
        if (moduleSet == null) {
            moduleSet = new LinkedHashSet<>();
            result.put(testInfo.moduleName, moduleSet);
        }
        moduleSet.add(testInfo);
    }

    private String getPathPrefix(File file, String subDirPrefix) throws IOException {
        String fname = file.getCanonicalPath();
        Preconditions.checkState(fname.startsWith(sourceDirectory.getCanonicalPath()));
        fname = fname.substring(sourceDirectory.getCanonicalPath().length(), fname.length());
        if (fname.contains(subDirPrefix)) {
            fname = fname.substring(0, fname.indexOf(subDirPrefix));
            fname = StringUtils.stripStart(fname, "/");
            if (StringUtils.isEmpty(fname)) {
                fname = PREFIX_TOP_LEVEL;
            }
            return fname;
        } else {
            logger.error("Could not find subDirPrefix {} in path: {}", subDirPrefix, fname);
            return PREFIX_TOP_LEVEL;
        }
    }

    private TestInfo checkAndGetTestInfo(String moduleName, String moduleRelDir, String testName,
            RootConfig rootConfig, ModuleConfig moduleConfig) {
        Preconditions.checkNotNull(moduleConfig);
        TestInfo testInfo;
        String rejectReason = null;
        try {
            if (rootConfig.excludedModules.contains(moduleName)) {
                rejectReason = "root level module exclude";
                return null;
            }
            if (!rootConfig.includedModules.isEmpty() && !rootConfig.includedModules.contains(moduleName)) {
                rejectReason = "root level include, but not for module";
                return null;
            }
            if (rootConfig.exclude.contains(testName)) {
                rejectReason = "root excludes test";
                return null;
            }
            if (moduleConfig.exclude.contains(testName)) {
                rejectReason = "module excludes test";
                return null;
            }
            boolean containsInclude = !rootConfig.include.isEmpty() || !moduleConfig.include.isEmpty();
            if (containsInclude) {
                if (!(rootConfig.include.contains(testName) || moduleConfig.include.contains(testName))) {
                    rejectReason = "test missing from include list";
                    return null;
                }
            }
            if (excludedProvided.contains(testName)) {
                // All qfiles handled via this...
                rejectReason = "test present in provided exclude list";
                return null;
            }

            // Add the test.
            testInfo = new TestInfo(moduleName, moduleRelDir, testName,
                    rootConfig.skipBatching.contains(testName) || moduleConfig.skipBatching.contains(testName),
                    rootConfig.isolate.contains(testName) || moduleConfig.isolate.contains(testName));
            return testInfo;

        } finally {
            if (rejectReason != null) {
                logger.debug("excluding {} due to {}", testName, rejectReason);
            }
        }
    }

    private static final class RootConfig {
        private final Set<String> includedModules;
        private final Set<String> excludedModules;
        private final Set<String> include;
        private final Set<String> exclude;
        private final Set<String> skipBatching;
        private final Set<String> isolate;
        private final int batchSize;
        private final String subDirForPrefix;

        RootConfig(Set<String> includedModules, Set<String> excludedModules, Set<String> include,
                Set<String> exclude, Set<String> skipBatching, Set<String> isolate, int batchSize,
                String subDirForPrefix) {
            this.includedModules = includedModules;
            this.excludedModules = excludedModules;
            this.include = include;
            this.exclude = exclude;
            this.skipBatching = skipBatching;
            this.isolate = isolate;
            this.batchSize = batchSize;
            this.subDirForPrefix = subDirForPrefix;
        }

        @Override
        public String toString() {
            return "RootConfig{" + "includedModules=" + includedModules + ", excludedModules=" + excludedModules
                    + ", include=" + include + ", exclude=" + exclude + ", skipBatching=" + skipBatching
                    + ", isolate=" + isolate + ", batchSize=" + batchSize + ", subDirForPrefix='" + subDirForPrefix
                    + '\'' + '}';
        }
    }

    private static final ModuleConfig FAKE_MODULE_CONFIG = new ModuleConfig("_FAKE_", new HashSet<String>(),
            new HashSet<String>(), new HashSet<String>(), new HashSet<String>(),
            DEFAULT_PROP_BATCH_SIZE_NOT_SPECIFIED, "_fake_");

    private static final class ModuleConfig {
        private final String name;
        private final Set<String> include;
        private final Set<String> exclude;
        private final Set<String> skipBatching;
        private final Set<String> isolate;
        private final String pathPrefix;
        private final int batchSize;

        ModuleConfig(String name, Set<String> include, Set<String> exclude, Set<String> skipBatching,
                Set<String> isolate, int batchSize, String pathPrefix) {
            this.name = name;
            this.include = include;
            this.exclude = exclude;
            this.skipBatching = skipBatching;
            this.isolate = isolate;
            this.batchSize = batchSize;
            this.pathPrefix = pathPrefix;
        }

        @Override
        public String toString() {
            return "ModuleConfig{" + "name='" + name + '\'' + ", include=" + include + ", exclude=" + exclude
                    + ", skipBatching=" + skipBatching + ", isolate=" + isolate + ", pathPrefix='" + pathPrefix
                    + '\'' + ", batchSize=" + batchSize + '}';
        }
    }

    private static class TestDir {
        final File path;
        final String module;

        TestDir(File path, String module) {
            this.path = path;
            this.module = module;
        }

        @Override
        public String toString() {
            return "TestDir{" + "path=" + path + ", module='" + module + '\'' + '}';
        }
    }

    private static class TestInfo {
        final String moduleName;
        final String moduleRelativeDir;
        final String testName;
        final boolean skipBatching;
        final boolean isIsolated;

        TestInfo(String moduleName, String moduleRelativeDir, String testName, boolean skipBatching,
                boolean isIsolated) {
            this.moduleName = moduleName;
            this.moduleRelativeDir = moduleRelativeDir;
            this.testName = testName;
            this.skipBatching = skipBatching;
            this.isIsolated = isIsolated;
        }

        @Override
        public String toString() {
            return "TestInfo{" + "moduleName='" + moduleName + '\'' + ", moduleRelativeDir='" + moduleRelativeDir
                    + '\'' + ", testName='" + testName + '\'' + ", skipBatching=" + skipBatching + ", isIsolated="
                    + isIsolated + '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            TestInfo testInfo = (TestInfo) o;

            return skipBatching == testInfo.skipBatching && isIsolated == testInfo.isIsolated
                    && moduleName.equals(testInfo.moduleName)
                    && moduleRelativeDir.equals(testInfo.moduleRelativeDir) && testName.equals(testInfo.testName);

        }

        @Override
        public int hashCode() {
            int result = moduleName.hashCode();
            result = 31 * result + moduleRelativeDir.hashCode();
            result = 31 * result + testName.hashCode();
            result = 31 * result + (skipBatching ? 1 : 0);
            result = 31 * result + (isIsolated ? 1 : 0);
            return result;
        }
    }

    private static final class DefaultFileListProvider implements FileListProvider {

        @Override
        public Collection<File> listFiles(File directory, String[] extensions, boolean recursive) {
            return FileUtils.listFiles(directory, extensions, recursive);
        }
    }
}