5454from django .test import TestCase
5555from django .test .runner import DiscoverRunner
5656from django .core .management import call_command
57+ from django .core .urlresolvers import RegexURLResolver
5758
5859import debug # pyflakes:ignore
5960
@@ -108,43 +109,51 @@ class RecordUrlsMiddleware(object):
108109 def process_request (self , request ):
109110 visited_urls .add (request .path )
110111
111- def get_url_patterns (module ):
112+ def get_url_patterns (module , apps = None ):
113+ def include (name ):
114+ if not apps :
115+ return True
116+ for app in apps :
117+ if name .startswith (app + '.' ):
118+ return True
119+ return False
120+ def exclude (name ):
121+ for pat in settings .TEST_URL_COVERAGE_EXCLUDE :
122+ if re .search (pat , name ):
123+ return True
124+ return False
125+ if not hasattr (module , 'urlpatterns' ):
126+ return []
112127 res = []
113- try :
114- patterns = module .urlpatterns
115- except AttributeError :
116- patterns = []
117- for item in patterns :
118- try :
119- subpatterns = get_url_patterns (item .urlconf_module )
120- except :
121- subpatterns = [("" , None )]
122- for sub , subitem in subpatterns :
123- if not sub :
124- res .append ((item .regex .pattern , item ))
125- elif sub .startswith ("^" ):
126- res .append ((item .regex .pattern + sub [1 :], subitem ))
127- else :
128- res .append ((item .regex .pattern + ".*" + sub , subitem ))
128+ for item in module .urlpatterns :
129+ if isinstance (item , RegexURLResolver ) and not type (item .urlconf_module ) is list :
130+ if include (item .urlconf_module .__name__ ) and not exclude (item .regex .pattern ):
131+ subpatterns = get_url_patterns (item .urlconf_module )
132+ for sub , subitem in subpatterns :
133+ if sub .startswith ("^" ):
134+ res .append ((item .regex .pattern + sub [1 :], subitem ))
135+ else :
136+ res .append ((item .regex .pattern + ".*" + sub , subitem ))
137+ else :
138+ res .append ((item .regex .pattern , item ))
129139 return res
130140
131-
132- def get_templates ():
141+ def get_templates (apps = None ):
133142 templates = set ()
134- # Should we teach this to use TEMPLATE_DIRS?
135- templatepath = os .path .join (settings .BASE_DIR , "templates" )
136- for root , dirs , files in os .walk (templatepath ):
137- if ".svn" in dirs :
138- dirs .remove (".svn" )
139- relative_path = root [len (templatepath )+ 1 :]
140- for file in files :
141- if file .endswith ("~" ) or file .startswith ("#" ):
142- continue
143- if relative_path == "" :
143+ templatepaths = settings .TEMPLATE_DIRS
144+ for templatepath in templatepaths :
145+ for dirpath , dirs , files in os .walk (templatepath ):
146+ if ".svn" in dirs :
147+ dirs .remove (".svn" )
148+ relative_path = dirpath [len (templatepath )+ 1 :]
149+ for file in files :
150+ if file .endswith ("~" ) or file .startswith ("#" ):
151+ continue
152+ if relative_path != "" :
153+ file = os .path .join (relative_path , file )
144154 templates .add (file )
145- else :
146- templates .add (os .path .join (relative_path , file ))
147-
155+ if apps :
156+ templates = [ t for t in templates if t .split (os .path .sep )[0 ] in apps ]
148157 return templates
149158
150159def save_test_results (failures , test_labels ):
@@ -219,13 +228,14 @@ def report_test_result(self, test):
219228 def template_coverage_test (self ):
220229 global loaded_templates
221230 if self .runner .check_coverage :
222- all = get_templates ()
231+ apps = [ app .split ('.' )[- 1 ] for app in self .runner .test_apps ]
232+ all = get_templates (apps )
223233 # The calculations here are slightly complicated by the situation
224234 # that loaded_templates also contain nomcom page templates loaded
225235 # from the database. However, those don't appear in all
226236 covered = [ k for k in all if k in loaded_templates ]
227237 self .runner .coverage_data ["template" ] = {
228- "coverage" : 1.0 * len (covered )/ len (all ),
238+ "coverage" : 1.0 * len (covered )/ len (all if len ( all ) > 0 else float ( 'nan' ) ),
229239 "covered" : dict ( (k , k in covered ) for k in all ),
230240 }
231241 self .report_test_result ("template" )
@@ -235,7 +245,7 @@ def template_coverage_test(self):
235245 def url_coverage_test (self ):
236246 if self .runner .check_coverage :
237247 import ietf .urls
238- url_patterns = get_url_patterns (ietf .urls )
248+ url_patterns = get_url_patterns (ietf .urls , self . runner . test_apps )
239249
240250 # skip some patterns that we don't bother with
241251 def ignore_pattern (regex , pattern ):
@@ -268,12 +278,13 @@ def ignore_pattern(regex, pattern):
268278
269279 def code_coverage_test (self ):
270280 if self .runner .check_coverage :
281+ include = [ os .path .join (path , '*' ) for path in self .runner .test_paths ]
271282 checker = self .runner .code_coverage_checker
272283 checker .stop ()
273284 checker .save ()
274285 checker ._harvest_data ()
275286 checker .config .from_args (ignore_errors = None , omit = settings .TEST_CODE_COVERAGE_EXCLUDE ,
276- include = None , file = None )
287+ include = include , file = None )
277288 reporter = CoverageReporter (checker , checker .config )
278289 self .runner .coverage_data ["code" ] = reporter .report ()
279290 self .report_test_result ("code" )
@@ -297,7 +308,7 @@ def __init__(self, skip_coverage=False, save_version_coverage=None, **kwargs):
297308 self .save_version_coverage = save_version_coverage
298309 #
299310 self .root_dir = os .path .dirname (settings .BASE_DIR )
300- self .coverage_file = os .path .join (self .root_dir , settings .TEST_CODE_COVERAGE_MASTER_FILE )
311+ self .coverage_file = os .path .join (self .root_dir , settings .TEST_COVERAGE_MASTER_FILE )
301312 super (IetfTestRunner , self ).__init__ (** kwargs )
302313
303314 def setup_test_environment (self , ** kwargs ):
@@ -348,7 +359,7 @@ def setup_test_environment(self, **kwargs):
348359 def teardown_test_environment (self , ** kwargs ):
349360 self .smtpd_driver .stop ()
350361 if self .check_coverage :
351- latest_coverage_file = os .path .join (self .root_dir , settings .TEST_CODE_COVERAGE_LATEST_FILE )
362+ latest_coverage_file = os .path .join (self .root_dir , settings .TEST_COVERAGE_LATEST_FILE )
352363 with open (latest_coverage_file , "w" ) as file :
353364 json .dump (self .coverage_data , file , indent = 2 , sort_keys = True )
354365 if self .save_version_coverage :
@@ -358,6 +369,39 @@ def teardown_test_environment(self, **kwargs):
358369 json .dump (self .coverage_master , file , indent = 2 , sort_keys = True )
359370 super (IetfTestRunner , self ).teardown_test_environment (** kwargs )
360371
372+ def get_test_paths (self , test_labels ):
373+ """Find the apps and paths matching the test labels, so we later can limit
374+ the coverage data to those apps and paths.
375+ """
376+ test_apps = []
377+ app_roots = set ( app .split ('.' )[0 ] for app in settings .INSTALLED_APPS )
378+ for label in test_labels :
379+ part_list = label .split ('.' )
380+ if label in settings .INSTALLED_APPS :
381+ # The label is simply an app in installed apps
382+ test_apps .append (label )
383+ elif not (part_list [0 ] in app_roots ):
384+ # try to add an app root to get a match with installed apps
385+ for root in app_roots :
386+ for j in range (len (part_list )):
387+ maybe_app = "." .join ([root ] + part_list [:j + 1 ])
388+ if maybe_app in settings .INSTALLED_APPS :
389+ test_apps .append (maybe_app )
390+ break
391+ else :
392+ continue
393+ break
394+ else :
395+ # the label is more detailed than a plain app, and the
396+ # root is in app_roots.
397+ for j in range (len (part_list )):
398+ maybe_app = "." .join (part_list [:j + 1 ])
399+ if maybe_app in settings .INSTALLED_APPS :
400+ test_apps .append (maybe_app )
401+ break
402+ test_paths = [ os .path .join (* app .split ('.' )) for app in test_apps ]
403+ return test_apps , test_paths
404+
361405 def run_tests (self , test_labels , extra_tests = None , ** kwargs ):
362406 # Tests that involve switching back and forth between the real
363407 # database and the test database are way too dangerous to run
@@ -375,7 +419,9 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
375419 self .run_full_test_suite = not test_labels
376420
377421 if not test_labels : # we only want to run our own tests
378- test_labels = [app for app in settings .INSTALLED_APPS if app .startswith ("ietf" )]
422+ test_labels = ["ietf" ]
423+
424+ self .test_apps , self .test_paths = self .get_test_paths (test_labels )
379425
380426 extra_tests = [
381427 CoverageTest (test_runner = self , methodName = 'url_coverage_test' ),
@@ -389,6 +435,10 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
389435
390436 if self .check_coverage :
391437 print ("" )
438+ if self .run_full_test_suite :
439+ print ("Test coverage data:" )
440+ else :
441+ print ("Test coverage data for %s:" % ", " .join (test_labels ))
392442 for test in ["template" , "url" , "code" ]:
393443 latest_coverage_version = self .coverage_master ["version" ]
394444
@@ -402,13 +452,20 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
402452 #test_missing = [ k for k,v in test_data["covered"].items() if not v ]
403453 test_coverage = test_data ["coverage" ]
404454
405- print ( "%-8s coverage: %.1f%% (%s: %.1f%%)" %
406- (test .capitalize (), test_coverage * 100 , latest_coverage_version , master_coverage * 100 , ))
455+ if self .run_full_test_suite :
456+ print (" %8s coverage: %6.2f%% (%s: %6.2f%%)" %
457+ (test .capitalize (), test_coverage * 100 , latest_coverage_version , master_coverage * 100 , ))
458+ else :
459+ print (" %8s coverage: %6.2f%%" %
460+ (test .capitalize (), test_coverage * 100 , ))
407461
408462 print ("""
409- Code coverage data has been written to '.coverage', readable by
410- %s/bin/coverage.
411- """ .replace (" " ,"" ) % settings .BASE_DIR )
463+ Per-file code and template coverage and per-url-pattern url coverage data
464+ for the latest test run has been written to %s.
465+
466+ Per-statement code coverage data has been written to '.coverage', readable
467+ by the 'coverage' program.
468+ """ .replace (" " ,"" ) % (settings .TEST_COVERAGE_LATEST_FILE ))
412469
413470 save_test_results (failures , test_labels )
414471
0 commit comments