py.test捕获未处理的异常

时间:2020-06-10 18:09:24

标签: pytest

我们使用的是py.test 2.8.7,我使用以下方法为每个测试用例创建一个单独的日志文件。但是,这不能处理未处理的异常。因此,如果代码片段引发Exception而不是失败而未使用assert失败,则Exception的堆栈跟踪不会记录到单独的文件中。有人可以帮我捕获这些异常吗?

def remove_special_chars(input):
    """
    Replaces all special characters which ideally shout not be included in the name of a file
    Such characters will be replaced with a dot so we know there was something useful there
    """
    for special_ch in ["/", "\\", "<", ">", "|", "&", ":", "*", "?", "\"", "'"]:
        input = input.replace(special_ch, ".")
    return input


def assemble_test_fqn(node):
    """
    Assembles a fully-qualified name for our test-case which will be used as its test log file name
    """
    current_node = node
    result = ""
    while current_node is not None:
        if current_node.name == "()":
            current_node = current_node.parent
            continue
        if result != "":
            result = "." + result
        result = current_node.name + result
        current_node = current_node.parent
    return remove_special_chars(result)


# This fixture creates a logger per test-case
@pytest.yield_fixture(scope="function", autouse=True)
def set_log_file_per_method(request):
    """
    Creates a separate file logging handler for each test method
    """

    # Assembling the location of the log folder
    test_log_dir = "%s/all_test_logs" % (request.config.getoption("--output-dir"))

    # Creating the log folder if it does not exist
    if not os.path.exists(test_log_dir):
        os.makedirs(test_log_dir)

    # Adding a file handler
    test_log_file = "%s/%s.log" % (test_log_dir, assemble_test_fqn(request.node))
    file_handler = logging.FileHandler(filename=test_log_file, mode="w")
    file_handler.setLevel("INFO")
    log_format = request.config.getoption("--log-format")
    log_formatter = logging.Formatter(log_format)
    file_handler.setFormatter(log_formatter)
    logging.getLogger('').addHandler(file_handler)

    yield

    # After the test finished, we remove the file handler
    file_handler.close()
    logging.getLogger('').removeHandler(file_handler)

1 个答案:

答案 0 :(得分:0)

我最终有了一个自定义插件:

import io
import os
import pytest


def remove_special_chars(text):
    """
    Replaces all special characters which ideally shout not be included in the name of a file
    Such characters will be replaced with a dot so we know there was something useful there
    """
    for special_ch in ["/", "\\", "<", ">", "|", "&", ":", "*", "?", "\"", "'"]:
        text = text.replace(special_ch, ".")
    return text


def assemble_test_fqn(node):
    """
    Assembles a fully-qualified name for our test-case which will be used as its test log file name
    The result will also include the potential path of the log file as the parents are appended to the fqn with a /
    """
    current_node = node
    result = ""
    while current_node is not None:
        if current_node.name == "()":
            current_node = current_node.parent
            continue
        if result != "":
            result = "/" + result
        result = remove_special_chars(current_node.name) + result
        current_node = current_node.parent
    return result


def as_unicode(text):
    """
    Encodes a text into unicode
    If it's already unicode, we do not touch it
    """
    if isinstance(text, unicode):
        return text
    else:
        return unicode(str(text))


class TestReport:
    """
    Holds a test-report
    """


    def __init__(self, fqn):
        self._fqn = fqn
        self._errors = []
        self._sections = []


    def add_error(self, error):
        """
        Adds an error (either an Exception or an assertion error) to the list of errors
        """
        self._errors.append(error)


    def add_sections(self, sections):
        """
        Adds captured sections to our internal list of sections
        Since tests can have multiple phases (setup, call, teardown) this will be invoked for all phases
        If for a newer phase we already captured a section, we override it in our already existing internal list
        """

        interim = []

        for current_section in self._sections:
            section_to_add = current_section

            # If the current section we already have is also present in the input parameter,
            # we override our existing section with the one from the input as that's newer
            for index, input_section in enumerate(sections):
                if current_section[0] == input_section[0]:
                    section_to_add = input_section
                    sections.pop(index)
                    break

            interim.append(section_to_add)

        # Adding the new sections from the input parameter to our internal list
        for input_section in sections:
            interim.append(input_section)

        # And finally overriding our internal list of sections
        self._sections = interim


    def save_to_file(self, log_folder):
        """
        Saves the current report to a log file
        """

        # Adding a file handler
        test_log_file = "%s/%s.log" % (log_folder, self._fqn)

        # Creating the log folder if it does not exist
        if not os.path.exists(os.path.dirname(test_log_file)):
            os.makedirs(os.path.dirname(test_log_file))

        # Saving the report to the given log file
        with io.open(test_log_file, 'w', encoding='UTF-8') as f:
            for error in self._errors:
                f.write(as_unicode(error))
                f.write(u"\n\n")
            for index, section in enumerate(self._sections):
                f.write(as_unicode(section[0]))
                f.write(u":\n")
                f.write((u"=" * (len(section[0]) + 1)) + u"\n")
                f.write(as_unicode(section[1]))
                if index < len(self._sections) - 1:
                    f.write(u"\n")


class ReportGenerator:
    """
    A py.test plugin which collects the test-reports and saves them to a separate file per test
    """


    def __init__(self, output_dir):
        self._reports = {}
        self._output_dir = output_dir


    @pytest.hookimpl(tryfirst=True, hookwrapper=True)
    def pytest_runtest_makereport(self, item, call):
        outcome = yield

        # Generating the fully-qualified name of the underlying test
        fqn = assemble_test_fqn(item)

        # Getting the already existing report for the given test from our internal dict or creating a new one if it's not already present
        # We need to do this as this method will be invoked for each phase (setup, call, teardown)
        if fqn not in self._reports:
            report = TestReport(fqn)
            self._reports.update({fqn: report})
        else:
            report = self._reports[fqn]

        result = outcome.result

        # Appending the sections for the current phase to the test-report
        report.add_sections(result.sections)

        # If we have an error, we add that as well to the test-report
        if hasattr(result, "longrepr") and result.longrepr is not None:
            error = result.longrepr
            error_text = ""
            if isinstance(error, str) or isinstance(error, unicode):
                error_text = as_unicode(error)
            elif isinstance(error, tuple):
                error_text = u"\n".join([as_unicode(e) for e in error])
            elif hasattr(error, "reprcrash") and hasattr(error, "reprtraceback"):
                if error.reprcrash is not None:
                    error_text += str(error.reprcrash)
                if error.reprtraceback is not None:
                    if error_text != "":
                        error_text += "\n\n"
                    error_text += str(error.reprtraceback)
            else:
                error_text = as_unicode(error)
            report.add_error(error_text)

        # Finally saving the report
        # We need to do this for all phases as we don't know if and when a test would fail
        # This will essentially override the previous log file for a test if we are in a newer phase
        report.save_to_file("%s/all_test_logs" % self._output_dir)


def pytest_configure(config):
    config._report_generator = ReportGenerator("result")
    config.pluginmanager.register(config._report_generator)
相关问题