• 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
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 _set_attributes_custom(test, data):
200    # We set the test name to the dirname of the control file.
201    test_new_name = test.path.split('/')
202    if test_new_name[-1] == 'control' or test_new_name[-1] == 'control.srv':
203        test.name = test_new_name[-2]
204    else:
205        control_name = "%s:%s"
206        control_name %= (test_new_name[-2],
207                         test_new_name[-1])
208        test.name = re.sub('control.*\.', '', control_name)
209
210    # We set verify to always False (0).
211    test.run_verify = 0
212
213    if hasattr(data, 'test_parameters'):
214        for para_name in data.test_parameters:
215            test_parameter = models.TestParameter.objects.get_or_create(
216                test=test, name=para_name)[0]
217            test_parameter.save()
218
219
220def update_tests_in_db(tests, dry_run=False, add_experimental=False,
221                       add_noncompliant=False, autotest_dir=None):
222    """
223    Scans through all tests and add them to the database.
224
225    This function invoked when -t supplied and for update_all.
226    When test code is discovered in the file system new tests may be added
227
228    @param tests: list of tests found in the filesystem.
229    @param dry_run: not used at this time.
230    @param add_experimental: add tests with experimental attribute set.
231    @param add_noncompliant: attempt adding test with invalid control files.
232    @param autotest_dir: prepended to path strings (/usr/local/autotest).
233    """
234    for test in tests:
235        new_test = models.Test.objects.get_or_create(
236                path=test.replace(autotest_dir, '').lstrip('/'))[0]
237        logging.info("Processing %s", new_test.path)
238
239        # Set the test's attributes
240        data = tests[test]
241        _set_attributes_clean(new_test, data)
242
243        # Custom Attribute Update
244        _set_attributes_custom(new_test, data)
245
246        # This only takes place if --add-noncompliant is provided on the CLI
247        if not new_test.name:
248            test_new_test = test.split('/')
249            if test_new_test[-1] == 'control':
250                new_test.name = test_new_test[-2]
251            else:
252                control_name = "%s:%s"
253                control_name %= (test_new_test[-2],
254                                 test_new_test[-1])
255                new_test.name = control_name.replace('control.', '')
256
257        # Experimental Check
258        if not add_experimental and new_test.experimental:
259            continue
260
261        _log_or_execute(repr(new_test), new_test.save)
262        add_label_dependencies(new_test)
263
264        # save TestParameter
265        for para_name in data.test_parameters:
266            test_parameter = models.TestParameter.objects.get_or_create(
267                test=new_test, name=para_name)[0]
268            test_parameter.save()
269
270
271def _set_attributes_clean(test, data):
272    """
273    First pass sets the attributes of the Test object from file system.
274
275    @param test: a test object to be populated for the database.
276    @param data: object with test data from the file system.
277    """
278    test_time = { 'short' : 1,
279                  'medium' : 2,
280                  'long' : 3, }
281
282
283    string_attributes = ('name', 'author', 'test_class', 'test_category',
284                         'test_category', 'sync_count')
285    for attribute in string_attributes:
286        setattr(test, attribute, getattr(data, attribute))
287
288    test.description = data.doc
289    test.dependencies = ", ".join(data.dependencies)
290
291    try:
292        test.test_type = control_data.CONTROL_TYPE.get_value(data.test_type)
293    except AttributeError:
294        raise Exception('Unknown test_type %s for test %s', data.test_type,
295                        data.name)
296
297    int_attributes = ('experimental', 'run_verify')
298    for attribute in int_attributes:
299        setattr(test, attribute, int(getattr(data, attribute)))
300
301    try:
302        test.test_time = int(data.time)
303        if test.test_time < 1 or test.time > 3:
304            raise Exception('Incorrect number %d for time' % test.time)
305    except ValueError:
306        pass
307
308    if not test.test_time and str == type(data.time):
309        test.test_time = test_time[data.time.lower()]
310
311    test.test_retry = data.retries
312
313
314def add_label_dependencies(test):
315    """
316    Add proper many-to-many relationships from DEPENDENCIES field.
317
318    @param test: test object for the database.
319    """
320
321    # clear out old relationships
322    _log_or_execute(repr(test), test.dependency_labels.clear,
323                    subject='clear dependencies from')
324
325    for label_name in test.dependencies.split(','):
326        label_name = label_name.strip().lower()
327        if not label_name:
328            continue
329
330        try:
331            label = models.Label.objects.get(name=label_name)
332        except models.Label.DoesNotExist:
333            log_dependency_not_found(label_name)
334            continue
335
336        _log_or_execute(repr(label), test.dependency_labels.add, label,
337                        subject='add dependency to %s' % test.name)
338
339
340def log_dependency_not_found(label_name):
341    """
342    Exception processing when label not found in database.
343
344    @param label_name: from test dependencies.
345    """
346    if label_name in DEPENDENCIES_NOT_FOUND:
347        return
348    logging.info("Dependency %s not found", label_name)
349    DEPENDENCIES_NOT_FOUND.add(label_name)
350
351
352def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False):
353    """
354    Find control files in file system and load a list with their info.
355
356    @param parent_dir: directory to search recursively.
357    @param control_pattern: name format of control file.
358    @param add_noncompliant: ignore control file parse errors.
359
360    @return dictionary of the form: tests[file_path] = parsed_object
361    """
362    tests = {}
363    profilers = False
364    if 'client/profilers' in parent_dir:
365        profilers = True
366    for dir in [ parent_dir ]:
367        files = recursive_walk(dir, control_pattern)
368        for file in files:
369            if '__init__.py' in file or '.svn' in file:
370                continue
371            if not profilers:
372                if not add_noncompliant:
373                    try:
374                        found_test = control_data.parse_control(file,
375                                                            raise_warnings=True)
376                        tests[file] = found_test
377                    except control_data.ControlVariableException, e:
378                        logging.warning("Skipping %s\n%s", file, e)
379                    except Exception, e:
380                        logging.error("Bad %s\n%s", file, e)
381                else:
382                    found_test = control_data.parse_control(file)
383                    tests[file] = found_test
384            else:
385                tests[file] = compiler.parseFile(file).doc
386    return tests
387
388
389def recursive_walk(path, wildcard):
390    """
391    Recursively go through a directory.
392
393    This function invoked by get_tests_from_fs().
394
395    @param path: base directory to start search.
396    @param wildcard: name format to match.
397
398    @return A list of files that match wildcard
399    """
400    files = []
401    directories = [ path ]
402    while len(directories)>0:
403        directory = directories.pop()
404        for name in os.listdir(directory):
405            fullpath = os.path.join(directory, name)
406            if os.path.isfile(fullpath):
407                # if we are a control file
408                if re.search(wildcard, name):
409                    files.append(fullpath)
410            elif os.path.isdir(fullpath):
411                directories.append(fullpath)
412    return files
413
414
415def _log_or_execute(content, func, *args, **kwargs):
416    """
417    Log a message if dry_run is enabled, or execute the given function.
418
419    Relies on the DRY_RUN global variable.
420
421    @param content: the actual log message.
422    @param func: function to execute if dry_run is not enabled.
423    @param subject: (Optional) The type of log being written. Defaults to
424                     the name of the provided function.
425    """
426    subject = kwargs.get('subject', func.__name__)
427
428    if DRY_RUN:
429        logging.info("Would %s: %s",  subject, content)
430    else:
431        func(*args)
432
433
434def _create_whitelist_set(whitelist_path):
435    """
436    Create a set with contents from a whitelist file for membership testing.
437
438    @param whitelist_path: full path to the whitelist file.
439
440    @return set with files listed one/line - newlines included.
441    """
442    f = open(whitelist_path, 'r')
443    whitelist_set = set([line.strip() for line in f])
444    f.close()
445    return whitelist_set
446
447
448def update_from_whitelist(whitelist_set, add_experimental, add_noncompliant,
449                          autotest_dir):
450    """
451    Scans through all tests in the whitelist and add them to the database.
452
453    This function invoked when -w supplied.
454
455    @param whitelist_set: set of tests in full-path form from a whitelist.
456    @param add_experimental: add tests with experimental attribute set.
457    @param add_noncompliant: attempt adding test with invalid control files.
458    @param autotest_dir: prepended to path strings (/usr/local/autotest).
459    """
460    tests = {}
461    profilers = {}
462    for file_path in whitelist_set:
463        if file_path.find('client/profilers') == -1:
464            try:
465                found_test = control_data.parse_control(file_path,
466                                                        raise_warnings=True)
467                tests[file_path] = found_test
468            except control_data.ControlVariableException, e:
469                logging.warning("Skipping %s\n%s", file, e)
470        else:
471            profilers[file_path] = compiler.parseFile(file_path).doc
472
473    if len(tests) > 0:
474        update_tests_in_db(tests, add_experimental=add_experimental,
475                           add_noncompliant=add_noncompliant,
476                           autotest_dir=autotest_dir)
477    if len(profilers) > 0:
478        update_profilers_in_db(profilers, add_noncompliant=add_noncompliant,
479                               description='NA')
480
481
482def main(argv):
483    """Main function
484    @param argv: List of command line parameters.
485    """
486
487    global DRY_RUN
488    parser = optparse.OptionParser()
489    parser.add_option('-c', '--db-clean-tests',
490                      dest='clean_tests', action='store_true',
491                      default=False,
492                help='Clean client and server tests with invalid control files')
493    parser.add_option('-C', '--db-clear-all-tests',
494                      dest='clear_all_tests', action='store_true',
495                      default=False,
496                help='Clear ALL client and server tests')
497    parser.add_option('-d', '--dry-run',
498                      dest='dry_run', action='store_true', default=False,
499                      help='Dry run for operation')
500    parser.add_option('-A', '--add-all',
501                      dest='add_all', action='store_true',
502                      default=False,
503                      help='Add site_tests, tests, and test_suites')
504    parser.add_option('-S', '--add-samples',
505                      dest='add_samples', action='store_true',
506                      default=False,
507                      help='Add samples.')
508    parser.add_option('-E', '--add-experimental',
509                      dest='add_experimental', action='store_true',
510                      default=True,
511                      help='Add experimental tests to frontend, works only '
512                           'with -A (--add-all) option')
513    parser.add_option('-N', '--add-noncompliant',
514                      dest='add_noncompliant', action='store_true',
515                      default=False,
516                      help='Add non-compliant tests (i.e. tests that do not '
517                           'define all required control variables), works '
518                           'only with -A (--add-all) option')
519    parser.add_option('-p', '--profile-dir', dest='profile_dir',
520                      help='Directory to recursively check for profiles')
521    parser.add_option('-t', '--tests-dir', dest='tests_dir',
522                      help='Directory to recursively check for control.*')
523    parser.add_option('-r', '--control-pattern', dest='control_pattern',
524                      default='^control.*',
525               help='The pattern to look for in directories for control files')
526    parser.add_option('-v', '--verbose',
527                      dest='verbose', action='store_true', default=False,
528                      help='Run in verbose mode')
529    parser.add_option('-w', '--whitelist-file', dest='whitelist_file',
530                      help='Filename for list of test names that must match')
531    parser.add_option('-z', '--autotest-dir', dest='autotest_dir',
532                      default=os.path.join(os.path.dirname(__file__), '..'),
533                      help='Autotest directory root')
534    options, args = parser.parse_args()
535
536    logging_manager.configure_logging(TestImporterLoggingConfig(),
537                                      verbose=options.verbose)
538
539    DRY_RUN = options.dry_run
540    if DRY_RUN:
541        logging.getLogger().setLevel(logging.WARN)
542
543    if len(argv) > 1 and options.add_noncompliant and not options.add_all:
544        logging.error('-N (--add-noncompliant) must be ran with option -A '
545                      '(--add-All).')
546        return 1
547
548    if len(argv) > 1 and options.add_experimental and not options.add_all:
549        logging.error('-E (--add-experimental) must be ran with option -A '
550                      '(--add-All).')
551        return 1
552
553    # Make sure autotest_dir is the absolute path
554    options.autotest_dir = os.path.abspath(options.autotest_dir)
555
556    if len(args) > 0:
557        logging.error("Invalid option(s) provided: %s", args)
558        parser.print_help()
559        return 1
560
561    if options.verbose:
562        logging.getLogger().setLevel(logging.DEBUG)
563
564    if len(argv) == 1 or (len(argv) == 2 and options.verbose):
565        update_all(options.autotest_dir, options.add_noncompliant,
566                   options.add_experimental)
567        db_clean_broken(options.autotest_dir)
568        return 0
569
570    if options.clear_all_tests:
571        if (options.clean_tests or options.add_all or options.add_samples or
572            options.add_noncompliant):
573            logging.error(
574                "Can only pass --autotest-dir, --dry-run and --verbose with "
575                "--db-clear-all-tests")
576            return 1
577        db_clean_all(options.autotest_dir)
578
579    whitelist_set = None
580    if options.whitelist_file:
581        if options.add_all:
582            logging.error("Cannot pass both --add-all and --whitelist-file")
583            return 1
584        whitelist_path = os.path.abspath(options.whitelist_file)
585        if not os.path.isfile(whitelist_path):
586            logging.error("--whitelist-file (%s) not found", whitelist_path)
587            return 1
588        logging.info("Using whitelist file %s", whitelist_path)
589        whitelist_set =  _create_whitelist_set(whitelist_path)
590        update_from_whitelist(whitelist_set,
591                              add_experimental=options.add_experimental,
592                              add_noncompliant=options.add_noncompliant,
593                              autotest_dir=options.autotest_dir)
594    if options.add_all:
595        update_all(options.autotest_dir, options.add_noncompliant,
596                   options.add_experimental)
597    if options.add_samples:
598        update_samples(options.autotest_dir, options.add_noncompliant,
599                       options.add_experimental)
600    if options.tests_dir:
601        options.tests_dir = os.path.abspath(options.tests_dir)
602        tests = get_tests_from_fs(options.tests_dir, options.control_pattern,
603                                  add_noncompliant=options.add_noncompliant)
604        update_tests_in_db(tests, add_experimental=options.add_experimental,
605                           add_noncompliant=options.add_noncompliant,
606                           autotest_dir=options.autotest_dir)
607    if options.profile_dir:
608        profilers = get_tests_from_fs(options.profile_dir, '.*py$')
609        update_profilers_in_db(profilers,
610                               add_noncompliant=options.add_noncompliant,
611                               description='NA')
612    if options.clean_tests:
613        db_clean_broken(options.autotest_dir)
614
615
616if __name__ == "__main__":
617    sys.exit(main(sys.argv))
618