feat: better test & dumpster output (#28044)

This commit is contained in:
David Arnold 2024-10-09 12:05:19 +02:00 committed by GitHub
parent 7e8290b05f
commit db93823001
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 34 deletions

View file

@ -33,6 +33,10 @@ class Color:
CYAN = 96
class FrappeDeprecationWarning(Warning):
...
try:
# since python 3.13, PEP 702
from warnings import deprecated as _deprecated
@ -44,7 +48,7 @@ except ImportError:
T = TypeVar("T", bound=Callable)
def _deprecated(message: str, category=DeprecationWarning, stacklevel=1) -> Callable[[T], T]:
def _deprecated(message: str, category=FrappeDeprecationWarning, stacklevel=1) -> Callable[[T], T]:
def decorator(func: T) -> T:
@functools.wraps(func)
def wrapper(*args, **kwargs):
@ -86,18 +90,14 @@ def deprecated(original: str, marked: str, graduation: str, msg: str, stacklevel
+ colorize(caller_filepath, Color.CYAN)
)
return functools.wraps(func)(
_deprecated(
colorize(f"`{original}`", Color.CYAN)
+ colorize(
f" was moved (DATE: {marked}) to frappe/deprecation_dumpster.py"
f" for removal (from {graduation} onwards); note:\n ",
Color.RED,
)
+ colorize(f"{msg}\n", Color.YELLOW),
stacklevel=stacklevel,
)
)(func)
func.__name__ = original
wrapper = _deprecated(
colorize(f"It was marked on {marked} for removal from {graduation} with note: ", Color.RED)
+ colorize(f"{msg}", Color.YELLOW),
stacklevel=stacklevel,
)
return functools.update_wrapper(wrapper, func)(func)
return decorator

View file

@ -1,5 +1,4 @@
from dataclasses import dataclass, field
from typing import Optional, Union
@dataclass

View file

@ -136,5 +136,3 @@ def _add_module_tests(runner, app: str, module: str):
class TestRunnerError(Exception):
"""Custom exception for test runner errors"""
pass

View file

@ -17,7 +17,9 @@ This TestResult class is typically used by the TestRunner to collect and display
test results during test execution in the Frappe framework.
"""
import io
import logging
import sys
import time
import unittest
@ -27,6 +29,20 @@ logger = logging.getLogger(__name__)
class TestResult(unittest.TextTestResult):
def __init__(self, stream, descriptions, verbosity):
super().__init__(stream, descriptions, verbosity)
self._old_stdout = []
self._old_stderr = []
def startTestRun(self):
if not sys.warnoptions:
import warnings
from frappe.deprecation_dumpster import FrappeDeprecationWarning
warnings.simplefilter("ignore")
warnings.filterwarnings("module", category=FrappeDeprecationWarning)
def startTest(self, test):
self.tb_locals = True
self._started_at = time.monotonic()
@ -34,15 +50,32 @@ class TestResult(unittest.TextTestResult):
test_class = unittest.util.strclass(test.__class__)
if getattr(self, "current_test_class", None) != test_class:
self.current_test_class = test_class
click.echo(f"\n{unittest.util.strclass(test.__class__)}")
logger.info(f"{unittest.util.strclass(test.__class__)}")
self.stream.write(f"\n{test_class}\n")
logger.info(f"{test_class}")
if new_doctypes := getattr(test.__class__, "_newly_created_test_records", None):
records = [f"{name} ({qty})" for name, qty in reversed(new_doctypes)]
click.secho(
f" Test Records created: {', '.join(records)}",
fg="bright_black",
)
hint = click.style(f" Test Records created: {', '.join(records)}", fg="bright_black")
self.stream.write(hint + "\n")
logger.info(f"records created: {', '.join(records)}")
self.stream.flush()
self._old_stderr.append(sys.stderr)
self._old_stdout.append(sys.stdout)
self._test_stdout_capture = io.StringIO()
self._test_stderr_capture = io.StringIO()
sys.stdout = self._test_stdout_capture
sys.stderr = self._test_stderr_capture
def stopTest(self, test):
super().stopTest(test)
sys.stdout = self._old_stderr.pop()
sys.stderr = self._old_stdout.pop()
for line in self._test_stdout_capture.getvalue().splitlines():
self.stream.write(f"{line}\n")
self.stream.flush()
for line in self._test_stderr_capture.getvalue().splitlines():
self.stream.write(f"{line}\n")
self.stream.flush()
def getTestMethodName(self, test):
return test._testMethodName if hasattr(test, "_testMethodName") else str(test)
@ -52,32 +85,34 @@ class TestResult(unittest.TextTestResult):
elapsed = time.monotonic() - self._started_at
threshold_passed = elapsed >= SLOW_TEST_THRESHOLD
long_elapsed = click.style(f" ({elapsed:.03}s)", fg="red") if threshold_passed else ""
click.echo(f" {click.style('', fg='green')} {self.getTestMethodName(test)}{long_elapsed}")
self._write_result(test, "", "green", long_elapsed)
logger.debug(f"{test!s:<200} {'[success]':>20}{elapsed}")
def addError(self, test, err):
super(unittest.TextTestResult, self).addError(test, err)
click.echo(f" {click.style('', fg='red')} {self.getTestMethodName(test)}")
self._write_result(test, "", "red")
logger.debug(f"{test!s:<200} {'[error]':>20}")
def addFailure(self, test, err):
super(unittest.TextTestResult, self).addFailure(test, err)
click.echo(f" {click.style('', fg='red')} {self.getTestMethodName(test)}")
self._write_result(test, "", "red")
logger.debug(f"{test!s:<200} {'[failure]':>20}")
def addSkip(self, test, reason):
super(unittest.TextTestResult, self).addSkip(test, reason)
click.echo(f" {click.style(' = ', fg='white')} {self.getTestMethodName(test)}")
self._write_result(test, " = ", "white")
logger.debug(f"{test!s:<200} {'[skipped]':>20}")
def addExpectedFailure(self, test, err):
super(unittest.TextTestResult, self).addExpectedFailure(test, err)
click.echo(f" {click.style('', fg='red')} {self.getTestMethodName(test)}")
self.stream.write("x")
self._write_result(test, "", "green")
logger.debug(f"{test!s:<200} {'[expected failure]':>20}")
def addUnexpectedSuccess(self, test):
super(unittest.TextTestResult, self).addUnexpectedSuccess(test)
click.echo(f" {click.style('', fg='green')} {self.getTestMethodName(test)}")
self.stream.write("u")
self._write_result(test, "", "red")
logger.debug(f"{test!s:<200} {'[unexpected success]':>20}")
def printErrors(self):
@ -95,5 +130,13 @@ class TestResult(unittest.TextTestResult):
def __str__(self):
return f"Tests: {self.testsRun}, Failing: {len(self.failures)}, Errors: {len(self.errors)}"
def _write_result(self, test, status, color, suffix=""):
test_method = self.getTestMethodName(test)
result = f" {click.style(status, fg=color)} {test_method}"
result += f" {suffix}" if suffix else ""
result += "\n"
self.stream.write(result)
self.stream.flush()
SLOW_TEST_THRESHOLD = 2

View file

@ -21,17 +21,12 @@ It can be customized through the TestConfig object passed during initialization.
import contextlib
import cProfile
import logging
import os
import pstats
import unittest
from collections import defaultdict
from collections.abc import Iterator
from io import StringIO
from pathlib import Path
import click
import frappe
from frappe.tests.classes.context_managers import debug_on
from .config import TestConfig
@ -59,7 +54,7 @@ class TestRunner(unittest.TextTestRunner):
failfast=False,
buffer=False,
resultclass=None,
warnings=None,
warnings="module",
*,
tb_locals=False,
cfg: TestConfig,