Source code for esptest.testcase.unittest_case

"""Glue between :class:`unittest.TestCase` and :class:`~esptest.testcase.xunit.XunitLogger`.

This module has **no** dependency on pytest, so the base class works both under a
plain ``unittest`` runner and under pytest.
"""

import os
import unittest

import esptest.common.compat_typing as t

from .xunit import XunitLogger


[docs] def get_case_result_from_outcome(test_case: unittest.TestCase) -> t.Tuple[bool, str]: """Read pass/fail and a brief message for the *current* test method. Meant to be called from ``tearDown`` and works with both runners, whose outcome objects differ: - unittest's ``_Outcome`` keeps ``(test, exc_info)`` tuples on ``.errors``, populated *before* ``tearDown`` runs (``result.failures`` is only filled in afterwards, so it cannot be relied on here). - pytest keeps an ``ExceptionInfo`` list on ``_outcome.result._excinfo``. Returns ``(passed, message)``; ``message`` is empty when the case passed. """ outcome = getattr(test_case, '_outcome', None) if outcome is None: return True, '' # unittest._Outcome: list of (test, exc_info); exc_info is None on success. for entry in getattr(outcome, 'errors', None) or []: exc_info = entry[1] if isinstance(entry, tuple) and len(entry) == 2 else None if exc_info: exc_type, exc_value = exc_info[0], exc_info[1] return False, f'{exc_type.__name__}: {exc_value}' result = getattr(outcome, 'result', None) if result is None: return True, '' # pytest: TestCaseFunction keeps ExceptionInfo list on _excinfo excinfo = getattr(result, '_excinfo', None) if excinfo: exc = excinfo[-1] value = getattr(exc, 'value', None) if value is not None: return False, f'{type(value).__name__}: {value}' return False, str(exc) # unittest fallback: TextTestResult.failures / .errors (filled post-tearDown) method_name = getattr(test_case, '_testMethodName', None) for bucket in (getattr(result, 'failures', None) or [], getattr(result, 'errors', None) or []): for test, traceback_str in bucket: if test is test_case or getattr(test, '_testMethodName', None) == method_name: message = traceback_str.strip().splitlines()[-1] if traceback_str else 'test failed' return False, message return True, ''
[docs] class EspTestCase(unittest.TestCase): """A :class:`unittest.TestCase` that streams results into an xUnit report. Subclasses only need to set :attr:`xunit_log_dir` (and optionally :attr:`target` / :attr:`config` / :attr:`xunit_suite_name`) and write plain ``test_*`` methods. Each case is opened in :meth:`setUp` and closed in :meth:`tearDown` with the result derived from the runner outcome. Under pytest, the ``bind_case_context`` fixture from ``esptest.pytest_plugin`` can inject ``target`` / ``config`` / ``xunit_log_dir`` onto the class automatically. To fold several classes (or a whole pytest session) into a *single* report, assign a shared :class:`~esptest.testcase.xunit.XunitLogger` to :attr:`xunit_logger` before :meth:`setUpClass` runs; the class then reuses that logger and does **not** close it (the owner is responsible for that). """ #: chip target, used in the default :meth:`case_id` target: str = 'unknown' #: build/config label, used in the default :meth:`case_id` config: str = 'Default' #: directory the xUnit report (and per-case artifacts) are written under xunit_log_dir: str = '' #: xUnit suite name; defaults to the class name when empty xunit_suite_name: str = '' #: the logger; created in :meth:`setUpClass`, or set externally to share one xunit_logger: t.Optional[XunitLogger] = None #: whether this class created (and must close) :attr:`xunit_logger` _xunit_owns_logger: bool = False
[docs] def case_id(self) -> str: """xUnit/JUnit case id, ``<target>.<config>.<method_name>`` by default.""" return f'{self.target}.{self.config}.{self._testMethodName}'
[docs] @classmethod def setUpClass(cls) -> None: super().setUpClass() if cls.xunit_logger is None: suite_name = cls.xunit_suite_name or cls.__name__ report_dir = os.path.join(cls.xunit_log_dir or '.', cls.__name__) cls.xunit_logger = XunitLogger(report_dir, suite_name=suite_name) cls._xunit_owns_logger = True else: cls._xunit_owns_logger = False
[docs] @classmethod def tearDownClass(cls) -> None: if cls._xunit_owns_logger and cls.xunit_logger is not None: cls.xunit_logger.close() cls.xunit_logger = None super().tearDownClass()
[docs] def setUp(self) -> None: super().setUp() if self.xunit_logger is not None: self.xunit_logger.begin_case(self.case_id(), classname=self.__class__.__name__)
[docs] def tearDown(self) -> None: if self.xunit_logger is not None and self.xunit_logger.has_running_case: result, message = get_case_result_from_outcome(self) self.xunit_logger.end_case(result=result, message=message) super().tearDown()