ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.116
Committed: Wed Feb 17 18:07:30 2010 UTC (15 years, 2 months ago) by metson
Content type: text/x-python
Branch: MAIN
Changes since 1.115: +4 -2 lines
Log Message:
add bin to the PATH via the env call

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