• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# Copyright 2008 Google Inc. All Rights Reserved.
4"""
5This utility allows for easy updating, removing and importing
6of tests into the autotest_web afe_autotests table.
7
8Example of updating client side tests:
9./test_importer.py -t /usr/local/autotest/client/tests
10
11If, for example, not all of your control files adhere to the standard outlined
12at http://autotest.kernel.org/wiki/ControlRequirements, you can force options:
13
14./test_importer.py --test-type server -t /usr/local/autotest/server/tests
15
16You would need to pass --add-noncompliant to include such control files,
17however.  An easy way to check for compliance is to run in dry mode:
18
19./test_importer.py --dry-run -t /usr/local/autotest/server/tests/mytest
20
21Or to check a single control file, you can use check_control_file_vars.py.
22
23Running with no options is equivalent to --add-all --db-clear-tests.
24
25Most options should be fairly self explanatory, use --help to display them.
26"""
27
28
29import common
30import logging, re, os, sys, optparse, compiler
31
32from autotest_lib.frontend import setup_django_environment
33from autotest_lib.frontend.afe import models
34from autotest_lib.client.common_lib import control_data, utils
35from autotest_lib.client.common_lib import logging_config, logging_manager
36
37
38class TestImporterLoggingConfig(logging_config.LoggingConfig):
39    #pylint: disable-msg=C0111
40    def configure_logging(self, results_dir=None, verbose=False):
41        super(TestImporterLoggingConfig, self).configure_logging(
42                                                               use_console=True,
43                                                               verbose=verbose)
44
45
46# Global
47DRY_RUN = False
48DEPENDENCIES_NOT_FOUND = set()
49
50
51def update_all(autotest_dir, add_noncompliant, add_experimental):
52    """
53    Function to scan through all tests and add them to the database.
54
55    This function invoked when no parameters supplied to the command line.
56    It 'synchronizes' the test database with the current contents of the
57    client and server test directories.  When test code is discovered
58    in the file system new tests may be added to the db.  Likewise,
59    if test code is not found in the filesystem, tests may be removed
60    from the db.  The base test directories are hard-coded to client/tests,
61    client/site_tests, server/tests and server/site_tests.
62
63    @param autotest_dir: prepended to path strings (/usr/local/autotest).
64    @param add_noncompliant: attempt adding test with invalid control files.
65    @param add_experimental: add tests with experimental attribute set.
66    """
67    for path in [ 'server/tests', 'server/site_tests', 'client/tests',
68                  'client/site_tests']:
69        test_path = os.path.join(autotest_dir, path)
70        if not os.path.exists(test_path):
71            continue
72        logging.info("Scanning %s", test_path)
73        tests = []
74        tests = get_tests_from_fs(test_path, "^control.*",
75                                 add_noncompliant=add_noncompliant)
76        update_tests_in_db(tests, add_experimental=add_experimental,
77                           add_noncompliant=add_noncompliant,
78                           autotest_dir=autotest_dir)
79    test_suite_path = os.path.join(autotest_dir, 'test_suites')
80    if os.path.exists(test_suite_path):
81        logging.info("Scanning %s", test_suite_path)
82        tests = get_tests_from_fs(test_suite_path, '.*',
83                                 add_noncompliant=add_noncompliant)
84        update_tests_in_db(tests, add_experimental=add_experimental,
85                           add_noncompliant=add_noncompliant,
86                           autotest_dir=autotest_dir)
87
88    profilers_path = os.path.join(autotest_dir, "client/profilers")
89    if os.path.exists(profilers_path):
90        logging.info("Scanning %s", profilers_path)
91        profilers = get_tests_from_fs(profilers_path, '.*py$')
92        update_profilers_in_db(profilers, add_noncompliant=add_noncompliant,
93                               description='NA')
94    # Clean bad db entries
95    db_clean_broken(autotest_dir)
96
97
98def update_samples(autotest_dir, add_noncompliant, add_experimental):
99    """
100    Add only sample tests to the database from the filesystem.
101
102    This function invoked when -S supplied on command line.
103    Only adds tests to the database - does not delete any.
104    Samples tests are formatted slightly differently than other tests.
105
106    @param autotest_dir: prepended to path strings (/usr/local/autotest).
107    @param add_noncompliant: attempt adding test with invalid control files.
108    @param add_experimental: add tests with experimental attribute set.
109    """
110    sample_path = os.path.join(autotest_dir, 'server/samples')
111    if os.path.exists(sample_path):
112        logging.info("Scanning %s", sample_path)
113        tests = get_tests_from_fs(sample_path, '.*srv$',
114                                  add_noncompliant=add_noncompliant)
115        update_tests_in_db(tests, add_experimental=add_experimental,
116                           add_noncompliant=add_noncompliant,
117                           autotest_dir=autotest_dir)
118
119
120def db_clean_broken(autotest_dir):
121    """
122    Remove tests from autotest_web that do not have valid control files
123
124    This function invoked when -c supplied on the command line and when
125    running update_all().  Removes tests from database which are not
126    found in the filesystem.  Also removes profilers which are just
127    a special case of tests.
128
129    @param autotest_dir: prepended to path strings (/usr/local/autotest).
130    """
131    for test in models.Test.objects.all():
132        full_path = os.path.join(autotest_dir, test.path)
133        if not os.path.isfile(full_path):
134            logging.info("Removing %s", test.path)
135            _log_or_execute(repr(test), test.delete)
136
137    # Find profilers that are no longer present
138    for profiler in models.Profiler.objects.all():
139        full_path = os.path.join(autotest_dir, "client", "profilers",
140                                 profiler.name)
141        if not os.path.exists(full_path):
142            logging.info("Removing %s", profiler.name)
143            _log_or_execute(repr(profiler), profiler.delete)
144
145
146def db_clean_all(autotest_dir):
147    """
148    Remove all tests from autotest_web - very destructive
149
150    This function invoked when -C supplied on the command line.
151    Removes ALL tests from the database.
152
153    @param autotest_dir: prepended to path strings (/usr/local/autotest).
154    """
155    for test in models.Test.objects.all():
156        full_path = os.path.join(autotest_dir, test.path)
157        logging.info("Removing %s", test.path)
158        _log_or_execute(repr(test), test.delete)
159
160    # Find profilers that are no longer present
161    for profiler in models.Profiler.objects.all():
162        full_path = os.path.join(autotest_dir, "client", "profilers",
163                                 profiler.name)
164        logging.info("Removing %s", profiler.name)
165        _log_or_execute(repr(profiler), profiler.delete)
166
167
168def update_profilers_in_db(profilers, description='NA',
169                           add_noncompliant=False):
170    """
171    Add only profilers to the database from the filesystem.
172
173    This function invoked when -p supplied on command line.
174    Only adds profilers to the database - does not delete any.
175    Profilers are formatted slightly differently than tests.
176
177    @param profilers: list of profilers found in the file system.
178    @param description: simple text to satisfy docstring.
179    @param add_noncompliant: attempt adding test with invalid control files.
180    """
181    for profiler in profilers:
182        name = os.path.basename(profiler)
183        if name.endswith('.py'):
184            name = name[:-3]
185        if not profilers[profiler]:
186            if add_noncompliant:
187                doc = description
188            else:
189                logging.warning("Skipping %s, missing docstring", profiler)
190                continue
191        else:
192            doc = profilers[profiler]
193
194        model = models.Profiler.objects.get_or_create(name=name)[0]
195        model.description = doc
196        _log_or_execute(repr(model), model.save)
197
198
199def update_tests_in_db(tests, dry_run=False, add_experimental=False,
200                       add_noncompliant=False, autotest_dir=None):
201    """
202    Scans through all tests and add them to the database.
203
204    This function invoked when -t supplied and for update_all.
205    When test code is discovered in the file system new tests may be added
206
207    @param tests: list of tests found in the filesystem.
208    @param dry_run: not used at this time.
209    @param add_experimental: add tests with experimental attribute set.
210    @param add_noncompliant: attempt adding test with invalid control files.
211    @param autotest_dir: prepended to path strings (/usr/local/autotest).
212    """
213    site_set_attributes_module = utils.import_site_module(
214        __file__, 'autotest_lib.utils.site_test_importer_attributes')
215
216    for test in tests:
217        new_test = models.Test.objects.get_or_create(
218                path=test.replace(autotest_dir, '').lstrip('/'))[0]
219        logging.info("Processing %s", new_test.path)
220
221        # Set the test's attributes
222        data = tests[test]
223        _set_attributes_clean(new_test, data)
224
225        # Custom Attribute Update
226        if site_set_attributes_module:
227            site_set_attributes_module._set_attributes_custom(new_test, data)
228
229        # This only takes place if --add-noncompliant is provided on the CLI
230        if not new_test.name:
231            test_new_test = test.split('/')
232            if test_new_test[-1] == 'control':
233                new_test.name = test_new_test[-2]
234            else:
235                control_name = "%s:%s"
236                control_name %= (test_new_test[-2],
237                                 test_new_test[-1])
238                new_test.name = control_name.replace('control.', '')
239
240        # Experimental Check
241        if not add_experimental and new_test.experimental:
242            continue
243
244        _log_or_execute(repr(new_test), new_test.save)
245        add_label_dependencies(new_test)
246
247        # save TestParameter
248        for para_name in data.test_parameters:
249            test_parameter = models.TestParameter.objects.get_or_create(
250                test=new_test, name=para_name)[0]
251            test_parameter.save()
252
253
254def _set_attributes_clean(test, data):
255    """
256    First pass sets the attributes of the Test object from file system.
257
258    @param test: a test object to be populated for the database.
259    @param data: object with test data from the file system.
260    """
261    test_time = { 'short' : 1,
262                  'medium' : 2,
263                  'long' : 3, }
264
265
266    string_attributes = ('name', 'author', 'test_class', 'test_category',
267                         'test_category', 'sync_count')
268    for attribute in string_attributes:
269        setattr(test, attribute, getattr(data, attribute))
270
271    test.description = data.doc
272    test.dependencies = ", ".join(data.dependencies)
273
274    try:
275        test.test_type = control_data.CONTROL_TYPE.get_value(data.test_type)
276    except AttributeError:
277        raise Exception('Unknown test_type %s for test %s', data.test_type,
278                        data.name)
279
280    int_attributes = ('experimental', 'run_verify')
281    for attribute in int_attributes:
282        setattr(test, attribute, int(getattr(data, attribute)))
283
284    try:
285        test.test_time = int(data.time)
286        if test.test_time < 1 or test.time > 3:
287            raise Exception('Incorrect number %d for time' % test.time)
288    except ValueError:
289        pass
290
291    if not test.test_time and str == type(data.time):
292        test.test_time = test_time[data.time.lower()]
293
294    test.test_retry = data.retries
295
296
297def add_label_dependencies(test):
298    """
299    Add proper many-to-many relationships from DEPENDENCIES field.
300
301    @param test: test object for the database.
302    """
303
304    # clear out old relationships
305    _log_or_execute(repr(test), test.dependency_labels.clear,
306                    subject='clear dependencies from')
307
308    for label_name in test.dependencies.split(','):
309        label_name = label_name.strip().lower()
310        if not label_name:
311            continue
312
313        try:
314            label = models.Label.objects.get(name=label_name)
315        except models.Label.DoesNotExist:
316            log_dependency_not_found(label_name)
317            continue
318
319        _log_or_execute(repr(label), test.dependency_labels.add, label,
320                        subject='add dependency to %s' % test.name)
321
322
323def log_dependency_not_found(label_name):
324    """
325    Exception processing when label not found in database.
326
327    @param label_name: from test dependencies.
328    """
329    if label_name in DEPENDENCIES_NOT_FOUND:
330        return
331    logging.info("Dependency %s not found", label_name)
332    DEPENDENCIES_NOT_FOUND.add(label_name)
333
334
335def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False):
336    """
337    Find control files in file system and load a list with their info.
338
339    @param parent_dir: directory to search recursively.
340    @param control_pattern: name format of control file.
341    @param add_noncompliant: ignore control file parse errors.
342
343    @return dictionary of the form: tests[file_path] = parsed_object
344    """
345    tests = {}
346    profilers = False
347    if 'client/profilers' in parent_dir:
348        profilers = True
349    for dir in [ parent_dir ]:
350        files = recursive_walk(dir, control_pattern)
351        for file in files:
352            if '__init__.py' in file or '.svn' in file:
353                continue
354            if not profilers:
355                if not add_noncompliant:
356                    try:
357                        found_test = control_data.parse_control(file,
358                                                            raise_warnings=True)
359                        tests[file] = found_test
360                    except control_data.ControlVariableException, e:
361                        logging.warning("Skipping %s\n%s", file, e)
362                    except Exception, e:
363                        logging.error("Bad %s\n%s", file, e)
364                else:
365                    found_test = control_data.parse_control(file)
366                    tests[file] = found_test
367            else:
368                tests[file] = compiler.parseFile(file).doc
369    return tests
370
371
372def recursive_walk(path, wildcard):
373    """
374    Recursively go through a directory.
375
376    This function invoked by get_tests_from_fs().
377
378    @param path: base directory to start search.
379    @param wildcard: name format to match.
380
381    @return A list of files that match wildcard
382    """
383    files = []
384    directories = [ path ]
385    while len(directories)>0:
386        directory = directories.pop()
387        for name in os.listdir(directory):
388            fullpath = os.path.join(directory, name)
389            if os.path.isfile(fullpath):
390                # if we are a control file
391                if re.search(wildcard, name):
392                    files.append(fullpath)
393            elif os.path.isdir(fullpath):
394                directories.append(fullpath)
395    return files
396
397
398def _log_or_execute(content, func, *args, **kwargs):
399    """
400    Log a message if dry_run is enabled, or execute the given function.
401
402    Relies on the DRY_RUN global variable.
403
404    @param content: the actual log message.
405    @param func: function to execute if dry_run is not enabled.
406    @param subject: (Optional) The type of log being written. Defaults to
407                     the name of the provided function.
408    """
409    subject = kwargs.get('subject', func.__name__)
410
411    if DRY_RUN:
412        logging.info("Would %s: %s",  subject, content)
413    else:
414        func(*args)
415
416
417def _create_whitelist_set(whitelist_path):
418    """
419    Create a set with contents from a whitelist file for membership testing.
420
421    @param whitelist_path: full path to the whitelist file.
422
423    @return set with files listed one/line - newlines included.
424    """
425    f = open(whitelist_path, 'r')
426    whitelist_set = set([line.strip() for line in f])
427    f.close()
428    return whitelist_set
429
430
431def update_from_whitelist(whitelist_set, add_experimental, add_noncompliant,
432                          autotest_dir):
433    """
434    Scans through all tests in the whitelist and add them to the database.
435
436    This function invoked when -w supplied.
437
438    @param whitelist_set: set of tests in full-path form from a whitelist.
439    @param add_experimental: add tests with experimental attribute set.
440    @param add_noncompliant: attempt adding test with invalid control files.
441    @param autotest_dir: prepended to path strings (/usr/local/autotest).
442    """
443    tests = {}
444    profilers = {}
445    for file_path in whitelist_set:
446        if file_path.find('client/profilers') == -1:
447            try:
448                found_test = control_data.parse_control(file_path,
449                                                        raise_warnings=True)
450                tests[file_path] = found_test
451            except control_data.ControlVariableException, e:
452                logging.warning("Skipping %s\n%s", file, e)
453        else:
454            profilers[file_path] = compiler.parseFile(file_path).doc
455
456    if len(tests) > 0:
457        update_tests_in_db(tests, add_experimental=add_experimental,
458                           add_noncompliant=add_noncompliant,
459                           autotest_dir=autotest_dir)
460    if len(profilers) > 0:
461        update_profilers_in_db(profilers, add_noncompliant=add_noncompliant,
462                               description='NA')
463
464
465def main(argv):
466    """Main function
467    @param argv: List of command line parameters.
468    """
469
470    global DRY_RUN
471    parser = optparse.OptionParser()
472    parser.add_option('-c', '--db-clean-tests',
473                      dest='clean_tests', action='store_true',
474                      default=False,
475                help='Clean client and server tests with invalid control files')
476    parser.add_option('-C', '--db-clear-all-tests',
477                      dest='clear_all_tests', action='store_true',
478                      default=False,
479                help='Clear ALL client and server tests')
480    parser.add_option('-d', '--dry-run',
481                      dest='dry_run', action='store_true', default=False,
482                      help='Dry run for operation')
483    parser.add_option('-A', '--add-all',
484                      dest='add_all', action='store_true',
485                      default=False,
486                      help='Add site_tests, tests, and test_suites')
487    parser.add_option('-S', '--add-samples',
488                      dest='add_samples', action='store_true',
489                      default=False,
490                      help='Add samples.')
491    parser.add_option('-E', '--add-experimental',
492                      dest='add_experimental', action='store_true',
493                      default=True,
494                      help='Add experimental tests to frontend, works only '
495                           'with -A (--add-all) option')
496    parser.add_option('-N', '--add-noncompliant',
497                      dest='add_noncompliant', action='store_true',
498                      default=False,
499                      help='Add non-compliant tests (i.e. tests that do not '
500                           'define all required control variables), works '
501                           'only with -A (--add-all) option')
502    parser.add_option('-p', '--profile-dir', dest='profile_dir',
503                      help='Directory to recursively check for profiles')
504    parser.add_option('-t', '--tests-dir', dest='tests_dir',
505                      help='Directory to recursively check for control.*')
506    parser.add_option('-r', '--control-pattern', dest='control_pattern',
507                      default='^control.*',
508               help='The pattern to look for in directories for control files')
509    parser.add_option('-v', '--verbose',
510                      dest='verbose', action='store_true', default=False,
511                      help='Run in verbose mode')
512    parser.add_option('-w', '--whitelist-file', dest='whitelist_file',
513                      help='Filename for list of test names that must match')
514    parser.add_option('-z', '--autotest-dir', dest='autotest_dir',
515                      default=os.path.join(os.path.dirname(__file__), '..'),
516                      help='Autotest directory root')
517    options, args = parser.parse_args()
518
519    logging_manager.configure_logging(TestImporterLoggingConfig(),
520                                      verbose=options.verbose)
521
522    DRY_RUN = options.dry_run
523    if DRY_RUN:
524        logging.getLogger().setLevel(logging.WARN)
525
526    if len(argv) > 1 and options.add_noncompliant and not options.add_all:
527        logging.error('-N (--add-noncompliant) must be ran with option -A '
528                      '(--add-All).')
529        return 1
530
531    if len(argv) > 1 and options.add_experimental and not options.add_all:
532        logging.error('-E (--add-experimental) must be ran with option -A '
533                      '(--add-All).')
534        return 1
535
536    # Make sure autotest_dir is the absolute path
537    options.autotest_dir = os.path.abspath(options.autotest_dir)
538
539    if len(args) > 0:
540        logging.error("Invalid option(s) provided: %s", args)
541        parser.print_help()
542        return 1
543
544    if options.verbose:
545        logging.getLogger().setLevel(logging.DEBUG)
546
547    if len(argv) == 1 or (len(argv) == 2 and options.verbose):
548        update_all(options.autotest_dir, options.add_noncompliant,
549                   options.add_experimental)
550        db_clean_broken(options.autotest_dir)
551        return 0
552
553    if options.clear_all_tests:
554        if (options.clean_tests or options.add_all or options.add_samples or
555            options.add_noncompliant):
556            logging.error(
557                "Can only pass --autotest-dir, --dry-run and --verbose with "
558                "--db-clear-all-tests")
559            return 1
560        db_clean_all(options.autotest_dir)
561
562    whitelist_set = None
563    if options.whitelist_file:
564        if options.add_all:
565            logging.error("Cannot pass both --add-all and --whitelist-file")
566            return 1
567        whitelist_path = os.path.abspath(options.whitelist_file)
568        if not os.path.isfile(whitelist_path):
569            logging.error("--whitelist-file (%s) not found", whitelist_path)
570            return 1
571        logging.info("Using whitelist file %s", whitelist_path)
572        whitelist_set =  _create_whitelist_set(whitelist_path)
573        update_from_whitelist(whitelist_set,
574                              add_experimental=options.add_experimental,
575                              add_noncompliant=options.add_noncompliant,
576                              autotest_dir=options.autotest_dir)
577    if options.add_all:
578        update_all(options.autotest_dir, options.add_noncompliant,
579                   options.add_experimental)
580    if options.add_samples:
581        update_samples(options.autotest_dir, options.add_noncompliant,
582                       options.add_experimental)
583    if options.tests_dir:
584        options.tests_dir = os.path.abspath(options.tests_dir)
585        tests = get_tests_from_fs(options.tests_dir, options.control_pattern,
586                                  add_noncompliant=options.add_noncompliant)
587        update_tests_in_db(tests, add_experimental=options.add_experimental,
588                           add_noncompliant=options.add_noncompliant,
589                           autotest_dir=options.autotest_dir)
590    if options.profile_dir:
591        profilers = get_tests_from_fs(options.profile_dir, '.*py$')
592        update_profilers_in_db(profilers,
593                               add_noncompliant=options.add_noncompliant,
594                               description='NA')
595    if options.clean_tests:
596        db_clean_broken(options.autotest_dir)
597
598
599if __name__ == "__main__":
600    sys.exit(main(sys.argv))
601