blob: 82f2dec3cc4a68cdb25144c84305992c26862d3a [file] [log] [blame] [edit]
#!/usr/bin/python
#
# Copyright 2008 Google Inc. All Rights Reserved.
"""
This utility allows for easy updating, removing and importing
of tests into the autotest_web afe_autotests table.
Example of updating client side tests:
./test_importer.py -t /usr/local/autotest/client/tests
If, for example, not all of your control files adhere to the standard outlined
at http://autotest.kernel.org/wiki/ControlRequirements, you can force options:
./test_importer.py --test-type server -t /usr/local/autotest/server/tests
You would need to pass --add-noncompliant to include such control files,
however. An easy way to check for compliance is to run in dry mode:
./test_importer.py --dry-run -t /usr/local/autotest/server/tests/mytest
Running with no options is equivalent to --add-all --db-clear-tests.
Most options should be fairly self explanatory, use --help to display them.
"""
import common
import logging, re, os, sys, optparse, compiler
from autotest_lib.frontend import setup_django_environment
from autotest_lib.frontend.afe import models
from autotest_lib.client.common_lib import control_data
from autotest_lib.client.common_lib import logging_config, logging_manager
class TestImporterLoggingConfig(logging_config.LoggingConfig):
#pylint: disable-msg=C0111
def configure_logging(self, results_dir=None, verbose=False):
super(TestImporterLoggingConfig, self).configure_logging(
use_console=True,
verbose=verbose)
# Global
DRY_RUN = False
DEPENDENCIES_NOT_FOUND = set()
def update_all(autotest_dir, add_noncompliant, add_experimental):
"""
Function to scan through all tests and add them to the database.
This function invoked when no parameters supplied to the command line.
It 'synchronizes' the test database with the current contents of the
client and server test directories. When test code is discovered
in the file system new tests may be added to the db. Likewise,
if test code is not found in the filesystem, tests may be removed
from the db. The base test directories are hard-coded to client/tests,
client/site_tests, server/tests and server/site_tests.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
@param add_noncompliant: attempt adding test with invalid control files.
@param add_experimental: add tests with experimental attribute set.
"""
for path in [ 'server/tests', 'server/site_tests', 'client/tests',
'client/site_tests']:
test_path = os.path.join(autotest_dir, path)
if not os.path.exists(test_path):
continue
logging.info("Scanning %s", test_path)
tests = []
tests = get_tests_from_fs(test_path, "^control.*",
add_noncompliant=add_noncompliant)
update_tests_in_db(tests, add_experimental=add_experimental,
add_noncompliant=add_noncompliant,
autotest_dir=autotest_dir)
test_suite_path = os.path.join(autotest_dir, 'test_suites')
if os.path.exists(test_suite_path):
logging.info("Scanning %s", test_suite_path)
tests = get_tests_from_fs(test_suite_path, '.*',
add_noncompliant=add_noncompliant)
update_tests_in_db(tests, add_experimental=add_experimental,
add_noncompliant=add_noncompliant,
autotest_dir=autotest_dir)
profilers_path = os.path.join(autotest_dir, "client/profilers")
if os.path.exists(profilers_path):
logging.info("Scanning %s", profilers_path)
profilers = get_tests_from_fs(profilers_path, '.*py$')
update_profilers_in_db(profilers, add_noncompliant=add_noncompliant,
description='NA')
# Clean bad db entries
db_clean_broken(autotest_dir)
def update_samples(autotest_dir, add_noncompliant, add_experimental):
"""
Add only sample tests to the database from the filesystem.
This function invoked when -S supplied on command line.
Only adds tests to the database - does not delete any.
Samples tests are formatted slightly differently than other tests.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
@param add_noncompliant: attempt adding test with invalid control files.
@param add_experimental: add tests with experimental attribute set.
"""
sample_path = os.path.join(autotest_dir, 'server/samples')
if os.path.exists(sample_path):
logging.info("Scanning %s", sample_path)
tests = get_tests_from_fs(sample_path, '.*srv$',
add_noncompliant=add_noncompliant)
update_tests_in_db(tests, add_experimental=add_experimental,
add_noncompliant=add_noncompliant,
autotest_dir=autotest_dir)
def db_clean_broken(autotest_dir):
"""
Remove tests from autotest_web that do not have valid control files
This function invoked when -c supplied on the command line and when
running update_all(). Removes tests from database which are not
found in the filesystem. Also removes profilers which are just
a special case of tests.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
"""
for test in models.Test.objects.all():
full_path = os.path.join(autotest_dir, test.path)
if not os.path.isfile(full_path):
logging.info("Removing %s", test.path)
_log_or_execute(repr(test), test.delete)
# Find profilers that are no longer present
for profiler in models.Profiler.objects.all():
full_path = os.path.join(autotest_dir, "client", "profilers",
profiler.name)
if not os.path.exists(full_path):
logging.info("Removing %s", profiler.name)
_log_or_execute(repr(profiler), profiler.delete)
def db_clean_all(autotest_dir):
"""
Remove all tests from autotest_web - very destructive
This function invoked when -C supplied on the command line.
Removes ALL tests from the database.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
"""
for test in models.Test.objects.all():
full_path = os.path.join(autotest_dir, test.path)
logging.info("Removing %s", test.path)
_log_or_execute(repr(test), test.delete)
# Find profilers that are no longer present
for profiler in models.Profiler.objects.all():
full_path = os.path.join(autotest_dir, "client", "profilers",
profiler.name)
logging.info("Removing %s", profiler.name)
_log_or_execute(repr(profiler), profiler.delete)
def update_profilers_in_db(profilers, description='NA',
add_noncompliant=False):
"""
Add only profilers to the database from the filesystem.
This function invoked when -p supplied on command line.
Only adds profilers to the database - does not delete any.
Profilers are formatted slightly differently than tests.
@param profilers: list of profilers found in the file system.
@param description: simple text to satisfy docstring.
@param add_noncompliant: attempt adding test with invalid control files.
"""
for profiler in profilers:
name = os.path.basename(profiler)
if name.endswith('.py'):
name = name[:-3]
if not profilers[profiler]:
if add_noncompliant:
doc = description
else:
logging.warning("Skipping %s, missing docstring", profiler)
continue
else:
doc = profilers[profiler]
model = models.Profiler.objects.get_or_create(name=name)[0]
model.description = doc
_log_or_execute(repr(model), model.save)
def _set_attributes_custom(test, data):
# We set the test name to the dirname of the control file.
test_new_name = test.path.split('/')
if test_new_name[-1] == 'control' or test_new_name[-1] == 'control.srv':
test.name = test_new_name[-2]
else:
control_name = "%s:%s"
control_name %= (test_new_name[-2],
test_new_name[-1])
test.name = re.sub('control.*\.', '', control_name)
# We set verify to always False (0).
test.run_verify = 0
if hasattr(data, 'test_parameters'):
for para_name in data.test_parameters:
test_parameter = models.TestParameter.objects.get_or_create(
test=test, name=para_name)[0]
test_parameter.save()
def update_tests_in_db(tests, dry_run=False, add_experimental=False,
add_noncompliant=False, autotest_dir=None):
"""
Scans through all tests and add them to the database.
This function invoked when -t supplied and for update_all.
When test code is discovered in the file system new tests may be added
@param tests: list of tests found in the filesystem.
@param dry_run: not used at this time.
@param add_experimental: add tests with experimental attribute set.
@param add_noncompliant: attempt adding test with invalid control files.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
"""
for test in tests:
new_test = models.Test.objects.get_or_create(
path=test.replace(autotest_dir, '').lstrip('/'))[0]
logging.info("Processing %s", new_test.path)
# Set the test's attributes
data = tests[test]
_set_attributes_clean(new_test, data)
# Custom Attribute Update
_set_attributes_custom(new_test, data)
# This only takes place if --add-noncompliant is provided on the CLI
if not new_test.name:
test_new_test = test.split('/')
if test_new_test[-1] == 'control':
new_test.name = test_new_test[-2]
else:
control_name = "%s:%s"
control_name %= (test_new_test[-2],
test_new_test[-1])
new_test.name = control_name.replace('control.', '')
# Experimental Check
if not add_experimental and new_test.experimental:
continue
_log_or_execute(repr(new_test), new_test.save)
add_label_dependencies(new_test)
# save TestParameter
for para_name in data.test_parameters:
test_parameter = models.TestParameter.objects.get_or_create(
test=new_test, name=para_name)[0]
test_parameter.save()
def _set_attributes_clean(test, data):
"""
First pass sets the attributes of the Test object from file system.
@param test: a test object to be populated for the database.
@param data: object with test data from the file system.
"""
test_time = { 'short' : 1,
'medium' : 2,
'long' : 3, }
string_attributes = ('name', 'author', 'test_class', 'test_category',
'test_category', 'sync_count')
for attribute in string_attributes:
setattr(test, attribute, getattr(data, attribute))
test.description = data.doc
test.dependencies = ", ".join(data.dependencies)
try:
test.test_type = control_data.CONTROL_TYPE.get_value(data.test_type)
except AttributeError:
raise Exception('Unknown test_type %s for test %s', data.test_type,
data.name)
int_attributes = ('experimental', 'run_verify')
for attribute in int_attributes:
setattr(test, attribute, int(getattr(data, attribute)))
try:
test.test_time = int(data.time)
if test.test_time < 1 or test.time > 3:
raise Exception('Incorrect number %d for time' % test.time)
except ValueError:
pass
if not test.test_time and str == type(data.time):
test.test_time = test_time[data.time.lower()]
# TODO(crbug.com/873716) DEPRECATED. Remove entirely from the models.
test.test_retry = 0
def add_label_dependencies(test):
"""
Add proper many-to-many relationships from DEPENDENCIES field.
@param test: test object for the database.
"""
# clear out old relationships
_log_or_execute(repr(test), test.dependency_labels.clear,
subject='clear dependencies from')
for label_name in test.dependencies.split(','):
label_name = label_name.strip().lower()
if not label_name:
continue
try:
label = models.Label.objects.get(name=label_name)
except models.Label.DoesNotExist:
log_dependency_not_found(label_name)
continue
_log_or_execute(repr(label), test.dependency_labels.add, label,
subject='add dependency to %s' % test.name)
def log_dependency_not_found(label_name):
"""
Exception processing when label not found in database.
@param label_name: from test dependencies.
"""
if label_name in DEPENDENCIES_NOT_FOUND:
return
logging.info("Dependency %s not found", label_name)
DEPENDENCIES_NOT_FOUND.add(label_name)
def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False):
"""
Find control files in file system and load a list with their info.
@param parent_dir: directory to search recursively.
@param control_pattern: name format of control file.
@param add_noncompliant: ignore control file parse errors.
@return dictionary of the form: tests[file_path] = parsed_object
"""
tests = {}
profilers = False
if 'client/profilers' in parent_dir:
profilers = True
for dir in [ parent_dir ]:
files = recursive_walk(dir, control_pattern)
for file in files:
if '__init__.py' in file or '.svn' in file:
continue
if not profilers:
if not add_noncompliant:
try:
found_test = control_data.parse_control(file,
raise_warnings=True)
tests[file] = found_test
except control_data.ControlVariableException, e:
logging.warning("Skipping %s\n%s", file, e)
except Exception, e:
logging.error("Bad %s\n%s", file, e)
else:
found_test = control_data.parse_control(file)
tests[file] = found_test
else:
tests[file] = compiler.parseFile(file).doc
return tests
def recursive_walk(path, wildcard):
"""
Recursively go through a directory.
This function invoked by get_tests_from_fs().
@param path: base directory to start search.
@param wildcard: name format to match.
@return A list of files that match wildcard
"""
files = []
directories = [ path ]
while len(directories)>0:
directory = directories.pop()
for name in os.listdir(directory):
fullpath = os.path.join(directory, name)
if os.path.isfile(fullpath):
# if we are a control file
if re.search(wildcard, name):
files.append(fullpath)
elif os.path.isdir(fullpath):
directories.append(fullpath)
return files
def _log_or_execute(content, func, *args, **kwargs):
"""
Log a message if dry_run is enabled, or execute the given function.
Relies on the DRY_RUN global variable.
@param content: the actual log message.
@param func: function to execute if dry_run is not enabled.
@param subject: (Optional) The type of log being written. Defaults to
the name of the provided function.
"""
subject = kwargs.get('subject', func.__name__)
if DRY_RUN:
logging.info("Would %s: %s", subject, content)
else:
func(*args)
def _create_whitelist_set(whitelist_path):
"""
Create a set with contents from a whitelist file for membership testing.
@param whitelist_path: full path to the whitelist file.
@return set with files listed one/line - newlines included.
"""
f = open(whitelist_path, 'r')
whitelist_set = set([line.strip() for line in f])
f.close()
return whitelist_set
def update_from_whitelist(whitelist_set, add_experimental, add_noncompliant,
autotest_dir):
"""
Scans through all tests in the whitelist and add them to the database.
This function invoked when -w supplied.
@param whitelist_set: set of tests in full-path form from a whitelist.
@param add_experimental: add tests with experimental attribute set.
@param add_noncompliant: attempt adding test with invalid control files.
@param autotest_dir: prepended to path strings (/usr/local/autotest).
"""
tests = {}
profilers = {}
for file_path in whitelist_set:
if file_path.find('client/profilers') == -1:
try:
found_test = control_data.parse_control(file_path,
raise_warnings=True)
tests[file_path] = found_test
except control_data.ControlVariableException, e:
logging.warning("Skipping %s\n%s", file, e)
else:
profilers[file_path] = compiler.parseFile(file_path).doc
if len(tests) > 0:
update_tests_in_db(tests, add_experimental=add_experimental,
add_noncompliant=add_noncompliant,
autotest_dir=autotest_dir)
if len(profilers) > 0:
update_profilers_in_db(profilers, add_noncompliant=add_noncompliant,
description='NA')
def main(argv):
"""Main function
@param argv: List of command line parameters.
"""
global DRY_RUN
parser = optparse.OptionParser()
parser.add_option('-c', '--db-clean-tests',
dest='clean_tests', action='store_true',
default=False,
help='Clean client and server tests with invalid control files')
parser.add_option('-C', '--db-clear-all-tests',
dest='clear_all_tests', action='store_true',
default=False,
help='Clear ALL client and server tests')
parser.add_option('-d', '--dry-run',
dest='dry_run', action='store_true', default=False,
help='Dry run for operation')
parser.add_option('-A', '--add-all',
dest='add_all', action='store_true',
default=False,
help='Add site_tests, tests, and test_suites')
parser.add_option('-S', '--add-samples',
dest='add_samples', action='store_true',
default=False,
help='Add samples.')
parser.add_option('-E', '--add-experimental',
dest='add_experimental', action='store_true',
default=True,
help='Add experimental tests to frontend, works only '
'with -A (--add-all) option')
parser.add_option('-N', '--add-noncompliant',
dest='add_noncompliant', action='store_true',
default=False,
help='Add non-compliant tests (i.e. tests that do not '
'define all required control variables), works '
'only with -A (--add-all) option')
parser.add_option('-p', '--profile-dir', dest='profile_dir',
help='Directory to recursively check for profiles')
parser.add_option('-t', '--tests-dir', dest='tests_dir',
help='Directory to recursively check for control.*')
parser.add_option('-r', '--control-pattern', dest='control_pattern',
default='^control.*',
help='The pattern to look for in directories for control files')
parser.add_option('-v', '--verbose',
dest='verbose', action='store_true', default=False,
help='Run in verbose mode')
parser.add_option('-w', '--whitelist-file', dest='whitelist_file',
help='Filename for list of test names that must match')
parser.add_option('-z', '--autotest-dir', dest='autotest_dir',
default=os.path.join(os.path.dirname(__file__), '..'),
help='Autotest directory root')
options, args = parser.parse_args()
logging_manager.configure_logging(TestImporterLoggingConfig(),
verbose=options.verbose)
DRY_RUN = options.dry_run
if DRY_RUN:
logging.getLogger().setLevel(logging.WARN)
if len(argv) > 1 and options.add_noncompliant and not options.add_all:
logging.error('-N (--add-noncompliant) must be ran with option -A '
'(--add-All).')
return 1
if len(argv) > 1 and options.add_experimental and not options.add_all:
logging.error('-E (--add-experimental) must be ran with option -A '
'(--add-All).')
return 1
# Make sure autotest_dir is the absolute path
options.autotest_dir = os.path.abspath(options.autotest_dir)
if len(args) > 0:
logging.error("Invalid option(s) provided: %s", args)
parser.print_help()
return 1
if options.verbose:
logging.getLogger().setLevel(logging.DEBUG)
if len(argv) == 1 or (len(argv) == 2 and options.verbose):
update_all(options.autotest_dir, options.add_noncompliant,
options.add_experimental)
db_clean_broken(options.autotest_dir)
return 0
if options.clear_all_tests:
if (options.clean_tests or options.add_all or options.add_samples or
options.add_noncompliant):
logging.error(
"Can only pass --autotest-dir, --dry-run and --verbose with "
"--db-clear-all-tests")
return 1
db_clean_all(options.autotest_dir)
whitelist_set = None
if options.whitelist_file:
if options.add_all:
logging.error("Cannot pass both --add-all and --whitelist-file")
return 1
whitelist_path = os.path.abspath(options.whitelist_file)
if not os.path.isfile(whitelist_path):
logging.error("--whitelist-file (%s) not found", whitelist_path)
return 1
logging.info("Using whitelist file %s", whitelist_path)
whitelist_set = _create_whitelist_set(whitelist_path)
update_from_whitelist(whitelist_set,
add_experimental=options.add_experimental,
add_noncompliant=options.add_noncompliant,
autotest_dir=options.autotest_dir)
if options.add_all:
update_all(options.autotest_dir, options.add_noncompliant,
options.add_experimental)
if options.add_samples:
update_samples(options.autotest_dir, options.add_noncompliant,
options.add_experimental)
if options.tests_dir:
options.tests_dir = os.path.abspath(options.tests_dir)
tests = get_tests_from_fs(options.tests_dir, options.control_pattern,
add_noncompliant=options.add_noncompliant)
update_tests_in_db(tests, add_experimental=options.add_experimental,
add_noncompliant=options.add_noncompliant,
autotest_dir=options.autotest_dir)
if options.profile_dir:
profilers = get_tests_from_fs(options.profile_dir, '.*py$')
update_profilers_in_db(profilers,
add_noncompliant=options.add_noncompliant,
description='NA')
if options.clean_tests:
db_clean_broken(options.autotest_dir)
if __name__ == "__main__":
sys.exit(main(sys.argv))