Skip to content

Commit e94ac99

Browse files
committed
Added per-app coverage measurements, to make the numbers shown when not running the full tests suite a bit more meaningful.
- Legacy-Id: 9156
1 parent 37ed580 commit e94ac99

3 files changed

Lines changed: 272 additions & 53 deletions

File tree

ietf/settings.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,22 @@ def skip_unreadable_post(record):
269269

270270
TEST_BLUESHEET_DIR = "tmp-bluesheet-dir"
271271

272+
# These are regexes
273+
TEST_URL_COVERAGE_EXCLUDE = [
274+
"^\^admin/",
275+
]
276+
277+
# Tese are filename globs
272278
TEST_CODE_COVERAGE_EXCLUDE = [
273279
"*/tests*",
274280
"*/admin.py",
275281
"*/migrations/*",
276282
"ietf/settings*",
277283
"ietf/utils/test_runner.py",
278284
]
279-
TEST_CODE_COVERAGE_MASTER_FILE = "coverage-master.json"
280-
TEST_CODE_COVERAGE_LATEST_FILE = "coverage-latest.json"
285+
286+
TEST_COVERAGE_MASTER_FILE = "release-coverage.json"
287+
TEST_COVERAGE_LATEST_FILE = "latest-coverage.json"
281288

282289
# WG Chair configuration
283290
MAX_WG_DELEGATES = 3

ietf/utils/test_runner.py

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from django.test import TestCase
5555
from django.test.runner import DiscoverRunner
5656
from django.core.management import call_command
57+
from django.core.urlresolvers import RegexURLResolver
5758

5859
import 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

150159
def 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

Comments
 (0)