尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

pytest 退出码完全指南:从 0 到 6 的含义、源码实现与 CI 实战应用

pytest 退出码完全指南:从 0 到 6 的含义、源码实现与 CI 实战应用 pytest 退出码完全指南从 0 到 6 的含义、源码实现与 CI 实战应用【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest导读pytest每次运行结束后都会向操作系统返回一个退出码exit code这是脚本、CI 流水线和测试平台判断测试结果的核心依据。本文基于当前仓库的官方参考文档 doc/en/reference/exit-codes.rst系统讲解 pytest 的 7 种退出码及其含义、ExitCode枚举的源码实现、退出码在命令行与 Python API 两种入口下的产生过程并结合源码与测试用例给出在 CI 脚本、shell 判断和插件开发中的实战用法。退出码总览一次运行、七种结局运行pytest可能产生以下7 种不同的退出码对应测试执行的七种典型结局退出码含义0所有测试都被收集且全部通过1测试被收集并运行但部分测试失败2测试执行被用户中断3执行测试时发生内部错误或某个插件在导入时抛出异常4pytest 命令行用法错误包括找不到插件或conftest.py导入失败5没有收集到任何测试6警告数量超过上限见--max-warnings选项这七种退出码是 pytest 公开 API 的一部分它们在源码中由pytest.ExitCode枚举统一表示可以直接导入使用from pytest import ExitCode从源码看退出码的完整定义ExitCode定义在 src/_pytest/config/init.py它是一个继承自enum.IntEnum的枚举因此每个成员既是符号常量又是真正的整数可以安全地与int直接比较或作为进程退出码返回final class ExitCode(enum.IntEnum): Encodes the valid exit codes by pytest. #: Tests passed. OK 0 #: Tests failed. TESTS_FAILED 1 #: pytest was interrupted. INTERRUPTED 2 #: An internal error got in the way. INTERNAL_ERROR 3 #: pytest was misused. USAGE_ERROR 4 #: pytest couldnt find tests. NO_TESTS_COLLECTED 5 #: All tests pass, but maximum number of warnings exceeded. MAX_WARNINGS_ERROR 6几个值得注意的实现细节版本引入ExitCode枚举自 pytest 5.0 起引入源码 docstring 中标明.. versionadded:: 5.0此前这些数字以裸整数形式散布在代码中如今被统一收敛为带语义名称的公开 API。可扩展性枚举 docstring 明确说明用户和插件也可以提供其他退出码。也就是说ExitCode只是 pytest 自身的官方编码表第三方插件或用户代码完全可以在自定义场景中返回任意其他整数。模块归属__module__ pytest这一行确保ExitCode在文档与内省introspection中以pytest.ExitCode的身份呈现而不是暴露内部的_pytest.config实现路径。退出码与int的双向兼容IntEnum的继承关系带来一个实用特性ExitCode成员可以直接和整数比较也能被int()转换反过来pytest 也允许把任意整数包装回枚举若数值不在枚举定义内则保留原整数。这一点在源码中体现得很直接——见下文命令行入口一节中ExitCode(ret)的用法。七种退出码逐个详解退出码 0OK全部通过所有被收集到的测试均执行成功且警告数量未超过阈值时返回0。这是 CI 中最期望看到的结果。从源码看pytest 在会话初始化时就把退出状态预置为ExitCode.OKsrc/_pytest/main.py只有发生失败、中断、错误等情况时才会被改写。$ pytest # 输出末尾通常为 12 passed in 0.35s $ echo $? 0退出码 1TESTS_FAILED存在失败用例只要有测试失败assertion 失败、异常、xfail 意外通过等pytest 就返回1。其判定逻辑在 src/_pytest/main.py 的_main函数中def _main(config: Config, session: Session) - int | ExitCode | None: config.hook.pytest_collection(sessionsession) config.hook.pytest_runtestloop(sessionsession) if session.testsfailed: return ExitCode.TESTS_FAILED elif session.testscollected 0: return ExitCode.NO_TESTS_COLLECTED return None可以看到判定顺序只要session.testsfailed非零就优先返回TESTS_FAILED只有既没有失败、又没有任何收集到的测试时才返回5。另外wrap_session中捕获到Failed异常时也会把退出状态置为ExitCode.TESTS_FAILED见 src/_pytest/main.py。退出码 2INTERRUPTED被用户中断当测试执行过程中收到KeyboardInterrupt典型场景是用户在终端按CtrlC或调用pytest.exit()时返回2。源码处理位于 src/_pytest/main.pyexcept (KeyboardInterrupt, exit.Exception): excinfo _pytest._code.ExceptionInfo.from_current() exitstatus: int | ExitCode ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): if excinfo.value.returncode is not None: exitstatus excinfo.value.returncode ... config.hook.pytest_keyboard_interrupt(excinfoexcinfo) session.exitstatus exitstatus注意一个细节pytest.exit()如果显式传了returncode则以传入值为准否则默认就是2。这意味着插件可以在pytest_sessionfinish等钩子中通过pytest.exit(reason..., returncode...)定制退出码。退出码 3INTERNAL_ERROR内部错误或插件导入异常这是pytest 自身或插件出问题的信号区别于用户测试代码的问题执行测试期间抛出未预期的BaseException如某些SystemExit、ValueError等被 src/_pytest/main.py 捕获后置为INTERNAL_ERROR并在终端输出INTERNALERROR开头的完整 traceback插件能够被找到、但在导入时抛出异常PluginImportFailure在 src/_pytest/config/init.py 中被捕获并返回ExitCode.INTERNAL_ERROR。源码中PluginImportFailure的 docstring 特意区分了两种情形插件找不到属于用法错误退出码 4插件导入时炸掉则是插件自身的缺陷按内部错误退出码 3处理。测试用例 testing/test_main.py 验证了pytest_sessionstart中抛出异常时返回INTERNAL_ERROR且首行输出INTERNALERROR Traceback ...的行为。退出码 4USAGE_ERROR命令行用法错误pytest 被误用时返回4包括但不限于命令行参数拼写错误或给了非法选项值指定了一个找不到的插件例如pytest -p no_such_pluginconftest.py导入失败ConftestImportFailure。在 src/_pytest/config/init.py 中ConftestImportFailure被捕获后直接返回ExitCode.USAGE_ERRORUsageError异常则在 src/_pytest/main.py 与 src/_pytest/config/init.py 两处兜底处理。这种设计保证了配置层面的错误不会与测试失败混为一谈。退出码 5NO_TESTS_COLLECTED未收集到任何测试运行结束但一个测试都没有收集到时返回5判定逻辑见上文_main中的elif分支。常见触发场景在空目录或路径下运行 pytest测试文件/函数命名不符合 pytest 的收集规则文件需匹配test_*.py或*_test.py函数/方法需以test开头使用了--ignore、-k表达式等过滤掉了全部用例显式使用--collect-only收集但无任何用例。对应测试 testing/test_main.py 断言collected 0 items时返回ExitCode.NO_TESTS_COLLECTED。注意如果收集阶段本身出错例如测试模块导入抛异常pytest 不会静默返回5而是在 src/_pytest/main.py 抛出session.InterruptedN error during collection进而走中断/错误路径避免把收集错误误报成没有测试。退出码 6MAX_WARNINGS_ERROR警告超限这是最特殊的退出码所有测试都通过了但累计警告数量超过阈值时返回6用于强制团队治理代码中的警告污染。触发方式是设置--max-warnings选项$ pytest --max-warnings10该选项在 src/_pytest/main.py 中定义typeint、默认None表示不限并提供了对应的 ini 配置项max_warningssrc/_pytest/main.py二者都支持。实际判定发生在 src/_pytest/terminal.py 的pytest_sessionfinish钩子中max_warnings self._get_max_warnings() if max_warnings is not None and session.exitstatus ExitCode.OK: warning_count len(self.stats.get(warnings, [])) if warning_count max_warnings: session.exitstatus ExitCode.MAX_WARNINGS_ERROR self.write_line( Tests pass, but maximum allowed warnings exceeded: f{warning_count} {max_warnings}, redTrue, )实现要点仅当退出状态仍为OK时才升级为6——如果测试本身已经失败退出码 1则保持1不变避免失败信息被警告淹没。另外_get_max_warningssrc/_pytest/terminal.py会依次读取命令行选项和 ini 配置命令行优先级更高。自定义退出码插件与pytest.exit()ExitCode枚举的设计允许用户和插件介入退出码决策主要有两条途径pytest.exit(reason, returncode...)在测试、fixture 或钩子中主动终止整个会话并指定退出码。不给returncode时默认为2INTERRUPTED 语义显式传值则完全以该值为准。测试 testing/test_main.py 验证了在pytest_sessionfinish中调用pytest.exit(returncode42)会原样返回42。pytest_internalerror钩子处理内部错误时可以返回自定义退出码见 testing/test_main.py 中的示例returncode{returncode!r}。官方文档特别提到一个真实场景如果你希望在没有收集到测试时返回自定义退出码而非默认的5可以考虑使用社区插件pytest-custom_exit_code。这类需求常见于空测试目录应视为失败的 CI 策略。退出码如何从进程传出命令行入口与 Python API要真正理解退出码需要区分 pytest 的两类入口它们的返回路径略有不同。命令行入口pytest命令 /python -m pytest_console_mainsrc/_pytest/config/init.py是 CLI 的真实实现它调用_main得到退出码后作为进程返回值同时针对BrokenPipeError如pytest | head提前关闭管道做了特殊处理重定向 stdout 到/dev/null并返回1与 Python 解释器对 EPIPE 的默认行为保持一致。Python API 入口pytest.main()在进程内编程式运行测试时使用pytest.main(args[...], plugins[...])其返回值就是退出码见 src/_pytest/config/init.py。它的转换逻辑值得注意src/_pytest/config/init.pyret: ExitCode | int config.hook.pytest_cmdline_main(configconfig) try: return ExitCode(ret) except ValueError: return ret即若返回值能映射到ExitCode枚举则转为枚举如pytest.main(...) ExitCode.OK可作布尔判断否则原样返回整数。这再次印证了插件可返回任意自定义退出码的设计意图。pytest_cmdline_main钩子本身则通过wrap_sessionsrc/_pytest/main.py这个会话骨架来组织整个生命周期预置OK→ 配置与pytest_sessionstart→ 执行pytest_collection/pytest_runtestloop→ 在finally中触发pytest_sessionfinish并回传退出状态。退出码的最终值正是在这一流程中被层层计算出来的。CI 与脚本中的实战用法Shell 中检查退出码在 bash 脚本或 CI 步骤中$?是获取退出码最直接的方式pytest tests/ -q status$? if [ $status -eq 0 ]; then echo All tests passed elif [ $status -eq 5 ]; then echo Warning: no tests were collected else echo Tests failed or errored with code $status fi利用退出码 5 的语义可以在 CI 中区分测试全过与根本没有测试两种状态从而在覆盖率门槛或发布门禁上采取不同策略。Python 中编程式判断import sys import pytest ret pytest.main([-q]) if ret pytest.ExitCode.OK: print(All green) elif ret pytest.ExitCode.NO_TESTS_COLLECTED: print(Nothing collected) sys.exit(1) # 把没有测试升级为 CI 失败 sys.exit(ret)常用组合选项速查需求命令/配置失败即停第一个失败后中断pytest -x对应 src/_pytest/main.py 处-x选项最多允许 N 个失败pytest --maxfailNsrc/_pytest/main.py只收集不执行pytest --collect-onlysrc/_pytest/main.py警告数量超限即失败pytest --max-warningsN或 ini 配置max_warnings N收集出错仍继续执行pytest --continue-on-collection-errors在pytest.ini/pyproject.toml/tox.ini中max_warnings也可以静态配置[pytest] max_warnings 50小结pytest 用 0~6 七个退出码把测试结果编码为机器可读的信号0全过、1有失败、2被中断、3内部/插件错误、4用法错误、5无测试、6警告超限。这些语义由pytest.ExitCode枚举在 src/_pytest/config/init.py 中统一定义并通过命令行入口_console_main与 Python APIpytest.main()两条路径传递给调用方插件则可以通过pytest.exit()或pytest_internalerror钩子介入定制。理解这些退出码是编写可靠 CI 脚本、设计插件行为以及排查测试失败但退出码却为 0等疑难问题的基础——本文对应的权威参考位于 doc/en/reference/exit-codes.rst可作为持续查阅的规范文档。【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表