ViewVC Help
View File | Revision Log | Show Annotations | Root Listing
root/cvsroot/COMP/WMCORE/setup.py
Revision: 1.78
Committed: Wed Jan 20 22:19:58 2010 UTC (15 years, 3 months ago) by meloam
Content type: text/x-python
Branch: MAIN
Changes since 1.77: +2 -1 lines
Log Message:
committing code that works, I don't care how much dave will try to shame me

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