ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.51
Committed: Tue Jan 12 13:02:37 2010 UTC (15 years, 3 months ago) by metson
Content type: text/x-python
Branch: MAIN
CVS Tags: forSecMod_b2, forSecMod
Changes since 1.50: +40 -26 lines
Log Message:
Give some polite messages when trying to run lint/coverage without the modules being installed.

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.37 from ConfigParser import ConfigParser
7 metson 1.30 import os, sys
8 metson 1.51 #PyLinter and coverage aren't standard, but aren't strictly necessary
9     can_lint = False
10     can_coverage = False
11 sfoulkes 1.29 try:
12     from pylint import lint
13 metson 1.51 can_lint = True
14     except:
15     pass
16     try:
17 meloam 1.47 import coverage
18 metson 1.51 can_coverage = True
19 sfoulkes 1.29 except:
20     pass
21 fvlingen 1.1
22 metson 1.22 """
23     Build, clean and test the WMCore package.
24     """
25    
26 metson 1.36 def generate_filelist():
27     files = []
28 meloam 1.48 for dirpath, dirnames, filenames in os.walk('src/python/'):
29 metson 1.36 # skipping CVS directories and their contents
30     pathelements = dirpath.split('/')
31     result = []
32     if not 'CVS' in pathelements:
33     # to build up a list of file names which contain tests
34     for file in filenames:
35     if file.endswith('.py'):
36     filepath = '/'.join([dirpath, file])
37     files.append(filepath)
38     return files
39    
40     def lint_files(files):
41     """
42     lint a (list of) file(s) and return the results
43     """
44     input = ['--rcfile=standards/.pylintrc',
45     '--output-format=parseable',
46     '--reports=n', ]
47     input.extend(files)
48 meloam 1.47 lint_result = lint.Run(input)
49 metson 1.36 return lint_result.linter.stats
50    
51 meloam 1.47 def runUnitTests():
52     # runs all the unittests, returning the testresults object
53     testfiles = []
54     # Add the test and src directory to the python path
55     mydir = os.getcwd()
56     # todo: not portable
57     testspypath = '/'.join([mydir, 'test/python/'])
58     srcpypath = '/'.join([mydir, 'src/python/'])
59     sys.path.append(testspypath)
60     sys.path.append(srcpypath)
61    
62     # Walk the directory tree
63 meloam 1.48 for dirpath, dirnames, filenames in os.walk('./test/python'):
64 meloam 1.47 # skipping CVS directories and their contents
65     pathelements = dirpath.split('/')
66     if not 'CVS' in pathelements:
67     # to build up a list of file names which contain tests
68     for file in filenames:
69     if file not in ['__init__.py']:
70     if file.endswith('_t.py'):
71     testmodpath = pathelements[3:]
72     testmodpath.append(file.replace('.py', ''))
73     testfiles.append('.'.join(testmodpath))
74    
75     sys.stderr = open('/dev/null', 'w')
76     sys.stdout = open('/dev/null', 'w')
77    
78     testsuite = TestSuite()
79     failedTestFiles = []
80     for test in testfiles:
81     try:
82     testsuite.addTest(TestLoader().loadTestsFromName(test))
83     except Exception, e:
84     failedTestFiles.append(test)
85    
86    
87 meloam 1.48 t = TextTestRunner(verbosity=0)
88 meloam 1.47 result = t.run(testsuite)
89     sys.stdout = sys.__stdout__
90     sys.stderr = sys.__stderr__
91     return (result, failedTestFiles)
92    
93    
94 metson 1.21 class TestCommand(Command):
95 metson 1.49 description = "Handle setup.py test with this class - walk through the " + \
96     "directory structure building up a list of tests, then build a test " + \
97     " suite and execute it."
98 metson 1.22 """
99     TODO: Pull database URL's from environment, and skip tests where database
100     URL is not present (e.g. for a slave without Oracle connection)
101 meloam 1.46
102     TODO: need to build a separate test runner for each test file, python is
103     keeping the test objects around, which is keeping it from destroying
104     filehandles, which is causing us to bomb out of a lot more tests than
105     necessary. Or, people could learn to close their files themselves.
106     either-or.
107 metson 1.22 """
108 metson 1.21 user_options = [ ]
109    
110     def initialize_options(self):
111     self._dir = os.getcwd()
112    
113     def finalize_options(self):
114     pass
115    
116     def run(self):
117     '''
118 metson 1.25 Finds all the tests modules in test/python/WMCore_t, and runs them.
119 metson 1.21 '''
120     testfiles = [ ]
121 metson 1.26
122 meloam 1.48 # attempt to perform coverage tests, even if they weren't asked for
123     # it doesn't cost (much), and by caching the results, a later
124     # coverage test run doesn't have to run through all the unittests
125     # just generate the coverage report
126     files = generate_filelist()
127     coverageEnabled = True;
128     try:
129     cov = coverage.coverage(branch = True, data_file="wmcore-coverage.dat" )
130     cov.start()
131     print "Caching code coverage statstics"
132     except:
133     coverageEnabled = False
134    
135 meloam 1.47 result, failedTestFiles = runUnitTests()
136 meloam 1.48
137     if coverageEnabled:
138     cov.stop()
139     cov.save()
140    
141 metson 1.32 if not result.wasSuccessful():
142 meloam 1.44 print "Tests unsuccessful. There were %s failures and %s errors"\
143     % (len(result.failures), len(result.errors))
144     print "Failurelist:\n%s" % "\n".join(map(lambda x: \
145     "FAILURE: %s\n%s" % (x[0],x[1] ), result.failures))
146     print "Errorlist:\n%s" % "\n".join(map(lambda x: \
147     "ERROR: %s\n%s" % (x[0],x[1] ), result.errors))
148     if len(failedTestFiles):
149     print "The following tests failed to load: \n===============\n%s" %\
150     "\n".join(failedTestFiles)
151     #"".join(' ', [result.failures[0],result.failures[1]]))
152     #print "Errorlist:\n%s" % result.errors #"".join(' ', [result.errors[0],result.errors[1]]))
153 meloam 1.45 print "Tests unsuccessful. There were %s failures and %s errors"\
154     % (len(result.failures), len(result.errors))
155 meloam 1.44 print "FAILED: setup.py test"
156     sys.exit(1)
157     else:
158     print "Tests successful"
159     if len(failedTestFiles):
160     print "The following tests failed to load: \n===============\n%s" %\
161     "\n".join(failedTestFiles)
162     print "PASSED: setup.py test"
163     sys.exit(0)
164 metson 1.32
165 meloam 1.44
166    
167    
168 metson 1.21 class CleanCommand(Command):
169 metson 1.49 description = "Clean up (delete) compiled files"
170 metson 1.21 user_options = [ ]
171    
172     def initialize_options(self):
173     self._clean_me = [ ]
174     for root, dirs, files in os.walk('.'):
175     for f in files:
176     if f.endswith('.pyc'):
177     self._clean_me.append(pjoin(root, f))
178    
179     def finalize_options(self):
180     pass
181    
182     def run(self):
183     for clean_me in self._clean_me:
184     try:
185     os.unlink(clean_me)
186     except:
187     pass
188    
189 metson 1.23 class LintCommand(Command):
190 metson 1.49 description = "Lint all files in the src tree"
191 metson 1.24 """
192     TODO: better format the test results, get some global result, make output
193     more buildbot friendly.
194     """
195    
196 metson 1.23 user_options = [ ]
197    
198     def initialize_options(self):
199     self._dir = os.getcwd()
200    
201     def finalize_options(self):
202     pass
203    
204     def run(self):
205     '''
206     Find the code and run lint on it
207     '''
208 metson 1.51 if can_lint:
209     srcpypath = '/'.join([self._dir, 'src/python/'])
210     sys.path.append(srcpypath)
211    
212     result = []
213     for filepath in generate_filelist():
214     result.append(lint_files([filepath]))
215     else:
216     print 'You need to install pylint before using the lint command'
217 metson 1.36
218     class ReportCommand(Command):
219 metson 1.49 description = "Generate a simple html report for ease of viewing in buildbot"
220 metson 1.36 """
221     To contain:
222     average lint score
223     % code coverage
224     list of classes missing tests
225     etc.
226     """
227    
228     user_options = [ ]
229    
230     def initialize_options(self):
231     pass
232    
233     def finalize_options(self):
234     pass
235    
236     def run(self):
237     """
238     run all the tests needed to generate the report and make an
239     html table
240     """
241     files = generate_filelist()
242    
243     error = 0
244     warning = 0
245     refactor = 0
246     convention = 0
247     statement = 0
248    
249     srcpypath = '/'.join([os.getcwd(), 'src/python/'])
250     sys.path.append(srcpypath)
251 metson 1.37
252     cfg = ConfigParser()
253     cfg.read('standards/.pylintrc')
254    
255 metson 1.40 # Supress stdout/stderr
256 metson 1.38 sys.stderr = open('/dev/null', 'w')
257 metson 1.39 sys.stdout = open('/dev/null', 'w')
258 meloam 1.47 # wrap it in an exception handler, otherwise we can't see why it fails
259     try:
260     # lint the code
261     for stats in lint_files(files):
262     error += stats['error']
263     warning += stats['warning']
264     refactor += stats['refactor']
265     convention += stats['convention']
266     statement += stats['statement']
267     except Exception,e:
268     # and restore the stdout/stderr
269     sys.stderr = sys.__stderr__
270     sys.stdout = sys.__stderr__
271     raise e
272 metson 1.37
273 metson 1.40 # and restore the stdout/stderr
274 meloam 1.47 sys.stderr = sys.__stderr__
275     sys.stdout = sys.__stderr__
276 metson 1.38
277 metson 1.37 stats = {'error': error,
278     'warning': warning,
279     'refactor': refactor,
280     'convention': convention,
281     'statement': statement}
282    
283     lint_score = eval(cfg.get('MASTER', 'evaluation'), {}, stats)
284 metson 1.36 coverage = 0 # TODO: calculate this
285     testless_classes = [] # TODO: generate this
286    
287     print "<table>"
288     print "<tr>"
289     print "<td colspan=2><h1>WMCore test report</h1></td>"
290     print "</tr>"
291     print "<tr>"
292     print "<td>Average lint score</td>"
293     print "<td>%.2f</td>" % lint_score
294     print "</tr>"
295     print "<tr>"
296     print "<td>% code coverage</td>"
297     print "<td>%s</td>" % coverage
298     print "</tr>"
299     print "<tr>"
300     print "<td>Classes missing tests</td>"
301     print "<td>"
302     if len(testless_classes) == 0:
303     print "None"
304     else:
305     print "<ul>"
306     for c in testless_classes:
307     print "<li>%c</li>" % c
308     print "</ul>"
309     print "</td>"
310     print "</tr>"
311     print "</table>"
312    
313     class CoverageCommand(Command):
314 metson 1.49 description = "Run code coverage tests"
315 metson 1.36 """
316 metson 1.49 To do this, we need to run all the unittests within the coverage
317     framework to record all the lines(and branches) executed
318 meloam 1.47 unfortunately, we have multiple code paths per database schema, so
319     we need to find a way to merge them.
320    
321     TODO: modify the test command to have a flag to record code coverage
322     the file thats used can then be used here, saving us from running
323     our tests twice
324 metson 1.36 """
325    
326     user_options = [ ]
327    
328     def initialize_options(self):
329     pass
330    
331     def finalize_options(self):
332     pass
333    
334     def run(self):
335     """
336     Determine the code's test coverage and return that as a float
337    
338     http://nedbatchelder.com/code/coverage/
339     """
340 metson 1.51 if can_coverage:
341     files = generate_filelist()
342     dataFile = None
343     cov = None
344 meloam 1.48
345 metson 1.51 # attempt to load previously cached coverage information if it exists
346     try:
347     dataFile = open("wmcore-coverage.dat","r")
348     cov = coverage.coverage(branch = True, data_file='wmcore-coverage.dat')
349     cov.load()
350     except:
351     cov = coverage.coverage(branch = True, )
352     cov.start()
353     runUnitTests()
354     cov.stop()
355     cov.save()
356    
357     # we have our coverage information, now let's do something with it
358     # get a list of modules
359     cov.report(morfs = files, file=sys.stdout)
360     return 0
361     else:
362     print 'You need the coverage module installed before running the' +\
363     ' coverage command'
364 metson 1.36
365 metson 1.41 class DumbCoverageCommand(Command):
366 metson 1.49 description = "Run a simple coverage test - find classes that don't have a unit test"
367 metson 1.41
368     user_options = [ ]
369    
370     def initialize_options(self):
371     pass
372    
373     def finalize_options(self):
374     pass
375    
376     def run(self):
377     """
378     Determine the code's test coverage in a dumb way and return that as a
379     float.
380     """
381 metson 1.42 print "This determines test coverage in a very crude manner. If your"
382     print "test file is incorrectly named it will not be counted, and"
383     print "result in a lower coverage score."
384 metson 1.43 print '----------------------------------------------------------------'
385 metson 1.42 filelist = generate_filelist()
386     tests = 0
387     files = 0
388     pkgcnt = 0
389 metson 1.41 dir = os.getcwd()
390 metson 1.42 pkg = {'name': '', 'files': 0, 'tests': 0}
391     for f in filelist:
392 metson 1.41 testpath = '/'.join([dir, f])
393     pth = testpath.split('./src/python/')
394     pth.append(pth[1].replace('/', '_t/').replace('.', '_t.'))
395 metson 1.42 if pkg['name'] == pth[2].rsplit('/', 1)[0].replace('_t/', '/'):
396     # pkg hasn't changed, increment counts
397     pkg['files'] += 1
398     else:
399     # new package, print stats for old package
400     pkgcnt += 1
401     if pkg['name'] != '' and pkg['files'] > 0:
402     print 'Package %s has coverage %.1f percent' % (pkg['name'],
403     (float(pkg['tests'])/float(pkg['files']) * 100))
404     # and start over for the new package
405     pkg['name'] = pth[2].rsplit('/', 1)[0].replace('_t/', '/')
406     # do global book keeping
407     files += pkg['files']
408     tests += pkg['tests']
409     pkg['files'] = 0
410     pkg['tests'] = 0
411 metson 1.41 pth[1] = 'test/python'
412     testpath = '/'.join(pth)
413 metson 1.42 try:
414     os.stat(testpath)
415     pkg['tests'] += 1
416 metson 1.41 except:
417 metson 1.42 pass
418    
419     coverage = (float(tests) / float(files)) * 100
420 metson 1.43 print '----------------------------------------------------------------'
421 metson 1.42 print 'Code coverage (%s packages) is %.2f percent' % (pkgcnt, coverage)
422 metson 1.41 return coverage
423 metson 1.49
424     class EnvCommand(Command):
425     description = "Configure the PYTHONPATH, DATABASE and PATH variables to" +\
426     "some sensible defaults, if not already set. Call with -q when eval-ing," +\
427 metson 1.50 """ e.g.:
428 metson 1.49 eval `python setup.py -q env`
429     """
430    
431     user_options = [ ]
432    
433     def initialize_options(self):
434     pass
435    
436     def finalize_options(self):
437     pass
438 metson 1.41
439 metson 1.49 def run(self):
440     if not os.getenv('DATABASE', False):
441     # Use an in memory sqlite one if none is configured.
442     print 'export DATABASE=sqlite://'
443    
444     here = os.path.abspath('.')
445     tests = here + '/test/python'
446     source = here + '/src/python'
447     webpth = source + '/WMCore/WebTools'
448    
449     pypath=os.getenv('PYTHONPATH', '').split(':')
450     for pth in [tests, source]:
451     if pth not in pypath:
452     pypath.append(pth)
453 metson 1.50
454     # We might want to add other executables to PATH
455 metson 1.49 expath=os.getenv('PATH', '').split(':')
456     for pth in [webpth]:
457     if pth not in expath:
458     expath.append(pth)
459    
460     print 'export PYTHONPATH=%s' % ':'.join(pypath)
461     print 'export PATH=%s' % ':'.join(expath)
462    
463 metson 1.22 def getPackages(package_dirs = []):
464     packages = []
465     for dir in package_dirs:
466     for dirpath, dirnames, filenames in os.walk('./%s' % dir):
467     # Exclude things here
468     if dirpath not in ['./src/python/', './src/python/IMProv']:
469     pathelements = dirpath.split('/')
470     if not 'CVS' in pathelements:
471     path = pathelements[3:]
472     packages.append('.'.join(path))
473     return packages
474    
475     package_dir = {'WMCore': 'src/python/WMCore',
476     'WMComponent' : 'src/python/WMComponent',
477     'WMQuality' : 'src/python/WMQuality'}
478    
479     setup (name = 'wmcore',
480     version = '1.0',
481 metson 1.36 maintainer_email = 'hn-cms-wmDevelopment@cern.ch',
482     cmdclass = {'test': TestCommand,
483 metson 1.23 'clean': CleanCommand,
484 metson 1.36 'lint': LintCommand,
485     'report': ReportCommand,
486 metson 1.41 'coverage': CoverageCommand ,
487 metson 1.49 'missing': DumbCoverageCommand,
488     'env': EnvCommand },
489 metson 1.22 package_dir = package_dir,
490     packages = getPackages(package_dir.values()),)
491 fvlingen 1.7