2015-05-08 14:20:43 -04:00
|
|
|
"""
|
|
|
|
|
Test selection utility.
|
|
|
|
|
|
|
|
|
|
Defines filtering rules for what tests to include in a suite depending
|
|
|
|
|
on whether they apply to C++ unit tests, dbtests, or JS tests.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
2016-04-28 14:11:49 -04:00
|
|
|
import errno
|
2015-05-08 14:20:43 -04:00
|
|
|
import fnmatch
|
|
|
|
|
import os.path
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
from . import config
|
|
|
|
|
from . import errors
|
|
|
|
|
from . import utils
|
|
|
|
|
from .utils import globstar
|
|
|
|
|
from .utils import jscomment
|
|
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
|
2015-08-04 11:33:33 -04:00
|
|
|
def _filter_cpp_tests(kind, root, include_files, exclude_files):
|
2015-05-08 14:20:43 -04:00
|
|
|
"""
|
2015-08-04 11:33:33 -04:00
|
|
|
Generic filtering logic for C++ tests that are sourced from a list
|
|
|
|
|
of test executables.
|
2015-05-08 14:20:43 -04:00
|
|
|
"""
|
|
|
|
|
include_files = utils.default_if_none(include_files, [])
|
|
|
|
|
exclude_files = utils.default_if_none(exclude_files, [])
|
|
|
|
|
|
2015-08-04 11:33:33 -04:00
|
|
|
tests = []
|
2015-05-08 14:20:43 -04:00
|
|
|
with open(root, "r") as fp:
|
2015-08-04 11:33:33 -04:00
|
|
|
for test_path in fp:
|
|
|
|
|
test_path = test_path.rstrip()
|
|
|
|
|
tests.append(test_path)
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
return list(_filter_by_filename(kind, tests, include_files, exclude_files))
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2015-08-04 11:33:33 -04:00
|
|
|
|
2016-04-28 14:11:49 -04:00
|
|
|
def filter_cpp_unit_tests(root=config.DEFAULT_UNIT_TEST_LIST,
|
|
|
|
|
include_files=None,
|
|
|
|
|
exclude_files=None):
|
2015-08-04 11:33:33 -04:00
|
|
|
"""
|
|
|
|
|
Filters out what C++ unit tests to run.
|
|
|
|
|
"""
|
|
|
|
|
return _filter_cpp_tests("C++ unit test", root, include_files, exclude_files)
|
|
|
|
|
|
|
|
|
|
|
2016-04-28 14:11:49 -04:00
|
|
|
def filter_cpp_integration_tests(root=config.DEFAULT_INTEGRATION_TEST_LIST,
|
2015-08-04 11:33:33 -04:00
|
|
|
include_files=None,
|
|
|
|
|
exclude_files=None):
|
|
|
|
|
"""
|
|
|
|
|
Filters out what C++ integration tests to run.
|
|
|
|
|
"""
|
|
|
|
|
return _filter_cpp_tests("C++ integration test", root, include_files, exclude_files)
|
2015-05-08 14:20:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_dbtests(binary=None, include_suites=None):
|
|
|
|
|
"""
|
|
|
|
|
Filters out what dbtests to run.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Command line option overrides the YAML configuration.
|
|
|
|
|
binary = utils.default_if_none(config.DBTEST_EXECUTABLE, binary)
|
|
|
|
|
# Use the default if nothing specified.
|
|
|
|
|
binary = utils.default_if_none(binary, config.DEFAULT_DBTEST_EXECUTABLE)
|
|
|
|
|
|
|
|
|
|
include_suites = utils.default_if_none(include_suites, [])
|
|
|
|
|
|
|
|
|
|
if not utils.is_string_list(include_suites):
|
|
|
|
|
raise TypeError("include_suites must be a list of strings")
|
|
|
|
|
|
|
|
|
|
# Ensure that executable files on Windows have a ".exe" extension.
|
|
|
|
|
if sys.platform == "win32" and os.path.splitext(binary)[1] != ".exe":
|
|
|
|
|
binary += ".exe"
|
|
|
|
|
|
2016-04-28 14:11:49 -04:00
|
|
|
if not os.path.isfile(binary):
|
|
|
|
|
raise IOError(errno.ENOENT, "File not found", binary)
|
|
|
|
|
|
2015-05-08 14:20:43 -04:00
|
|
|
program = subprocess.Popen([binary, "--list"], stdout=subprocess.PIPE)
|
|
|
|
|
stdout = program.communicate()[0]
|
|
|
|
|
|
|
|
|
|
if program.returncode != 0:
|
|
|
|
|
raise errors.ResmokeError("Getting list of dbtest suites failed")
|
|
|
|
|
|
|
|
|
|
dbtests = stdout.splitlines()
|
|
|
|
|
|
|
|
|
|
if not include_suites:
|
|
|
|
|
return dbtests
|
|
|
|
|
|
|
|
|
|
dbtests = set(dbtests)
|
|
|
|
|
|
|
|
|
|
(verbatim, globbed) = _partition(include_suites, normpath=False)
|
|
|
|
|
included = _pop_all("dbtest suite", dbtests, verbatim)
|
|
|
|
|
|
|
|
|
|
for suite_pattern in globbed:
|
|
|
|
|
for suite_name in dbtests:
|
|
|
|
|
if fnmatch.fnmatchcase(suite_name, suite_pattern):
|
|
|
|
|
included.add(suite_name)
|
|
|
|
|
|
|
|
|
|
return list(included)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_jstests(roots,
|
|
|
|
|
include_files=None,
|
|
|
|
|
include_with_all_tags=None,
|
|
|
|
|
include_with_any_tags=None,
|
|
|
|
|
exclude_files=None,
|
|
|
|
|
exclude_with_all_tags=None,
|
|
|
|
|
exclude_with_any_tags=None):
|
|
|
|
|
"""
|
|
|
|
|
Filters out what jstests to run.
|
|
|
|
|
"""
|
|
|
|
|
include_files = utils.default_if_none(include_files, [])
|
|
|
|
|
exclude_files = utils.default_if_none(exclude_files, [])
|
|
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
# Command line options are merged with YAML options.
|
2015-11-12 12:09:40 -05:00
|
|
|
tags = {
|
2017-01-05 19:02:51 -05:00
|
|
|
# The constructor for an empty set does not accept "None".
|
|
|
|
|
"exclude_with_all_tags": set(utils.default_if_none(exclude_with_all_tags, [])),
|
|
|
|
|
"exclude_with_any_tags": set(utils.default_if_none(exclude_with_any_tags, [])),
|
|
|
|
|
"include_with_all_tags": set(utils.default_if_none(include_with_all_tags, [])),
|
|
|
|
|
"include_with_any_tags": set(utils.default_if_none(include_with_any_tags, [])),
|
2015-11-12 12:09:40 -05:00
|
|
|
}
|
2017-01-05 19:02:51 -05:00
|
|
|
|
|
|
|
|
for name in tags:
|
|
|
|
|
if not utils.is_string_set(tags[name]):
|
|
|
|
|
raise TypeError("%s must be a YAML list of strings" % (name))
|
|
|
|
|
|
|
|
|
|
cmd_line_lists = (
|
2015-11-12 12:09:40 -05:00
|
|
|
("exclude_with_all_tags", config.EXCLUDE_WITH_ALL_TAGS),
|
|
|
|
|
("exclude_with_any_tags", config.EXCLUDE_WITH_ANY_TAGS),
|
|
|
|
|
("include_with_all_tags", config.INCLUDE_WITH_ALL_TAGS),
|
|
|
|
|
("include_with_any_tags", config.INCLUDE_WITH_ANY_TAGS),
|
|
|
|
|
)
|
2017-01-05 19:02:51 -05:00
|
|
|
|
|
|
|
|
# Merge command line options with YAML options.
|
|
|
|
|
for (tag_category, cmd_line_list) in cmd_line_lists:
|
|
|
|
|
if cmd_line_list is not None:
|
2015-11-12 12:09:40 -05:00
|
|
|
# Ignore the empty string when it is used as a tag. Specifying an empty string on the
|
2017-01-05 19:02:51 -05:00
|
|
|
# command line has no effect and allows a user to more easily synthesize a resmoke.py
|
|
|
|
|
# invocation in their Evergreen project configuration.
|
|
|
|
|
for cmd_line_tags in cmd_line_list:
|
|
|
|
|
tags[tag_category] |= set(tag for tag in cmd_line_tags.split(",") if tag != "")
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
jstests_list = []
|
|
|
|
|
for root in roots:
|
|
|
|
|
jstests_list.extend(globstar.iglob(root))
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
using_tags = False
|
|
|
|
|
for tag in tags.values():
|
|
|
|
|
if tag:
|
|
|
|
|
using_tags = True
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
# Skip converting 'jstests_list' to a set when it isn't being filtered in order to retain its
|
|
|
|
|
# ordering.
|
|
|
|
|
if not include_files and not exclude_files and not using_tags:
|
|
|
|
|
return jstests_list
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
jstests_set = _filter_by_filename("jstest", jstests_list, include_files, exclude_files)
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
# Skip parsing comments if not using tags.
|
2015-05-08 14:20:43 -04:00
|
|
|
if not using_tags:
|
2017-01-05 19:02:51 -05:00
|
|
|
return list(jstests_set)
|
2015-05-08 14:20:43 -04:00
|
|
|
|
|
|
|
|
excluded = set()
|
|
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
for filename in jstests_set:
|
2015-05-08 14:20:43 -04:00
|
|
|
file_tags = set(jscomment.get_tags(filename))
|
2017-01-05 19:02:51 -05:00
|
|
|
if tags["include_with_all_tags"] and tags["include_with_all_tags"] - file_tags:
|
|
|
|
|
excluded.add(filename)
|
|
|
|
|
if tags["include_with_any_tags"] and not tags["include_with_any_tags"] & file_tags:
|
2015-05-08 14:20:43 -04:00
|
|
|
excluded.add(filename)
|
2017-01-05 19:02:51 -05:00
|
|
|
if tags["exclude_with_all_tags"] and not tags["exclude_with_all_tags"] - file_tags:
|
|
|
|
|
excluded.add(filename)
|
|
|
|
|
if tags["exclude_with_any_tags"] and tags["exclude_with_any_tags"] & file_tags:
|
2015-05-08 14:20:43 -04:00
|
|
|
excluded.add(filename)
|
|
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
# Specifying include_files overrides tags.
|
|
|
|
|
return list((jstests_set - excluded) | set(include_files))
|
2015-05-08 14:20:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_by_filename(kind, universe, include_files, exclude_files):
|
|
|
|
|
"""
|
|
|
|
|
Filters out what tests to run solely by filename.
|
|
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
Returns either the set of files from 'universe' that are present in 'include_files', or the
|
|
|
|
|
set of files from 'universe' that aren't present in 'exclude_files', depending on which of
|
|
|
|
|
'include_files' and 'exclude_files' is non-empty.
|
|
|
|
|
|
|
|
|
|
An error is raised if both 'include_files' and 'exclude_files' are specified.
|
2015-05-08 14:20:43 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not utils.is_string_list(include_files):
|
|
|
|
|
raise TypeError("include_files must be a list of strings")
|
|
|
|
|
elif not utils.is_string_list(exclude_files):
|
|
|
|
|
raise TypeError("exclude_files must be a list of strings")
|
|
|
|
|
elif include_files and exclude_files:
|
|
|
|
|
raise ValueError("Cannot specify both include_files and exclude_files")
|
|
|
|
|
|
|
|
|
|
universe = set(universe)
|
2017-01-05 19:02:51 -05:00
|
|
|
files = include_files if include_files else exclude_files
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
(verbatim, globbed) = _partition(files)
|
|
|
|
|
# Remove all matching files of 'verbatim' from 'universe'.
|
|
|
|
|
files_verbatim = _pop_all(kind, universe, verbatim)
|
|
|
|
|
files_globbed = set()
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
for file_pattern in globbed:
|
|
|
|
|
files_globbed.update(globstar.iglob(file_pattern))
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
# Remove all matching files of 'files_globbed' from 'universe' without checking whether
|
|
|
|
|
# the same file is expanded to multiple times. This implicitly takes an intersection
|
|
|
|
|
# between 'files_globbed' and 'universe'.
|
|
|
|
|
files_globbed = _pop_all(kind, universe, files_globbed, validate=False)
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
if include_files:
|
|
|
|
|
return files_verbatim | files_globbed
|
2015-05-08 14:20:43 -04:00
|
|
|
|
2017-01-05 19:02:51 -05:00
|
|
|
return universe
|
2015-05-08 14:20:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _partition(pathnames, normpath=True):
|
|
|
|
|
"""
|
|
|
|
|
Splits 'pathnames' into two separate lists based on whether they
|
|
|
|
|
use a glob pattern.
|
|
|
|
|
|
|
|
|
|
Returns the pair (non-globbed pathnames, globbed pathnames).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
verbatim = []
|
|
|
|
|
globbed = []
|
|
|
|
|
|
|
|
|
|
for pathname in pathnames:
|
|
|
|
|
if globstar.is_glob_pattern(pathname):
|
|
|
|
|
globbed.append(pathname)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Normalize 'pathname' so exact string comparison can be used later.
|
|
|
|
|
if normpath:
|
|
|
|
|
pathname = os.path.normpath(pathname)
|
|
|
|
|
verbatim.append(pathname)
|
|
|
|
|
|
|
|
|
|
return (verbatim, globbed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pop_all(kind, universe, iterable, validate=True):
|
|
|
|
|
"""
|
|
|
|
|
Removes all elements of 'iterable' from 'universe' and returns them.
|
|
|
|
|
|
|
|
|
|
If 'validate' is true, then a ValueError is raised if a element
|
|
|
|
|
would be removed multiple times, or if an element of 'iterable' does
|
|
|
|
|
not appear in 'universe' at all.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
members = set()
|
|
|
|
|
|
|
|
|
|
for elem in iterable:
|
|
|
|
|
if validate and elem in members:
|
|
|
|
|
raise ValueError("%s '%s' specified multiple times" % (kind, elem))
|
|
|
|
|
|
|
|
|
|
if elem in universe:
|
|
|
|
|
universe.remove(elem)
|
|
|
|
|
members.add(elem)
|
|
|
|
|
elif validate:
|
|
|
|
|
raise ValueError("Unrecognized %s '%s'" % (kind, elem))
|
|
|
|
|
|
|
|
|
|
return members
|