ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.49
Committed: Tue Jan 12 12:45:58 2010 UTC (15 years, 3 months ago) by metson
Content type: text/x-python
Branch: MAIN
Changes since 1.48: +50 -18 lines
Log Message:
Add in EnvCommand to make setting up a sane environment easy. Use via eval `python setup.py -q env`.

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