ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.75
Committed: Wed Jan 20 07:02:51 2010 UTC (15 years, 3 months ago) by meloam
Content type: text/x-python
Branch: MAIN
Changes since 1.74: +5 -1 lines
Log Message:
provide a sensible default for the WMCOREBASE environment variable. not sure if we even want to keep it though...

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