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