feat: django support. (#54)
This commit is contained in:
committed by
GitHub
parent
c969a5b007
commit
48bf141103
@@ -9,6 +9,7 @@ from neotest_python.base import NeotestAdapter, NeotestResult
|
||||
class TestRunner(str, Enum):
|
||||
PYTEST = "pytest"
|
||||
UNITTEST = "unittest"
|
||||
DJANGO = "django"
|
||||
|
||||
|
||||
def get_adapter(runner: TestRunner) -> NeotestAdapter:
|
||||
@@ -20,6 +21,10 @@ def get_adapter(runner: TestRunner) -> NeotestAdapter:
|
||||
from .unittest import UnittestNeotestAdapter
|
||||
|
||||
return UnittestNeotestAdapter()
|
||||
elif runner == TestRunner.DJANGO:
|
||||
from .django_unittest import DjangoNeotestAdapter
|
||||
|
||||
return DjangoNeotestAdapter()
|
||||
raise NotImplementedError(runner)
|
||||
|
||||
|
||||
|
132
neotest_python/django_unittest.py
Normal file
132
neotest_python/django_unittest.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import inspect
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import unittest
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Any, Tuple, Dict, List
|
||||
from unittest import TestCase
|
||||
from unittest.runner import TextTestResult
|
||||
from django import setup as django_setup
|
||||
from django.test.runner import DiscoverRunner
|
||||
from .base import NeotestAdapter, NeotestError, NeotestResultStatus
|
||||
|
||||
|
||||
class CaseUtilsMixin:
|
||||
def case_file(self, case) -> str:
|
||||
return str(Path(inspect.getmodule(case).__file__).absolute())
|
||||
|
||||
def case_id_elems(self, case) -> List[str]:
|
||||
file = self.case_file(case)
|
||||
elems = [file, case.__class__.__name__]
|
||||
if isinstance(case, TestCase):
|
||||
elems.append(case._testMethodName)
|
||||
return elems
|
||||
|
||||
def case_id(self, case: "TestCase | TestSuite") -> str:
|
||||
return "::".join(self.case_id_elems(case))
|
||||
|
||||
|
||||
class DjangoNeotestAdapter(CaseUtilsMixin, NeotestAdapter):
|
||||
def convert_args(self, case_id: str, args: List[str]) -> List[str]:
|
||||
"""Converts a neotest ID into test specifier for unittest"""
|
||||
path, *child_ids = case_id.split("::")
|
||||
if not child_ids:
|
||||
child_ids = []
|
||||
relative_file = os.path.relpath(path, os.getcwd())
|
||||
relative_stem = os.path.splitext(relative_file)[0]
|
||||
relative_dotted = relative_stem.replace(os.sep, ".")
|
||||
return [*args, ".".join([relative_dotted, *child_ids])]
|
||||
|
||||
def run(self, args: List[str], _) -> Dict:
|
||||
errs: Dict[str, Tuple[Exception, Any, TracebackType]] = {}
|
||||
results = {}
|
||||
|
||||
class NeotestTextTestResult(CaseUtilsMixin, TextTestResult):
|
||||
def addFailure(_, test: TestCase, err) -> None:
|
||||
errs[self.case_id(test)] = err
|
||||
return super().addFailure(test, err)
|
||||
|
||||
def addError(_, test: TestCase, err) -> None:
|
||||
errs[self.case_id(test)] = err
|
||||
return super().addError(test, err)
|
||||
|
||||
def addSuccess(_, test: TestCase) -> None:
|
||||
results[self.case_id(test)] = {
|
||||
"status": NeotestResultStatus.PASSED,
|
||||
}
|
||||
|
||||
class DjangoUnittestRunner(CaseUtilsMixin, DiscoverRunner):
|
||||
def __init__(self, **kwargs):
|
||||
django_setup()
|
||||
DiscoverRunner.__init__(self, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def add_arguments(cls, parser):
|
||||
DiscoverRunner.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--verbosity",
|
||||
nargs="?",
|
||||
default=2
|
||||
)
|
||||
parser.add_argument(
|
||||
"--failfast",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
# override
|
||||
def get_resultclass(self):
|
||||
return NeotestTextTestResult
|
||||
|
||||
def collect_results(self, django_test_results, neotest_results):
|
||||
for case, message in (
|
||||
django_test_results.failures + django_test_results.errors
|
||||
):
|
||||
case_id = self.case_id(case)
|
||||
error_line = None
|
||||
case_file = self.case_file(case)
|
||||
if case_id in errs:
|
||||
trace = errs[case_id][2]
|
||||
summary = traceback.extract_tb(trace)
|
||||
for frame in reversed(summary):
|
||||
if frame.filename == case_file:
|
||||
error_line = frame.lineno - 1
|
||||
break
|
||||
neotest_results[case_id] = {
|
||||
"status": NeotestResultStatus.FAILED,
|
||||
"errors": [{"message": message, "line": error_line}],
|
||||
"short": None,
|
||||
}
|
||||
for case, message in django_test_results.skipped:
|
||||
neotest_results[self.case_id(case)] = {
|
||||
"short": None,
|
||||
"status": NeotestResultStatus.SKIPPED,
|
||||
"errors": None,
|
||||
}
|
||||
|
||||
# override
|
||||
def suite_result(self, suite, suite_results, **kwargs):
|
||||
"""Collect Django test suite results and convert them to Neotest compatible results."""
|
||||
self.collect_results(suite_results, results)
|
||||
return (
|
||||
len(suite_results.failures)
|
||||
+ len(suite_results.errors)
|
||||
+ len(suite_results.unexpectedSuccesses)
|
||||
)
|
||||
|
||||
# Make sure we can import relative to current path
|
||||
sys.path.insert(0, os.getcwd())
|
||||
# Prepend an executable name which is just used in output
|
||||
argv = ["neotest-python"] + self.convert_args(args[-1], args[:-1])
|
||||
# parse args
|
||||
parser = ArgumentParser()
|
||||
DjangoUnittestRunner.add_arguments(parser)
|
||||
# run tests
|
||||
runner = DjangoUnittestRunner(
|
||||
**vars(parser.parse_args(argv[1:-1])) # parse plugin config args
|
||||
)
|
||||
runner.run_tests(test_labels=[argv[-1]]) # pass test label
|
||||
return results
|
Reference in New Issue
Block a user