Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

discovery.py
"""
Test discovery functions.
"""

import copy
import os
import sys

from lit.TestingConfig import TestingConfig
from lit import LitConfig, Test

def chooseConfigFileFromDir(dir, config_names):
    for name in config_names:
        p = os.path.join(dir, name)
        if os.path.exists(p):
            return p
    return None

def dirContainsTestSuite(path, lit_config):
    cfgpath = chooseConfigFileFromDir(path, lit_config.site_config_names)
    if not cfgpath:
        cfgpath = chooseConfigFileFromDir(path, lit_config.config_names)
    return cfgpath

def getTestSuite(item, litConfig, cache):
    """getTestSuite(item, litConfig, cache) -> (suite, relative_path)

    Find the test suite containing @arg item.

    @retval (None, ...) - Indicates no test suite contains @arg item.
    @retval (suite, relative_path) - The suite that @arg item is in, and its
    relative path inside that suite.
    """
    def search1(path):
        # Check for a site config or a lit config.
        cfgpath = dirContainsTestSuite(path, litConfig)

        # If we didn't find a config file, keep looking.
        if not cfgpath:
            parent,base = os.path.split(path)
            if parent == path:
                return (None, ())

            ts, relative = search(parent)
            return (ts, relative + (base,))

        # This is a private builtin parameter which can be used to perform
        # translation of configuration paths.  Specifically, this parameter
        # can be set to a dictionary that the discovery process will consult
        # when it finds a configuration it is about to load.  If the given
        # path is in the map, the value of that key is a path to the
        # configuration to load instead.
        config_map = litConfig.params.get('config_map')
        if config_map:
            cfgpath = os.path.realpath(cfgpath)
            target = config_map.get(os.path.normcase(cfgpath))
            if target:
                cfgpath = target

        # We found a test suite, create a new config for it and load it.
        if litConfig.debug:
            litConfig.note('loading suite config %r' % cfgpath)

        cfg = TestingConfig.fromdefaults(litConfig)
        cfg.load_from_path(cfgpath, litConfig)
        source_root = os.path.realpath(cfg.test_source_root or path)
        exec_root = os.path.realpath(cfg.test_exec_root or path)
        return Test.TestSuite(cfg.name, source_root, exec_root, cfg), ()

    def search(path):
        # Check for an already instantiated test suite.
        real_path = os.path.realpath(path)
        res = cache.get(real_path)
        if res is None:
            cache[real_path] = res = search1(path)
        return res

    # Canonicalize the path.
    item = os.path.normpath(os.path.join(os.getcwd(), item))

    # Skip files and virtual components.
    components = []
    while not os.path.isdir(item):
        parent,base = os.path.split(item)
        if parent == item:
            return (None, ())
        components.append(base)
        item = parent
    components.reverse()

    ts, relative = search(item)
    return ts, tuple(relative + tuple(components))

def getLocalConfig(ts, path_in_suite, litConfig, cache):
    def search1(path_in_suite):
        # Get the parent config.
        if not path_in_suite:
            parent = ts.config
        else:
            parent = search(path_in_suite[:-1])

        # Check if there is a local configuration file.
        source_path = ts.getSourcePath(path_in_suite)
        cfgpath = chooseConfigFileFromDir(source_path, litConfig.local_config_names)

        # If not, just reuse the parent config.
        if not cfgpath:
            return parent

        # Otherwise, copy the current config and load the local configuration
        # file into it.
        config = copy.deepcopy(parent)
        if litConfig.debug:
            litConfig.note('loading local config %r' % cfgpath)
        config.load_from_path(cfgpath, litConfig)
        return config

    def search(path_in_suite):
        key = (ts, path_in_suite)
        res = cache.get(key)
        if res is None:
            cache[key] = res = search1(path_in_suite)
        return res

    return search(path_in_suite)

def getTests(path, litConfig, testSuiteCache,
             localConfigCache, indirectlyRunCheck):
    # Find the test suite for this input and its relative path.
    ts,path_in_suite = getTestSuite(path, litConfig, testSuiteCache)
    if ts is None:
        litConfig.warning('unable to find test suite for %r' % path)
        return (),()

    if litConfig.debug:
        litConfig.note('resolved input %r to %r::%r' % (path, ts.name,
                                                        path_in_suite))

    return ts, getTestsInSuite(ts, path_in_suite, litConfig,
                               testSuiteCache, localConfigCache, indirectlyRunCheck)

