ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.125
Committed: Tue Aug 17 17:46:29 2010 UTC (14 years, 8 months ago) by meloam
Content type: text/x-python
Branch: MAIN
CVS Tags: WMCORE_T0_1_1_0_pre9, WMCORE_T0_1_1_0_pre4, WMCORE_T0_1_1_0_pre3, WMCORE_T0_1_1_0_pre2, WMCORE_T0_1_1_0_pre1, WMCORE_0_1_1_pre25, HEAD
Changes since 1.124: +13 -5 lines
Error occurred while calculating annotation data.
Log Message:
adding code coverage to testing

File Contents

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