diff --git a/ietf/api/tests_views_rpc.py b/ietf/api/tests_views_rpc.py index 032b4b9495..ece2af1b85 100644 --- a/ietf/api/tests_views_rpc.py +++ b/ietf/api/tests_views_rpc.py @@ -6,6 +6,7 @@ from django.conf import settings from django.core.files.base import ContentFile from django.db.models import Max +from django.db.models.functions import Coalesce from django.test.utils import override_settings from django.urls import reverse as urlreverse @@ -22,7 +23,9 @@ def test_draftviewset_references(self): viewname = "ietf.api.purple_api.draft-references" # non-existent draft - bad_id = Document.objects.aggregate(unused_id=Max("id") + 100)["unused_id"] + bad_id = Document.objects.aggregate(unused_id=Coalesce(Max("id"), 0) + 100)[ + "unused_id" + ] url = urlreverse(viewname, kwargs={"doc_id": bad_id}) # Without credentials r = self.client.get(url) diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index 1a3d4e5c3d..df8ed1fd61 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -70,7 +70,7 @@ from django.template.loaders.filesystem import Loader as BaseLoader from django.test.runner import DiscoverRunner from django.core.management import call_command -from django.urls import URLResolver # type: ignore +from django.urls import URLResolver, resolve, Resolver404 # type: ignore from django.template.backends.django import DjangoTemplates from django.template.backends.django import Template # type: ignore[attr-defined] from django.utils import timezone @@ -88,6 +88,26 @@ from mypy_boto3_s3.service_resource import Bucket +class UrlCoverageWarning(UserWarning): + """Warning category for URL coverage-related warnings""" + pass + + +class UninterestingPatternWarning(UrlCoverageWarning): + """Warning category for unexpected URL match patterns + + These are common, caused by tests that hit a URL that is not selected for + coverage checking. The warning is in place to help with a putative future + review of whether we're selecting the right patterns to check for coverage. + """ + pass + + +# Configure warnings for reasonable output quantity +warnings.simplefilter("once", UrlCoverageWarning) +warnings.simplefilter("ignore", UninterestingPatternWarning) + + loaded_templates: set[str] = set() visited_urls: set[str] = set() test_database_name: Optional[str] = None @@ -550,21 +570,37 @@ def ignore_pattern(regex, pattern): ) or pattern.callback == django.views.static.serve) - patterns = [(regex, re.compile(regex, re.U), obj) for regex, obj in url_patterns - if not ignore_pattern(regex, obj)] + patterns ={ + regex: obj + for regex, obj in url_patterns + if not ignore_pattern(regex, obj) + } covered = set() for url in visited_urls: - for regex, compiled, obj in patterns: - if regex not in covered and compiled.match(url[1:]): # strip leading / - covered.add(regex) - break + try: + resolved = resolve(url) # let Django resolve the URL for us + except Resolver404: + warnings.warn( + f"Unable to resolve visited URL {url}", UrlCoverageWarning + ) + continue + if resolved.route not in patterns: + warnings.warn( + f"WARNING: url resolved to an unexpected pattern (url='{url}', " + f"resolved to r'{resolved.route}'", + UninterestingPatternWarning, + ) + continue + covered.add(resolved.route) self.runner.coverage_data["url"] = { - "coverage": 1.0*len(covered)/len(patterns), - "covered": dict( (k, (o.lookup_str, k in covered)) for k,p,o in patterns ), + "coverage": 1.0 * len(covered) / len(patterns), + "covered": dict( + (k, (o.lookup_str, k in covered)) for k, o in patterns.items() + ), "format": 4, - } + } self.report_test_result("url") else: