pytest每条case在子进程单独运行,allure收集不到测试结果解决办法

背景

  1. python接口运行c++ 共享库,c++ so 共享库segfaults 后会导致整个python 解释器退出,影响后续测试用例运行
  2. c++ so库有内存泄漏,长时间运行测试用例,导致内存不足。

解决方案

  1. 针对第一个问题,使用pytest-xdist, 每个worker crash后xdist可以重启一个worker进行后续测试
  2. 使用pytest-forked. pytest-forked官网
    pytest-forked支持每条测试用例运行在单独forked的子进程里,这样每条测试用例运行结束后可以释放占用的资源。 但是因为测试用例在forked的子进程里运行,父进程的allure报告监听不到子进程运行的测试用例的状态。分析原因如下:
    pytest-forked入口在site-packages/pytest_forked/_ init _.py
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_protocol(item):
    if item.config.getvalue("forked") or item.get_closest_marker("forked"):
        ihook = item.ihook
        ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
        reports = forked_run_report(item) 
        for rep in reports:
            ihook.pytest_runtest_logreport(report=rep)
        ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
        return True
        
def forked_run_report(item):
    # for now, we run setup/teardown in the subprocess
    # XXX optionally allow sharing of setup/teardown
    from _pytest.runner import runtestprotocol

    EXITSTATUS_TESTEXIT = 4
    import marshal

    def runforked():
        try:
            reports = runtestprotocol(item, log=False) 
        except KeyboardInterrupt:
            os._exit(EXITSTATUS_TESTEXIT)
        return marshal.dumps([serialize_report(x) for x in reports])

    ff = py.process.ForkedFunc(runforked) # forked子进程里运行测试用例,由于子进程复制父进程资源,对allure test result进行的更改不会影响父进程的test result, 导致父进程的allure test result收集不到子进程运行case的状态
    result = ff.waitfinish()
    if result.retval is not None:
        report_dumps = marshal.loads(result.retval)
        return [runner.TestReport(**x) for x in report_dumps]
    else:
        if result.exitstatus == EXITSTATUS_TESTEXIT:
            pytest.exit(f"forked test item {item} raised Exit")
        return [report_process_crash(item, result)]
     

对源码进行更改:
子进程中返回allure test result

def forked_run_report(item):
    # for now, we run setup/teardown in the subprocess
    # XXX optionally allow sharing of setup/teardown
    from _pytest.runner import runtestprotocol

    EXITSTATUS_TESTEXIT = 4
    import marshal

    def runforked():
        try:
        	allure_listener = item.config.pluginmanager.get_plugin("allure_listener")
        	uuid = allure_listener._cache.get(item.nodeid) #获取item对应的uuid
        	test_result = allure_listener.allure_logger.get_test(uuid) # 获取item对应的test_result        	
            reports = runtestprotocol(item, log=False)
        except KeyboardInterrupt:
            os._exit(EXITSTATUS_TESTEXIT)
        return marshal.dumps([serialize_report(x) for x in reports]+[test_result]) # 子进程除了返回report外,额外返回allure test result. 注意需要对test_result进行序列化操作

    ff = py.process.ForkedFunc(runforked)
    result = ff.waitfinish()
    if result.retval is not None:
        report_dumps = marshal.loads(result.retval) # 对子进程返回的内容进行反序列化,有与子进程返回的list最后一个元素添加了allure test result,所以此时需要对子进程的返回list处理
        return [runner.TestReport(**x) for x in report_dumps[:-1]], report_dumps[-1] # 最后一个元素为额外返回的allure test result
    else:
        if result.exitstatus == EXITSTATUS_TESTEXIT:
            pytest.exit(f"forked test item {item} raised Exit")
        return [report_process_crash(item, result)], None

父进程中,使用子进程返回的allure test result替换原来的值

