root: test label handling and error reporting in PytestTestRunner (#14000)

* root: test label handling and error reporting in PytestTestRunner

- Added a check for empty test labels and return a clear error instead of failing silently.
- Improved handling of dotted module paths + included support for single-module names.
- Replaced a RuntimeError with a printed error message when a test label can't be resolved.
- Wrapped the pytest.main call in a try/except to catch unexpected errors and print them nicely.

* Fix handling for test file without extension and lint

* oops

* little improvments too
This commit is contained in:
Dominic R
2025-06-11 13:14:38 -04:00
committed by GitHub
parent 5af2378738
commit 88d83c10a4

View File

@ -7,6 +7,7 @@ from unittest import TestCase
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init from authentik.lib.sentry import sentry_init
@ -22,6 +23,7 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.logger = get_logger().bind(runner="pytest")
self.args = [] self.args = []
if self.failfast: if self.failfast:
@ -34,22 +36,33 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("no_capture", False): if kwargs.get("no_capture", False):
self.args.append("--capture=no") self.args.append("--capture=no")
self._setup_test_environment()
def _setup_test_environment(self):
"""Configure test environment settings"""
settings.TEST = True settings.TEST = True
settings.CELERY["task_always_eager"] = True settings.CELERY["task_always_eager"] = True
CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb")
CONFIG.set("blueprints_dir", "./blueprints")
CONFIG.set(
"outposts.container_image_base",
f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
)
CONFIG.set("tenants.enabled", False)
CONFIG.set("outposts.disable_embedded_outpost", False)
CONFIG.set("error_reporting.sample_rate", 0)
CONFIG.set("error_reporting.environment", "testing")
CONFIG.set("error_reporting.send_pii", True)
sentry_init()
# Test-specific configuration
test_config = {
"events.context_processors.geoip": "tests/GeoLite2-City-Test.mmdb",
"events.context_processors.asn": "tests/GeoLite2-ASN-Test.mmdb",
"blueprints_dir": "./blueprints",
"outposts.container_image_base": f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
"tenants.enabled": False,
"outposts.disable_embedded_outpost": False,
"error_reporting.sample_rate": 0,
"error_reporting.environment": "testing",
"error_reporting.send_pii": True,
}
for key, value in test_config.items():
CONFIG.set(key, value)
sentry_init()
self.logger.debug("Test environment configured")
# Send startup signals
pre_startup.send(sender=self, mode="test") pre_startup.send(sender=self, mode="test")
startup.send(sender=self, mode="test") startup.send(sender=self, mode="test")
post_startup.send(sender=self, mode="test") post_startup.send(sender=self, mode="test")
@ -72,7 +85,21 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
help="Disable any capturing of stdout/stderr during tests.", help="Disable any capturing of stdout/stderr during tests.",
) )
def run_tests(self, test_labels, extra_tests=None, **kwargs): def _validate_test_label(self, label: str) -> bool:
"""Validate test label format"""
if not label:
return False
# Check for invalid characters, but allow forward slashes and colons
# for paths and pytest markers
invalid_chars = set('\\*?"<>|')
if any(c in label for c in invalid_chars):
self.logger.error("Invalid characters in test label", label=label)
return False
return True
def run_tests(self, test_labels: list[str], extra_tests=None, **kwargs):
"""Run pytest and return the exitcode. """Run pytest and return the exitcode.
It translates some of Django's test command option to pytest's. It translates some of Django's test command option to pytest's.
@ -82,10 +109,17 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
The extra_tests argument has been deprecated since Django 5.x The extra_tests argument has been deprecated since Django 5.x
It is kept for compatibility with PyCharm's Django test runner. It is kept for compatibility with PyCharm's Django test runner.
""" """
if not test_labels:
self.logger.error("No test files specified")
return 1
for label in test_labels: for label in test_labels:
if not self._validate_test_label(label):
return 1
valid_label_found = False valid_label_found = False
label_as_path = os.path.abspath(label) label_as_path = os.path.abspath(label)
# File path has been specified # File path has been specified
if os.path.exists(label_as_path): if os.path.exists(label_as_path):
self.args.append(label_as_path) self.args.append(label_as_path)
@ -93,24 +127,30 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
elif "::" in label: elif "::" in label:
self.args.append(label) self.args.append(label)
valid_label_found = True valid_label_found = True
# Convert dotted module path to file_path::class::method
else: else:
# Check if the label is a dotted module path
path_pieces = label.split(".") path_pieces = label.split(".")
# Check whether only class or class and method are specified
for i in range(-1, -3, -1): for i in range(-1, -3, -1):
try:
path = os.path.join(*path_pieces[:i]) + ".py" path = os.path.join(*path_pieces[:i]) + ".py"
label_as_path = os.path.abspath(path) if os.path.exists(path):
if os.path.exists(label_as_path): if i < -1:
path_method = label_as_path + "::" + "::".join(path_pieces[i:]) path_method = path + "::" + "::".join(path_pieces[i:])
self.args.append(path_method) self.args.append(path_method)
else:
self.args.append(path)
valid_label_found = True valid_label_found = True
break break
except (TypeError, IndexError):
continue
if not valid_label_found: if not valid_label_found:
raise RuntimeError( self.logger.error("Test file not found", label=label)
f"One of the test labels: {label!r}, " return 1
f"is not supported. Use a dotted module name or "
f"path instead."
)
self.logger.info("Running tests", test_files=self.args)
try:
return pytest.main(self.args) return pytest.main(self.args)
except Exception as e:
self.logger.error("Error running tests", error=str(e), test_files=self.args)
return 1