ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.117
Committed: Wed Feb 17 20:03:05 2010 UTC (15 years, 2 months ago) by meloam
Content type: text/x-python
Branch: MAIN
Changes since 1.116: +1 -0 lines
Log Message:
test messaging

File Contents

# User Rev Content
1 fvlingen 1.1 #!/usr/bin/env python
2 metson 1.21 from distutils.core import setup, Command
3     from unittest import TextTestRunner, TestLoader, TestSuite
4     from glob import glob
5     from os.path import splitext, basename, join as pjoin, walk
6 metson 1.85 from ConfigParser import ConfigParser, NoOptionError
7 meloam 1.53 import os, sys, os.path
8 metson 1.85 import logging
9     import unittest
10 meloam 1.111 import time
11 metson 1.51 #PyLinter and coverage aren't standard, but aren't strictly necessary
12 meloam 1.117 # you should get them though
13 metson 1.51 can_lint = False
14     can_coverage = False
15 meloam 1.88 can_nose = False
16 sfoulkes 1.29 try:
17 metson 1.87 from pylint.lint import Run
18 metson 1.85 from pylint.lint import preprocess_options, cb_init_hook
19     from pylint import checkers
20 metson 1.51 can_lint = True
21     except:
22     pass
23     try:
24 meloam 1.47 import coverage
25 metson 1.51 can_coverage = True
26 sfoulkes 1.29 except:
27     pass
28 fvlingen 1.1
29 meloam 1.88 try:
30     import nose
31 meloam 1.115 from nose.plugins import Plugin, PluginTester
32     import nose.failure
33     import nose.case
34 meloam 1.88 can_nose = True
35     except:
36     pass
37    
38     if can_nose:
39 meloam 1.115 class DetailedOutputter(Plugin):
40     name = "detailed"
41     def __init__(self):
42     pass
43    
44     def setOutputStream(self, stream):
45     """Get handle on output stream so the plugin can print id #s
46     """
47     self.stream = stream
48    
49     def beforeTest(self, test):
50     if ( test.id().startswith('nose.failure') ):
51     pass
52     #Plugin.beforeTest(self, test)
53     else:
54    
55     self.stream.write('%s -- ' % (test.id(), ))
56    
57     def configure(self, options, conf):
58     """
59     Configure plugin. Skip plugin is enabled by default.
60     """
61     self.enabled = True
62    
63 meloam 1.96 class TestCommand(Command):
64 meloam 1.100 """Runs our test suite"""
65 meloam 1.111 # WARNING WARNING WARNING
66     # if you set this to true, I will really delete your database
67     # after every test
68     # WARNING WARNING WARNING
69     user_options = [('reallyDeleteMyDatabaseAfterEveryTest=',
70     None,
71 meloam 1.114 'If you set this I WILL DELETE YOUR DATABASE AFTER EVERY TEST. DO NOT RUN ON A PRODUCTION SYSTEM'),
72     ('buildBotMode=',
73     None,
74     'Are we running inside buildbot?')]
75 meloam 1.88
76     def initialize_options(self):
77 meloam 1.111 self.reallyDeleteMyDatabaseAfterEveryTest = False
78 meloam 1.114 self.buildBotMode = False
79 meloam 1.88 pass
80    
81     def finalize_options(self):
82     pass
83    
84     def run(self):
85 meloam 1.111 if self.reallyDeleteMyDatabaseAfterEveryTest:
86     print "#### WE ARE DELETING YOUR DATABASE. 3 SECONDS TO CANCEL ####"
87     sys.stdout.flush()
88     import WMQuality.TestInit
89     WMQuality.TestInit.deleteDatabaseAfterEveryTest( "I'm Serious" )
90 meloam 1.115 time.sleep(4)
91 meloam 1.114
92     if self.buildBotMode:
93     retval = nose.run(argv=[__file__,'--all-modules','-v','test/python'])
94     else:
95 meloam 1.115 retval = nose.run(argv=['sup.py','--all-modules','-v','test/python','-a','!integration'],
96     addplugins=[DetailedOutputter()])
97 meloam 1.111
98 meloam 1.94 if retval:
99 meloam 1.107 sys.exit( 0 )
100 meloam 1.94 else:
101 meloam 1.107 sys.exit( 1 )
102 meloam 1.88 else:
103 meloam 1.101 class TestCommand(Command):
104 meloam 1.88 user_options = [ ]
105     def run(self):
106 meloam 1.97 print "Nose isn't installed. You must install the nose package to run tests (easy_install nose might do it)"
107 meloam 1.109 sys.exit(1)
108 meloam 1.88 pass
109    
110     def initialize_options(self):
111     pass
112    
113     def finalize_options(self):
114     pass
115     pass
116    
117 metson 1.87 if can_lint:
118     class LinterRun(Run):
119     def __init__(self, args, reporter=None):
120     self._rcfile = None
121     self._plugins = []
122     preprocess_options(args, {
123     # option: (callback, takearg)
124     'rcfile': (self.cb_set_rcfile, True),
125     'load-plugins': (self.cb_add_plugins, True),
126     })
127     self.linter = linter = self.LinterClass((
128     ('rcfile',
129     {'action' : 'callback', 'callback' : lambda *args: 1,
130     'type': 'string', 'metavar': '<file>',
131     'help' : 'Specify a configuration file.'}),
132    
133     ('init-hook',
134     {'action' : 'callback', 'type' : 'string', 'metavar': '<code>',
135     'callback' : cb_init_hook,
136     'help' : 'Python code to execute, usually for sys.path \
137     manipulation such as pygtk.require().'}),
138    
139     ('help-msg',
140     {'action' : 'callback', 'type' : 'string', 'metavar': '<msg-id>',
141     'callback' : self.cb_help_message,
142     'group': 'Commands',
143     'help' : '''Display a help message for the given message id and \
144     exit. The value may be a comma separated list of message ids.'''}),
145    
146     ('list-msgs',
147     {'action' : 'callback', 'metavar': '<msg-id>',
148     'callback' : self.cb_list_messages,
149     'group': 'Commands',
150     'help' : "Generate pylint's full documentation."}),
151    
152     ('generate-rcfile',
153     {'action' : 'callback', 'callback' : self.cb_generate_config,
154     'group': 'Commands',
155     'help' : '''Generate a sample configuration file according to \
156     the current configuration. You can put other options before this one to get \
157     them in the generated configuration.'''}),
158    
159     ('generate-man',
160     {'action' : 'callback', 'callback' : self.cb_generate_manpage,
161     'group': 'Commands',
162     'help' : "Generate pylint's man page.",'hide': 'True'}),
163    
164     ('errors-only',
165     {'action' : 'callback', 'callback' : self.cb_error_mode,
166     'short': 'e',
167     'help' : '''In error mode, checkers without error messages are \
168     disabled and for others, only the ERROR messages are displayed, and no reports \
169     are done by default'''}),
170    
171     ('profile',
172     {'type' : 'yn', 'metavar' : '<y_or_n>',
173     'default': False,
174     'help' : 'Profiled execution.'}),
175    
176     ), option_groups=self.option_groups,
177     reporter=reporter, pylintrc=self._rcfile)
178     # register standard checkers
179     checkers.initialize(linter)
180     # load command line plugins
181     linter.load_plugin_modules(self._plugins)
182     # read configuration
183     linter.disable_message('W0704')
184     linter.read_config_file()
185     # is there some additional plugins in the file configuration, in
186     config_parser = linter._config_parser
187     if config_parser.has_option('MASTER', 'load-plugins'):
188     plugins = splitstrip(config_parser.get('MASTER', 'load-plugins'))
189     linter.load_plugin_modules(plugins)
190     # now we can load file config and command line, plugins (which can
191     # provide options) have been registered
192     linter.load_config_file()
193     if reporter:
194     # if a custom reporter is provided as argument, it may be overriden
195     # by file parameters, so re-set it here, but before command line
196     # parsing so it's still overrideable by command line option
197     linter.set_reporter(reporter)
198     args = linter.load_command_line_configuration(args)
199     # insert current working directory to the python path to have a correct
200     # behaviour
201     sys.path.insert(0, os.getcwd())
202     if self.linter.config.profile:
203     print >> sys.stderr, '** profiled run'
204     from hotshot import Profile, stats
205     prof = Profile('stones.prof')
206     prof.runcall(linter.check, args)
207     prof.close()
208     data = stats.load('stones.prof')
209     data.strip_dirs()
210     data.sort_stats('time', 'calls')
211     data.print_stats(30)
212     sys.path.pop(0)
213    
214     def cb_set_rcfile(self, name, value):
215     """callback for option preprocessing (ie before optik parsing)"""
216     self._rcfile = value
217    
218     def cb_add_plugins(self, name, value):
219     """callback for option preprocessing (ie before optik parsing)"""
220     self._plugins.extend(splitstrip(value))
221    
222     def cb_error_mode(self, *args, **kwargs):
223     """error mode:
224     * checkers without error messages are disabled
225     * for others, only the ERROR messages are displayed
226     * disable reports
227     * do not save execution information
228     """
229     self.linter.disable_noerror_checkers()
230     self.linter.set_option('disable-msg-cat', 'WCRFI')
231     self.linter.set_option('reports', False)
232     self.linter.set_option('persistent', False)
233    
234     def cb_generate_config(self, *args, **kwargs):
235     """optik callback for sample config file generation"""
236     self.linter.generate_config(skipsections=('COMMANDS',))
237    
238     def cb_generate_manpage(self, *args, **kwargs):
239     """optik callback for sample config file generation"""
240     from pylint import __pkginfo__
241     self.linter.generate_manpage(__pkginfo__)
242    
243     def cb_help_message(self, option, opt_name, value, parser):
244     """optik callback for printing some help about a particular message"""
245     self.linter.help_message(splitstrip(value))
246    
247     def cb_list_messages(self, option, opt_name, value, parser):
248     """optik callback for printing available messages"""
249     self.linter.list_messages()
250     else:
251     class LinterRun:
252     def __init__(self):
253     pass
254 metson 1.85
255 metson 1.22 """
256     Build, clean and test the WMCore package.
257     """
258 metson 1.112
259     def get_relative_path():
260     return os.path.dirname(os.path.abspath(os.path.join(os.getcwd(), sys.argv[0])))
261 metson 1.22
262 metson 1.92 def generate_filelist(basepath=None, recurse=True, ignore=False):
263 metson 1.85 if basepath:
264     walkpath = os.path.join(get_relative_path(), 'src/python', basepath)
265     else:
266     walkpath = os.path.join(get_relative_path(), 'src/python')
267    
268 metson 1.36 files = []
269 metson 1.85
270     if walkpath.endswith('.py'):
271 metson 1.92 if ignore and walkpath.endswith(ignore):
272     files.append(walkpath)
273 metson 1.85 else:
274     for dirpath, dirnames, filenames in os.walk(walkpath):
275     # skipping CVS directories and their contents
276     pathelements = dirpath.split('/')
277     result = []
278     if not 'CVS' in pathelements:
279     # to build up a list of file names which contain tests
280     for file in filenames:
281     if file.endswith('.py'):
282     filepath = '/'.join([dirpath, file])
283     files.append(filepath)
284    
285     if len(files) == 0 and recurse:
286     files = generate_filelist(basepath + '.py', not recurse)
287    
288 metson 1.36 return files
289    
290 metson 1.85 def lint_score(stats, evaluation):
291     return eval(evaluation, {}, stats)
292    
293     def lint_files(files, reports=False):
294 metson 1.36 """
295 metson 1.85 lint a (list of) file(s) and return the results as a dictionary containing
296     filename : result_dict
297 metson 1.36 """
298 metson 1.85
299     rcfile=os.path.join(get_relative_path(),'standards/.pylintrc')
300    
301 metson 1.92 arguements = ['--rcfile=%s' % rcfile, '--ignore=DefaultConfig.py']
302 metson 1.86
303     if not reports:
304     arguements.append('-rn')
305    
306 metson 1.85 arguements.extend(files)
307    
308     lntr = LinterRun(arguements)
309    
310     results = {}
311     for file in files:
312     lntr.linter.check(file)
313     results[file] = {'stats': lntr.linter.stats,
314     'score': lint_score(lntr.linter.stats,
315     lntr.linter.config.evaluation)
316     }
317     if reports:
318     print '----------------------------------'
319     print 'Your code has been rated at %.2f/10' % \
320     lint_score(lntr.linter.stats, lntr.linter.config.evaluation)
321 metson 1.36
322 metson 1.85 return results, lntr.linter.config.evaluation
323 meloam 1.69
324 metson 1.21 class CleanCommand(Command):
325 metson 1.49 description = "Clean up (delete) compiled files"
326 metson 1.21 user_options = [ ]
327    
328     def initialize_options(self):
329     self._clean_me = [ ]
330     for root, dirs, files in os.walk('.'):
331     for f in files:
332     if f.endswith('.pyc'):
333     self._clean_me.append(pjoin(root, f))
334    
335     def finalize_options(self):
336     pass
337    
338     def run(self):
339     for clean_me in self._clean_me:
340     try:
341     os.unlink(clean_me)
342     except:
343     pass
344    
345 metson 1.23 class LintCommand(Command):
346 metson 1.49 description = "Lint all files in the src tree"
347 metson 1.24 """
348     TODO: better format the test results, get some global result, make output
349     more buildbot friendly.
350     """
351    
352 metson 1.86 user_options = [ ('package=', 'p', 'package to lint, default to None'),
353     ('report', 'r', 'return a detailed lint report, default False')]
354 metson 1.23
355     def initialize_options(self):
356 metson 1.85 self._dir = get_relative_path()
357     self.package = None
358 metson 1.86 self.report = False
359 metson 1.85
360 metson 1.23 def finalize_options(self):
361 metson 1.86 if self.report:
362     self.report = True
363    
364 metson 1.23 def run(self):
365     '''
366     Find the code and run lint on it
367     '''
368 metson 1.51 if can_lint:
369 metson 1.85 srcpypath = os.path.join(self._dir, 'src/python/')
370    
371 metson 1.51 sys.path.append(srcpypath)
372    
373 metson 1.85 files_to_lint = []
374    
375     if self.package:
376     if self.package.endswith('.py'):
377     cnt = self.package.count('.') - 1
378 metson 1.92 files_to_lint = generate_filelist(self.package.replace('.', '/', cnt), 'DeafultConfig.py')
379 metson 1.85 else:
380 metson 1.92 files_to_lint = generate_filelist(self.package.replace('.', '/'), 'DeafultConfig.py')
381 metson 1.85 else:
382 metson 1.92 files_to_lint = generate_filelist(ignore='DeafultConfig.py')
383 metson 1.86
384     results, evaluation = lint_files(files_to_lint, self.report)
385 metson 1.85 ln = len(results)
386     scr = 0
387     print
388     for k, v in results.items():
389     print "%s: %.2f/10" % (k.replace('src/python/', ''), v['score'])
390     scr += v['score']
391     if ln > 1:
392     print '--------------------------------------------------------'
393     print 'Average pylint score for %s is: %.2f/10' % (self.package,
394     scr/ln)
395    
396 metson 1.51 else:
397     print 'You need to install pylint before using the lint command'
398 metson 1.36
399     class ReportCommand(Command):
400 metson 1.49 description = "Generate a simple html report for ease of viewing in buildbot"
401 metson 1.36 """
402     To contain:
403     average lint score
404     % code coverage
405     list of classes missing tests
406     etc.
407     """
408    
409     user_options = [ ]
410    
411     def initialize_options(self):
412     pass
413    
414     def finalize_options(self):
415     pass
416    
417     def run(self):
418     """
419     run all the tests needed to generate the report and make an
420     html table
421     """
422     files = generate_filelist()
423    
424     error = 0
425     warning = 0
426     refactor = 0
427     convention = 0
428     statement = 0
429    
430 metson 1.85 srcpypath = '/'.join([get_relative_path(), 'src/python/'])
431 metson 1.36 sys.path.append(srcpypath)
432 metson 1.37
433     cfg = ConfigParser()
434     cfg.read('standards/.pylintrc')
435    
436 metson 1.40 # Supress stdout/stderr
437 metson 1.38 sys.stderr = open('/dev/null', 'w')
438 metson 1.39 sys.stdout = open('/dev/null', 'w')
439 meloam 1.47 # wrap it in an exception handler, otherwise we can't see why it fails
440     try:
441     # lint the code
442     for stats in lint_files(files):
443     error += stats['error']
444     warning += stats['warning']
445     refactor += stats['refactor']
446     convention += stats['convention']
447     statement += stats['statement']
448     except Exception,e:
449     # and restore the stdout/stderr
450     sys.stderr = sys.__stderr__
451     sys.stdout = sys.__stderr__
452     raise e
453 metson 1.37
454 metson 1.40 # and restore the stdout/stderr
455 meloam 1.47 sys.stderr = sys.__stderr__
456     sys.stdout = sys.__stderr__
457 metson 1.38
458 metson 1.37 stats = {'error': error,
459     'warning': warning,
460     'refactor': refactor,
461     'convention': convention,
462     'statement': statement}
463    
464     lint_score = eval(cfg.get('MASTER', 'evaluation'), {}, stats)
465 metson 1.36 coverage = 0 # TODO: calculate this
466     testless_classes = [] # TODO: generate this
467    
468     print "<table>"
469     print "<tr>"
470     print "<td colspan=2><h1>WMCore test report</h1></td>"
471     print "</tr>"
472     print "<tr>"
473     print "<td>Average lint score</td>"
474     print "<td>%.2f</td>" % lint_score
475     print "</tr>"
476     print "<tr>"
477     print "<td>% code coverage</td>"
478     print "<td>%s</td>" % coverage
479     print "</tr>"
480     print "<tr>"
481     print "<td>Classes missing tests</td>"
482     print "<td>"
483     if len(testless_classes) == 0:
484     print "None"
485     else:
486     print "<ul>"
487     for c in testless_classes:
488     print "<li>%c</li>" % c
489     print "</ul>"
490     print "</td>"
491     print "</tr>"
492     print "</table>"
493    
494     class CoverageCommand(Command):
495 metson 1.49 description = "Run code coverage tests"
496 metson 1.36 """
497 metson 1.49 To do this, we need to run all the unittests within the coverage
498     framework to record all the lines(and branches) executed
499 meloam 1.47 unfortunately, we have multiple code paths per database schema, so
500     we need to find a way to merge them.
501    
502     TODO: modify the test command to have a flag to record code coverage
503     the file thats used can then be used here, saving us from running
504     our tests twice
505 metson 1.36 """
506    
507     user_options = [ ]
508    
509     def initialize_options(self):
510     pass
511    
512     def finalize_options(self):
513     pass
514    
515     def run(self):
516     """
517     Determine the code's test coverage and return that as a float
518    
519     http://nedbatchelder.com/code/coverage/
520     """
521 metson 1.51 if can_coverage:
522     files = generate_filelist()
523     dataFile = None
524     cov = None
525 meloam 1.48
526 metson 1.51 # attempt to load previously cached coverage information if it exists
527     try:
528     dataFile = open("wmcore-coverage.dat","r")
529     cov = coverage.coverage(branch = True, data_file='wmcore-coverage.dat')
530     cov.load()
531     except:
532     cov = coverage.coverage(branch = True, )
533     cov.start()
534     runUnitTests()
535     cov.stop()
536     cov.save()
537    
538     # we have our coverage information, now let's do something with it
539     # get a list of modules
540     cov.report(morfs = files, file=sys.stdout)
541     return 0
542     else:
543     print 'You need the coverage module installed before running the' +\
544     ' coverage command'
545 metson 1.36
546 metson 1.41 class DumbCoverageCommand(Command):
547 metson 1.49 description = "Run a simple coverage test - find classes that don't have a unit test"
548 metson 1.41
549     user_options = [ ]
550    
551     def initialize_options(self):
552     pass
553    
554     def finalize_options(self):
555     pass
556    
557     def run(self):
558     """
559     Determine the code's test coverage in a dumb way and return that as a
560     float.
561     """
562 metson 1.42 print "This determines test coverage in a very crude manner. If your"
563     print "test file is incorrectly named it will not be counted, and"
564     print "result in a lower coverage score."
565 metson 1.43 print '----------------------------------------------------------------'
566 metson 1.42 filelist = generate_filelist()
567     tests = 0
568     files = 0
569     pkgcnt = 0
570 metson 1.85 dir = get_relative_path()
571 metson 1.42 pkg = {'name': '', 'files': 0, 'tests': 0}
572     for f in filelist:
573 metson 1.41 testpath = '/'.join([dir, f])
574     pth = testpath.split('./src/python/')
575     pth.append(pth[1].replace('/', '_t/').replace('.', '_t.'))
576 metson 1.42 if pkg['name'] == pth[2].rsplit('/', 1)[0].replace('_t/', '/'):
577     # pkg hasn't changed, increment counts
578     pkg['files'] += 1
579     else:
580     # new package, print stats for old package
581     pkgcnt += 1
582     if pkg['name'] != '' and pkg['files'] > 0:
583     print 'Package %s has coverage %.1f percent' % (pkg['name'],
584     (float(pkg['tests'])/float(pkg['files']) * 100))
585     # and start over for the new package
586     pkg['name'] = pth[2].rsplit('/', 1)[0].replace('_t/', '/')
587     # do global book keeping
588     files += pkg['files']
589     tests += pkg['tests']
590     pkg['files'] = 0
591     pkg['tests'] = 0
592 metson 1.41 pth[1] = 'test/python'
593     testpath = '/'.join(pth)
594 metson 1.42 try:
595     os.stat(testpath)
596     pkg['tests'] += 1
597 metson 1.41 except:
598 metson 1.42 pass
599    
600     coverage = (float(tests) / float(files)) * 100
601 metson 1.43 print '----------------------------------------------------------------'
602 metson 1.42 print 'Code coverage (%s packages) is %.2f percent' % (pkgcnt, coverage)
603 metson 1.41 return coverage
604 metson 1.49
605     class EnvCommand(Command):
606     description = "Configure the PYTHONPATH, DATABASE and PATH variables to" +\
607     "some sensible defaults, if not already set. Call with -q when eval-ing," +\
608 metson 1.50 """ e.g.:
609 metson 1.49 eval `python setup.py -q env`
610     """
611    
612     user_options = [ ]
613    
614     def initialize_options(self):
615     pass
616    
617     def finalize_options(self):
618     pass
619 metson 1.41
620 metson 1.49 def run(self):
621     if not os.getenv('DATABASE', False):
622     # Use an in memory sqlite one if none is configured.
623     print 'export DATABASE=sqlite://'
624 metson 1.110 if not os.getenv('COUCHURL', False):
625     # Use the default localhost URL if none is configured.
626     print 'export COUCHURL=localhost:5984'
627 metson 1.93 here = get_relative_path()
628    
629 metson 1.49 tests = here + '/test/python'
630     source = here + '/src/python'
631 metson 1.116 # Stuff we want on the path
632     exepth = [source + '/WMCore/WebTools',
633     here + '/bin']
634 metson 1.93
635     pypath=os.getenv('PYTHONPATH', '').strip(':').split(':')
636 metson 1.49
637     for pth in [tests, source]:
638     if pth not in pypath:
639     pypath.append(pth)
640 metson 1.50
641     # We might want to add other executables to PATH
642 metson 1.49 expath=os.getenv('PATH', '').split(':')
643 metson 1.116 for pth in exepth:
644 metson 1.49 if pth not in expath:
645     expath.append(pth)
646    
647     print 'export PYTHONPATH=%s' % ':'.join(pypath)
648     print 'export PATH=%s' % ':'.join(expath)
649 metson 1.85
650     #We want the WMCORE root set, too
651 metson 1.113 print 'export WMCORE_ROOT=%s' % get_relative_path()
652     print 'export WMCOREBASE=$WMCORE_ROOT'
653     print 'export WTBASE=$WMCORE_ROOT/src'
654    
655 metson 1.85
656 metson 1.49
657 metson 1.22 def getPackages(package_dirs = []):
658     packages = []
659     for dir in package_dirs:
660     for dirpath, dirnames, filenames in os.walk('./%s' % dir):
661     # Exclude things here
662     if dirpath not in ['./src/python/', './src/python/IMProv']:
663     pathelements = dirpath.split('/')
664     if not 'CVS' in pathelements:
665     path = pathelements[3:]
666     packages.append('.'.join(path))
667     return packages
668    
669     package_dir = {'WMCore': 'src/python/WMCore',
670     'WMComponent' : 'src/python/WMComponent',
671     'WMQuality' : 'src/python/WMQuality'}
672    
673     setup (name = 'wmcore',
674     version = '1.0',
675 metson 1.36 maintainer_email = 'hn-cms-wmDevelopment@cern.ch',
676 meloam 1.96 cmdclass = {'clean': CleanCommand,
677 metson 1.36 'lint': LintCommand,
678     'report': ReportCommand,
679 metson 1.41 'coverage': CoverageCommand ,
680 metson 1.49 'missing': DumbCoverageCommand,
681 meloam 1.88 'env': EnvCommand,
682 meloam 1.96 'test' : TestCommand },
683 metson 1.22 package_dir = package_dir,
684     packages = getPackages(package_dir.values()),)