@pytest.hookimpl(tryfirst=True)
def pytest_runtest_protocol(item):
    if item.config.getvalue("forked") or item.get_closest_marker("forked"):
        ihook = item.ihook
        ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
        reports, test_result = forked_run_report(item) # 除了report还额外返回了allure test_result
        for rep in reports:
            ihook.pytest_runtest_logreport(report=rep)
        if test_result:
        	allure_listener = item.config.pluginmanager.get_plugin("allure_listener")
        	if allure_listener:
        		uuid = allure_listener._cache.get(item.nodeid)
        		allure_listener.allure_logger.schedule_test(uuid, deserialize_report(test_result)) # test_report进行marshal.load后为dict,需要反序列化为allure_common.model2.TestResult对象
        ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
        return True

序列化及反序列化report和test result

# copied from xdist remote
def serialize_report(rep):
    import py

    d = rep.__dict__.copy()
    if isinstance(rep, TestReport):
	    if hasattr(rep.longrepr, "toterminal"):
	        d["longrepr"] = str(rep.longrepr)
	    else:
	        d["longrepr"] = rep.longrepr
	    for name in d:
	        if isinstance(d[name], py.path.local):
	            d[name] = str(d[name])
	        elif name == "result":
	            d[name] = None  # for now
	elif isinstance(rep, TestResult):
		if hasattr(rep, "attachments"):
			d["attachments"]=str(rep.attachments)
		if hasattr(rep, "labels"):
			d["labels"]=str(rep.labels)
		if hasattr(rep, "links"):
			d["links"]=str(rep.links)
		if hasattr(rep, "steps"):
			d["steps"]=str(rep.steps)
		if hasattr(rep, "parameters"):
			d["parameters"]=str(rep.parameters)
		if hasattr(rep, "statusDetails") and rep.statusDetails:
			d["statusDetails"]=str([rep.statusDetails])
    return d

def deserialze_report(rep_dict):  #将字典反序列化为allure_common.model2.TestResult
	d= rep.copy()
	if "attachments" in d:
		d["attachments"] = eval(d["attachments"])
	if "labels" in d:
		d["labels"] = eval(d["labels"])
	if "links" in d:
		d["links"] = eval(d["links"])
	if "steps" in d:
		d["steps"] = eval(d["steps"])
	if "parameters" in d:
		d["parameters"] = eval(d["parameters"])
	if "statusDetails" in d and d["statusDetails"]:
		d["statusDetails"] = eval(d["statusDetails"])[0]
	test_result = TestResult(**d)
	return test_result

对crash的处理:

def report_process_crash(item, result):
    from _pytest._code import getfslineno

    path, lineno = getfslineno(item)
    info = "%s:%s: running the test CRASHED with signal %d" % (
        path,
        lineno,
        result.signal,
    )
    from _pytest import runner

    # pytest >= 4.1
    has_from_call = getattr(runner.CallInfo, "from_call", None) is not None
    if has_from_call:
        call = runner.CallInfo.from_call(lambda: 0 / 0, "???")
    else:
        call = runner.CallInfo(lambda: 0 / 0, "???")
    #call.excinfo = info 源码代码会报错,所以注释,修改为以下内容:
    call.excinfo.value.args=(info,1)
    #更新allure test result 结果, 添加以下内容
	allure_listener = item.config.pluginmanager.get_plugin("allure_listener")
	if allure_listener:
        	uuid = allure_listener._cache.get(item.nodeid) #获取item对应的uuid
        	test_result = allure_listener.allure_logger.get_test(uuid) # 获取item对应的test_result        	
            test_result.status="failed"
            test_result.statusDetails=StatusDetails(message=info)
	#添加内容结束
    rep = runner.pytest_runtest_makereport(item, call)
    if result.out:
        rep.sections.append(("captured stdout", result.out))
    if result.err:
        rep.sections.append(("captured stderr", result.err))

    xfail_marker = item.get_closest_marker("xfail")
    if not xfail_marker:
        return rep

    rep.outcome = "skipped"
    rep.wasxfail = (
        "reason: {xfail_reason}; "
        "pytest-forked reason: {crash_info}".format(
            xfail_reason=xfail_marker.kwargs["reason"],
            crash_info=info,
        )
    )
    warnings.warn(
        "pytest-forked xfail support is incomplete at the moment and may "
        "output a misleading reason message",
        RuntimeWarning,
    )

    return rep
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值