ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.111
Committed: Fri Feb 5 17:44:15 2010 UTC (15 years, 2 months ago) by meloam
Content type: text/x-python
Branch: MAIN
CVS Tags: WMCORE_0_1_1_pre20
Changes since 1.110: +16 -4 lines
Log Message:
Adding in command line option/machinery to let you completely delete your database after each test. (AKA, giving you enough rope to hang yourself)

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