def getTestsInSuite(ts, path_in_suite, litConfig,
                    testSuiteCache, localConfigCache, indirectlyRunCheck):
    # Check that the source path exists (errors here are reported by the
    # caller).
    source_path = ts.getSourcePath(path_in_suite)
    if not os.path.exists(source_path):
        return

    # Check if the user named a test directly.
    if not os.path.isdir(source_path):
        test_dir_in_suite = path_in_suite[:-1]
        lc = getLocalConfig(ts, test_dir_in_suite, litConfig, localConfigCache)
        test = Test.Test(ts, path_in_suite, lc)

        # Issue a error if the specified test would not be run if
        # the user had specified the containing directory instead of
        # of naming the test directly. This helps to avoid writing
        # tests which are not executed. The check adds some performance
        # overhead which might be important if a large number of tests
        # are being run directly.
        # This check can be disabled by using --no-indirectly-run-check or
        # setting the standalone_tests variable in the suite's configuration.
        if (
            indirectlyRunCheck
            and lc.test_format is not None
            and not lc.standalone_tests
        ):
            found = False
            for res in lc.test_format.getTestsInDirectory(ts, test_dir_in_suite,
                                                          litConfig, lc):
                if test.getFullName() == res.getFullName():
                    found = True
                    break
            if not found:
                litConfig.error(
                    '%r would not be run indirectly: change name or LIT config'
                    '(e.g. suffixes or standalone_tests variables)'
                    % test.getFullName())

        yield test
        return

    # Otherwise we have a directory to search for tests, start by getting the
    # local configuration.
    lc = getLocalConfig(ts, path_in_suite, litConfig, localConfigCache)

    # Directory contains tests to be run standalone. Do not try to discover.
    if lc.standalone_tests:
        if lc.suffixes or lc.excludes:
            litConfig.warning(
                'standalone_tests set in LIT config but suffixes or excludes'
                    ' are also set'
            )
        return

    # Search for tests.
    if lc.test_format is not None:
        for res in lc.test_format.getTestsInDirectory(ts, path_in_suite,
                                                      litConfig, lc):
            yield res

    # Search subdirectories.
    for filename in os.listdir(source_path):
        # FIXME: This doesn't belong here?
        if filename in ('Output', '.svn', '.git') or filename in lc.excludes:
            continue

        # Ignore non-directories.
        file_sourcepath = os.path.join(source_path, filename)
        if not os.path.isdir(file_sourcepath):
            continue

        # Check for nested test suites, first in the execpath in case there is a
        # site configuration and then in the source path.
        subpath = path_in_suite + (filename,)
        file_execpath = ts.getExecPath(subpath)
        if dirContainsTestSuite(file_execpath, litConfig):
            sub_ts, subpath_in_suite = getTestSuite(file_execpath, litConfig,
                                                    testSuiteCache)
        elif dirContainsTestSuite(file_sourcepath, litConfig):
            sub_ts, subpath_in_suite = getTestSuite(file_sourcepath, litConfig,
                                                    testSuiteCache)
        else:
            sub_ts = None

        # If the this directory recursively maps back to the current test suite,
        # disregard it (this can happen if the exec root is located inside the
        # current test suite, for example).
        if sub_ts is ts:
            continue

        # Otherwise, load from the nested test suite, if present.
        if sub_ts is not None:
            subiter = getTestsInSuite(sub_ts, subpath_in_suite, litConfig,
                                      testSuiteCache, localConfigCache,
                                      indirectlyRunCheck)
        else:
            subiter = getTestsInSuite(ts, subpath, litConfig, testSuiteCache,
                                      localConfigCache, indirectlyRunCheck)

        N = 0
        for res in subiter:
            N += 1
            yield res
        if sub_ts and not N:
            litConfig.warning('test suite %r contained no tests' % sub_ts.name)

def find_tests_for_inputs(lit_config, inputs, indirectlyRunCheck):
    """
    find_tests_for_inputs(lit_config, inputs) -> [Test]

    Given a configuration object and a list of input specifiers, find all the
    tests to execute.
    """

    # Expand '@...' form in inputs.
    actual_inputs = []
    for input in inputs:
        if input.startswith('@'):
            f = open(input[1:])
            try:
                for ln in f:
                    ln = ln.strip()
                    if ln:
                        actual_inputs.append(ln)
            finally:
                f.close()
        else:
            actual_inputs.append(input)

    # Load the tests from the inputs.
    tests = []
    test_suite_cache = {}
    local_config_cache = {}
    for input in actual_inputs:
        prev = len(tests)
        tests.extend(getTests(input, lit_config, test_suite_cache,
                              local_config_cache, indirectlyRunCheck)[1])
        if prev == len(tests):
            lit_config.warning('input %r contained no tests' % input)

    # This data is no longer needed but keeping it around causes awful
    # performance problems while the test suites run.
    for k, suite in test_suite_cache.items():
      if suite[0]:
        suite[0].test_times = None

    # If there were any errors during test discovery, exit now.
    if lit_config.numErrors:
        sys.stderr.write('%d errors, exiting.\n' % lit_config.numErrors)
        sys.exit(2)

    return tests