py.test 捕获未处理的异常 [英] py.test capture unhandled exception

查看:34
本文介绍了py.test 捕获未处理的异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We are using py.test 2.8.7 and I have the below method which creates a separate log file for every test-case. However this does not handle unhandled Exceptions. So if a code snippet throws an Exception instead of failing with an assert, the stack-trace of the Exception is not logged into the separate file. Can someone please help me in how I could capture these Exceptions?

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)

推荐答案

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

I have ended-up with a custom plugin:

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)

这篇关于py.test 捕获未处理的异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