test_importer.py revision 4b5c31e1821307b880b57a41f6d8ea21948fc9e8
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 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 logging, time, re, os, MySQLdb, sys, optparse, compiler
30import common
31from autotest_lib.client.common_lib import control_data, test, global_config
32from autotest_lib.client.common_lib import utils
33
34
35logging.basicConfig(level=logging.DEBUG)
36# Global
37DRY_RUN = False
38DEPENDENCIES_NOT_FOUND = set()
39
40def main(argv):
41    """Main function"""
42    global DRY_RUN
43    parser = optparse.OptionParser()
44    parser.add_option('-c', '--db-clear-tests',
45                      dest='clear_tests', action='store_true',
46                      default=False,
47                help='Clear client and server tests with invalid control files')
48    parser.add_option('-d', '--dry-run',
49                      dest='dry_run', action='store_true', default=False,
50                      help='Dry run for operation')
51    parser.add_option('-A', '--add-all',
52                      dest='add_all', action='store_true',
53                      default=False,
54                      help='Add samples, site_tests, tests, and test_suites')
55    parser.add_option('-E', '--add-experimental',
56                      dest='add_experimental', action='store_true',
57                      default=True,
58                      help='Add experimental tests to frontend')
59    parser.add_option('-N', '--add-noncompliant',
60                      dest='add_noncompliant', action='store_true',
61                      default=False,
62                      help='Add non-compliant tests (i.e. tests that do not '
63                           'define all required control variables)')
64    parser.add_option('-p', '--profile-dir', dest='profile_dir',
65                      help='Directory to recursively check for profiles')
66    parser.add_option('-t', '--tests-dir', dest='tests_dir',
67                      help='Directory to recursively check for control.*')
68    parser.add_option('-r', '--control-pattern', dest='control_pattern',
69                      default='^control.*',
70               help='The pattern to look for in directories for control files')
71    parser.add_option('-v', '--verbose',
72                      dest='verbose', action='store_true', default=False,
73                      help='Run in verbose mode')
74    parser.add_option('-z', '--autotest_dir', dest='autotest_dir',
75                      default=os.path.join(os.path.dirname(__file__), '..'),
76                      help='Autotest directory root')
77    options, args = parser.parse_args()
78    DRY_RUN = options.dry_run
79    # Make sure autotest_dir is the absolute path
80    options.autotest_dir = os.path.abspath(options.autotest_dir)
81
82    if len(args) > 0:
83        print "Invalid option(s) provided: ", args
84        parser.print_help()
85        return 1
86
87    if len(argv) == 1:
88        update_all(options.autotest_dir, options.add_noncompliant,
89                   options.add_experimental, options.verbose)
90        db_clean_broken(options.autotest_dir, options.verbose)
91        return 0
92
93    if options.add_all:
94        update_all(options.autotest_dir, options.add_noncompliant,
95                   options.add_experimental, options.verbose)
96    if options.clear_tests:
97        db_clean_broken(options.autotest_dir, options.verbose)
98    if options.tests_dir:
99        options.tests_dir = os.path.abspath(options.tests_dir)
100        tests = get_tests_from_fs(options.tests_dir, options.control_pattern,
101                                  add_noncompliant=options.add_noncompliant)
102        update_tests_in_db(tests, add_experimental=options.add_experimental,
103                           add_noncompliant=options.add_noncompliant,
104                           autotest_dir=options.autotest_dir,
105                           verbose=options.verbose)
106    if options.profile_dir:
107        profilers = get_tests_from_fs(options.profile_dir, '.*py$')
108        update_profilers_in_db(profilers, verbose=options.verbose,
109                               add_noncompliant=options.add_noncompliant,
110                               description='NA')
111
112
113def update_all(autotest_dir, add_noncompliant, add_experimental, verbose):
114    """Function to scan through all tests and add them to the database."""
115    for path in [ 'server/tests', 'server/site_tests', 'client/tests',
116                  'client/site_tests']:
117        test_path = os.path.join(autotest_dir, path)
118        if not os.path.exists(test_path):
119            continue
120        if verbose:
121            print "Scanning " + test_path
122        tests = []
123        tests = get_tests_from_fs(test_path, "^control.*",
124                                 add_noncompliant=add_noncompliant)
125        update_tests_in_db(tests, add_experimental=add_experimental,
126                           add_noncompliant=add_noncompliant,
127                           autotest_dir=autotest_dir,
128                           verbose=verbose)
129    test_suite_path = os.path.join(autotest_dir, 'test_suites')
130    if os.path.exists(test_suite_path):
131        if verbose:
132            print "Scanning " + test_suite_path
133        tests = get_tests_from_fs(test_suite_path, '.*',
134                                 add_noncompliant=add_noncompliant)
135        update_tests_in_db(tests, add_experimental=add_experimental,
136                           add_noncompliant=add_noncompliant,
137                           autotest_dir=autotest_dir,
138                           verbose=verbose)
139    sample_path = os.path.join(autotest_dir, 'server/samples')
140    if os.path.exists(sample_path):
141        if verbose:
142            print "Scanning " + sample_path
143        tests = get_tests_from_fs(sample_path, '.*srv$',
144                                 add_noncompliant=add_noncompliant)
145        update_tests_in_db(tests, add_experimental=add_experimental,
146                           add_noncompliant=add_noncompliant,
147                           autotest_dir=autotest_dir,
148                           verbose=verbose)
149
150    profilers_path = os.path.join(autotest_dir, "client/profilers")
151    if os.path.exists(profilers_path):
152        if verbose:
153            print "Scanning " + profilers_path
154        profilers = get_tests_from_fs(profilers_path, '.*py$')
155        update_profilers_in_db(profilers, verbose=verbose,
156                               add_noncompliant=add_noncompliant,
157                               description='NA')
158    # Clean bad db entries
159    db_clean_broken(autotest_dir, verbose)
160
161
162def db_clean_broken(autotest_dir, verbose):
163    """Remove tests from autotest_web that do not have valid control files
164
165       Arguments:
166        tests: a list of control file relative paths used as keys for deletion.
167    """
168    connection=db_connect()
169    cursor = connection.cursor()
170    # Get tests
171    sql = "SELECT id, path FROM autotests";
172    cursor.execute(sql)
173    results = cursor.fetchall()
174    for test_id, path in results:
175        full_path = os.path.join(autotest_dir, path)
176        if not os.path.isfile(full_path):
177            if verbose:
178                print "Removing " + path
179            db_execute(cursor, "DELETE FROM autotests WHERE id=%s" % test_id)
180            db_execute(cursor, "DELETE FROM autotests_dependency_labels WHERE "
181                               "test_id=%s" % test_id)
182
183    # Find profilers that are no longer present
184    profilers = []
185    sql = "SELECT name FROM profilers"
186    cursor.execute(sql)
187    results = cursor.fetchall()
188    for path in results:
189        full_path = os.path.join(autotest_dir, "client/profilers", path[0])
190        if not os.path.exists(full_path):
191            if verbose:
192                print "Removing " + path[0]
193            sql = "DELETE FROM profilers WHERE name='%s'" % path[0]
194            db_execute(cursor, sql)
195
196
197    connection.commit()
198    connection.close()
199
200
201def update_profilers_in_db(profilers, verbose=False, description='NA',
202                           add_noncompliant=False):
203    """Update profilers in autotest_web database"""
204    connection=db_connect()
205    cursor = connection.cursor()
206    for profiler in profilers:
207        name = os.path.basename(profiler).rstrip(".py")
208        if not profilers[profiler]:
209            if add_noncompliant:
210                doc = description
211            else:
212                print "Skipping %s, missing docstring" % profiler
213        else:
214            doc = profilers[profiler]
215        # check if test exists
216        sql = "SELECT name FROM profilers WHERE name='%s'" % name
217        cursor.execute(sql)
218        results = cursor.fetchall()
219        if results:
220            sql = "UPDATE profilers SET name='%s', description='%s' "\
221                  "WHERE name='%s'"
222            sql %= (MySQLdb.escape_string(name), MySQLdb.escape_string(doc),
223                    MySQLdb.escape_string(name))
224        else:
225            # Insert newly into DB
226            sql = "INSERT into profilers (name, description) VALUES('%s', '%s')"
227            sql %= (MySQLdb.escape_string(name), MySQLdb.escape_string(doc))
228
229        db_execute(cursor, sql)
230
231    connection.commit()
232    connection.close()
233
234
235def update_tests_in_db(tests, dry_run=False, add_experimental=False,
236                       add_noncompliant=False, verbose=False,
237                       autotest_dir=None):
238    """Update or add each test to the database"""
239    connection=db_connect()
240    cursor = connection.cursor()
241    new_test_dicts = []
242    for test in tests:
243        new_test = {}
244        new_test['path'] = test.replace(autotest_dir, '').lstrip('/')
245        if verbose:
246            print "Processing " + new_test['path']
247        # Create a name for the test
248        for key in dir(tests[test]):
249            if not key.startswith('__'):
250                value = getattr(tests[test], key)
251                if not callable(value):
252                    new_test[key] = value
253        # This only takes place if --add-noncompliant is provided on the CLI
254        if 'name' not in new_test:
255            test_new_test = test.split('/')
256            if test_new_test[-1] == 'control':
257                new_test['name'] = test_new_test[-2]
258            else:
259                control_name = "%s:%s"
260                control_name %= (test_new_test[-2],
261                                 test_new_test[-1])
262                new_test['name'] = control_name.replace('control.', '')
263        # Experimental Check
264        if not add_experimental:
265            if int(new_test['experimental']):
266                continue
267        # clean tests for insertion into db
268        new_test = dict_db_clean(new_test)
269        new_test_dicts.append(new_test)
270        sql = "SELECT name,path FROM autotests WHERE path='%s' LIMIT 1"
271        sql %= new_test['path']
272        cursor.execute(sql)
273        # check for entries already in existence
274        results = cursor.fetchall()
275        if results:
276            sql = ("UPDATE autotests SET name='%s', test_class='%s',"
277                  "description='%s', test_type=%d, path='%s',"
278                  "author='%s', dependencies='%s',"
279                  "experimental=%d, run_verify=%d, test_time=%d,"
280                  "test_category='%s', sync_count=%d"
281                  " WHERE path='%s'")
282            sql %= (new_test['name'], new_test['test_class'], new_test['doc'],
283                    int(new_test['test_type']), new_test['path'],
284                    new_test['author'],
285                    new_test['dependencies'], int(new_test['experimental']),
286                    int(new_test['run_verify']), new_test['time'],
287                    new_test['test_category'], new_test['sync_count'],
288                    new_test['path'])
289        else:
290            # Create a relative path
291            path = test.replace(autotest_dir, '')
292            sql = ("INSERT INTO autotests"
293                  "(name, test_class, description, test_type, path, "
294                  "author, dependencies, experimental, "
295                  "run_verify, test_time, test_category, sync_count) "
296                  "VALUES('%s','%s','%s',%d,'%s','%s','%s',%d,%d,%d,"
297                  "'%s',%d)")
298            sql %= (new_test['name'], new_test['test_class'], new_test['doc'],
299                    int(new_test['test_type']), new_test['path'],
300                    new_test['author'], new_test['dependencies'],
301                    int(new_test['experimental']), int(new_test['run_verify']),
302                    new_test['time'], new_test['test_category'],
303                    new_test['sync_count'])
304
305        db_execute(cursor, sql)
306
307    add_label_dependencies(new_test_dicts, cursor)
308
309    connection.commit()
310    connection.close()
311
312
313def dict_db_clean(test):
314    """Take a tests dictionary from update_db and make it pretty for SQL"""
315
316    test_type = { 'client' : 1,
317                  'server' : 2, }
318    test_time = { 'short' : 1,
319                  'medium' : 2,
320                  'long' : 3, }
321
322    test['name'] = MySQLdb.escape_string(test['name'])
323    test['author'] = MySQLdb.escape_string(test['author'])
324    test['test_class'] = MySQLdb.escape_string(test['test_class'])
325    test['test_category'] = MySQLdb.escape_string(test['test_category'])
326    test['doc'] = MySQLdb.escape_string(test['doc'])
327    test['dependencies'] = ", ".join(test['dependencies'])
328    # TODO Fix when we move from synch_type to sync_count
329    if test['sync_count'] == 1:
330        test['synch_type'] = 1
331    else:
332        test['synch_type'] = 2
333    try:
334        test['test_type'] = int(test['test_type'])
335        if test['test_type'] != 1 and test['test_type'] != 2:
336            raise Exception('Incorrect number %d for test_type' %
337                            test['test_type'])
338    except ValueError:
339        pass
340    try:
341        test['time'] = int(test['time'])
342        if test['time'] < 1 or test['time'] > 3:
343            raise Exception('Incorrect number %d for time' %
344                            test['time'])
345    except ValueError:
346        pass
347
348    if str == type(test['time']):
349        test['time'] = test_time[test['time'].lower()]
350    if str == type(test['test_type']):
351        test['test_type'] = test_type[test['test_type'].lower()]
352
353    return test
354
355
356def add_label_dependencies(tests, cursor):
357    """
358    Look at the DEPENDENCIES field for each test and add the proper many-to-many
359    relationships.
360    """
361    label_name_to_id = get_id_map(cursor, 'labels', 'name')
362    test_path_to_id = get_id_map(cursor, 'autotests', 'path')
363
364    # clear out old relationships
365    test_ids = ','.join(str(test_path_to_id[test['path']])
366                        for test in tests)
367    db_execute(cursor,
368               'DELETE FROM autotests_dependency_labels WHERE test_id IN (%s)' %
369               test_ids)
370
371    value_pairs = []
372    for test in tests:
373        test_id = test_path_to_id[test['path']]
374        for label_name in test['dependencies'].split(','):
375            label_name = label_name.strip().lower()
376            if not label_name:
377                continue
378            if label_name not in label_name_to_id:
379                log_dependency_not_found(label_name)
380                continue
381            label_id = label_name_to_id[label_name]
382            value_pairs.append('(%s, %s)' % (test_id, label_id))
383
384    if not value_pairs:
385        return
386
387    query = ('INSERT INTO autotests_dependency_labels (test_id, label_id) '
388             'VALUES ' + ','.join(value_pairs))
389    db_execute(cursor, query)
390
391
392def log_dependency_not_found(label_name):
393    if label_name in DEPENDENCIES_NOT_FOUND:
394        return
395    print 'Dependency %s not found' % label_name
396    DEPENDENCIES_NOT_FOUND.add(label_name)
397
398
399def get_id_map(cursor, table_name, name_field):
400    cursor.execute('SELECT id, %s FROM %s' % (name_field, table_name))
401    name_to_id = {}
402    for item_id, item_name in cursor.fetchall():
403        name_to_id[item_name] = item_id
404    return name_to_id
405
406
407def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False):
408    """Find control jobs in location and create one big job
409       Returns:
410        dictionary of the form:
411            tests[file_path] = parsed_object
412
413    """
414    tests = {}
415    profilers = False
416    if 'client/profilers' in parent_dir:
417        profilers = True
418    for dir in [ parent_dir ]:
419        files = recursive_walk(dir, control_pattern)
420        for file in files:
421            if '__init__.py' in file or '.svn' in file:
422                continue
423            if not profilers:
424                if not add_noncompliant:
425                    try:
426                        found_test = control_data.parse_control(file,
427                                                            raise_warnings=True)
428                        tests[file] = found_test
429                    except control_data.ControlVariableException, e:
430                        print "Skipping %s\n%s" % (file, e)
431                        pass
432                else:
433                    found_test = control_data.parse_control(file)
434                    tests[file] = found_test
435            else:
436                script = file.rstrip(".py")
437                tests[file] = compiler.parseFile(file).doc
438    return tests
439
440
441def recursive_walk(path, wildcard):
442    """Recurisvely go through a directory.
443        Returns:
444        A list of files that match wildcard
445    """
446    files = []
447    directories = [ path ]
448    while len(directories)>0:
449        directory = directories.pop()
450        for name in os.listdir(directory):
451            fullpath = os.path.join(directory, name)
452            if os.path.isfile(fullpath):
453                # if we are a control file
454                if re.search(wildcard, name):
455                    files.append(fullpath)
456            elif os.path.isdir(fullpath):
457                directories.append(fullpath)
458    return files
459
460
461def db_connect():
462    """Connect to the AUTOTEST_WEB database and return a connect object."""
463    c = global_config.global_config
464    db_host = c.get_config_value('AUTOTEST_WEB', 'host')
465    db_name = c.get_config_value('AUTOTEST_WEB', 'database')
466    username = c.get_config_value('AUTOTEST_WEB', 'user')
467    password = c.get_config_value('AUTOTEST_WEB', 'password')
468    connection = MySQLdb.connect(host=db_host, db=db_name,
469                                 user=username,
470                                 passwd=password)
471    return connection
472
473
474def db_execute(cursor, sql):
475    """Execute SQL or print out what would be executed if dry_run is defined"""
476
477    if DRY_RUN:
478        print "Would run: " + sql
479    else:
480        cursor.execute(sql)
481
482
483if __name__ == "__main__":
484    main(sys.argv)
485