当前位置: 首页 > 后端技术 > Python

为什么使用PyCharm运行测试用例成功却无法退出?

时间:2023-03-26 19:25:49 Python

本文同步发布于字节谈云公众号。前言前段时间,由于项目使用的某个SDK升级,使用PyCharm+unittest运行用例时,可以运行并输出结果,但一直无法退出用例。随着排查的深入,发现是这个SDK中的线程在“作祟”。使用简单代码重现为简单起见,以下代码(Python2)包含简单的线程逻辑和重现所遇到问题的用例:#coding:utf-8importthreadingimporttimeimportunittestdeftick():whileTrue:print('tick')time.sleep(3)t=threading.Thread(target=tick)t.start()classTestString(unittest.TestCase):deftest_upper(self):self.assertEqual('foo'.upper(),'FOO')此代码启动一个线程并每3秒输出一次滴答声。另一方面,定义了一个用例来判断字符串的upper()方法。如果线程逻辑被删除,用例可以正常结束;相反,PyCharm显示用例执行成功,但是一直无法退出用例,如下图:为什么不能退出?在运行用例之前,必须打开一个新线程来执行tick()函数。由于该函数使用while循环不断输出字符串,不难推断,用例框架在退出时一直在等待线程结束,导致用例无法退出。为了验证这个思路,我们来看一下PyCharm运行用例的入口代码。不同的操作系统、PyCharm(社区版、专业版)和测试用例入口文件路径在单体测试框架下是不同的。Mac上的PyCharm社区版针对unittest的用例入口文件路径为"/Applications/PyCharmCE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_unittest_runner.py",该文件内容如下:#coding=utf-8importosimportsysfromunittestimportmainfrom_jb_runner_toolsimportjb_start_tests,jb_doc_args,JB_DISABLE_BUFFERING,PROJECT_DIRfromteamcityimportunittestpyif__name__=='__main__':path,targets,additional_args=jb_start_tests()args=["python-munittest"]如果路径:assertos.path.exists(path),"{0}:Nosuchfileordirectory".format(path)ifsys.version_info>(3,0)andos.path.isfile(path):#在Py3中可以直接运行脚本,这比发现机制稳定得多#例如它支持文件名中的连字符PY-23549additional_args=[path]+additional_argselse:discovery_args=["discover","-s"]#py2中的Unittest不支持支持直接运行脚本(以及py2和py3中的文件夹),#但它可以使用“发现”来查找某个文件夹中的所有测试(可选地通过脚本过滤)ifos.path.isfile(path):discovery_args+=[os.path.dirname(path),"-p",os.path.basename(path)]else:discovery_args.append(path)discovery_args+=["-t",PROJECT_DIR]#强制单元计算相对于此文件夹的路径additional_args=discovery_args+additional_argseliftargets:additional_args+=targetsargs+=additional_argsjb_doc_args("unittests",args)#工作目录应该在路径上,这就是从sysmand行启动时unittest的工作方式sys.path.insert(0,PROJECT_DIR)逻辑beforeexit(main(argv=args,module=None,testRunner=unittestpy.TeamcityTestRunner,buffer=notJB_DISABLE_BUFFERING))主要是结合运行用例的参数。本文遇到的问题最关键的是最后一行main(argv=args,module=None,testRunner=unittestpy.TeamcityTestRunner,buffer=notJB_DISABLE_BUFFERING),这里主要是unittest.TestProgram,相关核心内容如下:classTestProgram(object):"""运行一组测试的命令行程序,主要是为了方便制作测试模块executable."""USAGE=USAGE_FROM_MODULE#默认测试failfast=catchbreak=buffer=progName=Nonedef__init__(self,module='__main__',defaultTest=None,argv=None,testRunner=None,testLoader=loader.defaultTestLoader,exit=True,verbosity=1,failfast=None,catchbreak=None,buffer=None):...self.退出=退出...自我。parseArgs(argv)自我。runTests()defrunTests(self):...self.result=testRunner.run(self.test)ifself.exit:sys.exit(notself.result.wasSuccessful())当PyCharm的_jb_unittest_runner.py调用main()(即TestProgram()),没有输入退出参数,所以取默认值True,最后指定runTests()运行用例,根据用例的结果决定退出退出代码(0或1),然后调用sys.exit()退出用例执行过程。如果在这里打断点,你会发现自己一直卡在这句话里。sys.exit()的作用是退出当前线程。如果其他线程还没有结束,进程自然不会结束。很明显,tick函数所在的线程并没有被显式退出,导致了用例运行成功但无法退出的现象。如何解决?既然明白了原因,解决方案也就呼之欲出了。方法一:运行用例时不执行线程逻辑如果用例不需要执行周期性任务的线程逻辑,可以通过环境变量、配置文件等方式控制,不执行线程逻辑当用例运行时,从而防止用例无法退出。方法二:显式退出进程而不是线程使用os._exit(n)退出进程。需要注意的是,该方法不会调用清理逻辑,刷新标准IO缓存等,一般用在fork()之后的子进程中。由于单元测试对流程没有特殊要求,所以这里的测试用例一般不会产生副作用。我们可以简单修改_jb_unittest_runner.py的最后一个逻辑,显式指定exit=False,即不让unittest调用sys.exit(),而是在外部调用os._exit()。prog=main(argv=args,module=None,testRunner=unittestpy.TeamcityTestRunner,buffer=notJB_DISABLE_BUFFERING,exit=False)os._exit(notprog.result.wasSuccessful())方法三:优雅退出线程_jb_unittest_runner。py在用例结束时向当前进程发送SIGKILL信号。当用例线程收到此信号时,它会执行清理逻辑(如果需要)以优雅地退出,然后退出进程。这种方法将在另一篇文章中详细讨论,本文只需要了解这种思路即可。总结如果通过PyCharm执行用例时触发运行周期性任务的线程逻辑,会导致用例完成但无法退出。原因是sys.exit()用于退出当前线程而不是进程。如果一个线程不退出,就会导致进程无法退出。解决方案只有三种,不执行线程逻辑,退出进程,或者优雅退出线程。