OpenStack 单元测试工作流程详解:tox + testr
tox testr 单元测试工作流程详解从环境隔离到并行执行在 OpenStack 等大型 Python 项目中,tox和testr是一对经典组合:tox 负责环境隔离,testr 负责测试仓库 并行执行 历史结果管理。本文系统讲解两者协作的完整工作流程。一、两个工具各做什么工具全称核心职责toxtox标准化的虚拟环境管理 测试编排testrtestrepository测试仓库管理、并行执行、结果持久化一句话概括:tox解决的是「在干净的虚拟环境里跑测试,避免依赖污染」。testr解决的是「几十万测试怎么并行跑、跑完的结果如何归档复用」。tox ↓ 创建/复用虚拟环境 ↓ 执行 commands python setup.py testr --slowest --coverage --testr-args--subunit ↓ 调用 pbr 的 testr 入口 testr run ↓ 读取 .testr.conf ↓ 根据 test_command 启动测试进程 python -m subunit.run discover -t ./ ./tests/unit ↓ 执行测试用例输出 subunit v1 流 ↓ testr 捕获流 ↓ 写入 .testrepository/{next_number} ↓ 合并历史结果 testr last --subunit ↓ 输出最新一次运行的 subunit 流 subunit-trace --no-failure-debug -f ↓ 转换为人类可读的实时输出两者解耦 —— tox 可以单独跑 pytest/nose,testr 也可以独立于 tox 使用。组合起来就形成「环境隔离 高性能并行 结果仓库」的完整闭环。二、tox 工作原理2.1 核心机制tox 读取tox.ini(或pyproject.toml中的[tool.tox]),按其中定义的testenv创建虚拟环境、安装依赖、执行命令。tox.ini ├── [tox] 全局配置 ├── [testenv] 默认环境模板 ├── [testenv:py311] 具名环境:Python 3.11 ├── [testenv:pep8] 具名环境:代码风格检查 └── [testenv:cover] 具名环境:覆盖率2.2 执行步骤1. 解析 tox.ini,确定要运行的 env(默认 [testenv],或 -e 指定) 2. 在 .tox/envname/ 下创建 venv 3. 安装 deps ... 列出的依赖 4. 安装项目本身(pip install .) 5. 执行 commands ... 中的命令 6. 收集退出码,汇总结果2.3 典型 tox.ini[tox] envlist py39,py311,pep8,cover skipsdist False [testenv] usedevelop True setenv VIRTUAL_ENV{envdir} LANGen_US.UTF-8 deps -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands python -m testr run {posargs} [testenv:pep8] deps flake8 commands flake8 {toxinidir}/myproject [testenv:cover] setenv {[testenv]setenv} PYTHONcoverage run --source myproject -m testr run commands coverage erase {env:PYTHON} coverage report coverage html注意commands python -m testr run—— 这就是 tox 把测试执行权交给 testr 的关键。[tox] envlist py27,cover skipsdist False [testenv] deps -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt testrepository subunit [testenv:cover] setenv VIRTUAL_ENV{envdir} NOSE_WITH_COVERAGE1 commands python setup.py testr --slowest --coverage \ --testr-args--subunittox -ecover --sitepackages -v 各参数含义参数含义-e cover选择执行 cover 环境–sitepackages复用系统已安装的包不重新虚拟化所有依赖-v详细输出三、testr 工作原理3.1 核心概念:testrepositorytestr 在项目根目录维护一个.testr/目录,里面存放:.testr/ ├── config # testr 配置 ├── last # 上次运行的 subunit 流 ├── slowest # 最慢测试列表 ├── failing # 最近失败的测试 └── history # 历史运行记录这是一个持久化的测试仓库,不只是一次性的执行框架。3.2 subunit 协议testr 的核心数据格式是subunit—— 一种二进制/文本混合的测试结果流协议,兼容 xUnit 但更丰富。test: mypackage.tests.test_foo.TestFoo.test_bar success: mypackage.tests.test_foo.TestFoo.test_bar (1.234s)特点:流式传输,边跑边出结果支持多进程汇流(并行测试的多个 worker 写入同一流)兼容 Pythonunittest、pytest、nose等主流框架3.3 .testr.conf[DEFAULT] test_command python -m subunit.run discover -t . $TESTS_DIR test_list_command python -m subunit.run discover -t . $TESTS_DIR --list group_regex ([^\.]\.) test_run_concurrency 1test_command:实际执行测试的命令,subunit.run 是 subrun 的入口,负责输出 subunit 流test_list_command:列出所有可用测试 ID(用于过滤、并行分片)group_regex:把测试按前缀分组,用于并行调度test_run_concurrency并行度3.4 核心参数详解python setup.py testr --slowest --coverage --testr-args--subunit参数含义–slowest测试结束后显示最慢的 10 个用例–coverage自动启用 coverage生成 .coverage 文件–testr-args“–subunit”透传给 testr run让 testr 输出 subunit 流--coverage的工作机制testr 在执行前向 test_command 注入 coverage 包装等价于 python -m coverage run --source -m subunit.run discover …每个测试进程会写自己的 .coverage.{pid} 文件后续脚本 coverage combine 合并所有进程的覆盖率数据四、完整工作流程把 tox 和 testr 串起来,一次tox -e py311的内部流程:┌─────────────────────────────────────────────────────────────┐ │ Step 1: tox 解析 tox.ini,确定运行 envpy311 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 2: tox 在 .tox/py311/ 创建虚拟环境 │ │ (若已存在且无 skip-install,直接复用) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 3: pip install -r requirements.txt │ │ pip install -r test-requirements.txt │ │ pip install -e . (项目本身,usedevelopTrue) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 4: 执行 commands: python -m testr run │ │ 此刻控制权从 tox 转交给 testr │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 5: testr 读取 .testr/config 获取 test_command │ │ 读取 .testr/history 准备 failing-first 调度 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 6: testr 调用 test_list_command 列出全部测试 ID │ │ 按 group_regex 切分,分发给 N 个并行 worker │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 7: 每个 worker 执行: │ │ python -m subunit.run discover -t . $TESTS_DIR test_ids │ │ 输出 subunit 流到 stdout │ └─────────────────────────────────────────────────────┬───────┘ │ ┌───────────────────────────────────────────────┘ │ (多个 worker 的 subunit 流汇入) ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 8: testr 把 subunit 流 tee 到: │ │ - .testr/last (上次完整流) │ │ - .testr/history (累积历史,供下次调度) │ │ - 实时渲染到终端 (subunit2py 或 subunit2html) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 9: testr 统计 pass/fail/skip,以 exit code 返回 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 10: tox 接收到 exit code, │ │ 非 0 则标记 env 失败,汇总到最终结果 │ └─────────────────────────────────────────────────────────────┘五、testr 常用命令# 初始化测试仓库(项目根目录)testr init# 运行全部测试testr run# 运行指定测试(支持正则)testr run mypackage.tests.test_foo# 只跑上次失败的测试(failing-first,加速回归)testr run--failing# 查看上次运行结果testr last# 查看 N 次运行中最慢的测试testr slowest# 列出上次失败的测试testr failing# 在上次失败的基础上再次跑testr run--subunit|subunit-filter --no-success|subunit2pyunit# 把上次的 subunit 流转成 HTML 报告testr last--subunit|subunit2html output.html六、并行执行机制testr 的并行能力是其最大价值之一。流程:1. testr 调用 test_list_command 获取全部 test ID 2. 按 group_regex 把测试分成若干「组」(同组测试不会拆到不同 worker,避免共享 fixture 冲突) 3. 启动 N 个 worker 进程(默认 CPU 核数) 4. 每个 worker 拿到自己的测试子集,通过 test_command 执行 5. 各 worker 的 subunit 流汇入 testr 的总流 6. testr 实时把汇总流写入 .testr/last 和终端配置并发数:# 通过环境变量TESTRUN_CONCURRENCY8testr run# 或在 .testr.conf 中指定[DEFAULT]test_concurrency8注意:并行要求测试之间无共享状态/全局 fixture。如果用setUpClass/setUpModule做数据库初始化,多 worker会各自跑一次,可能造成连接数爆炸。七、subunit 流的处理subunit 是 testr 体系的「血液」。常用管道命令:# 把上次结果中失败的测试抽出来再跑testr last--subunit|subunit-filter --no-success|subunit-run# 实时把 subunit 流转换成人类可读输出testr run--subunit|subunit2pyunit# 转 HTMLtestr run--subunit|subunit2html report.html# 转 JUnit XML(兼容 CI)testr run--subunit|subunit2junitxml junit.xml# 过滤出只跑某个 packagetestr run--subunit|subunit-filter --name-patternmypackage.tests.test_db|subunit-run这是 testr 区别于 pytest 的关键:测试结果是一段可流动的数据,不是固定的报告文件。你可以任意接管道处理。八、最小可运行示例8.1 项目结构myproject/ ├── myproject/ │ ├── __init__.py │ └── foo.py ├── myproject/tests/ │ ├── __init__.py │ ├── test_foo.py ├── tox.ini ├── .testr.conf ├── requirements.txt └── test-requirements.txt8.2 test-requirements.txttestrepository subunit fixtures testtools8.3 .testr.conf[DEFAULT] test_command python -m subunit.run discover -t . myproject/tests $LISTOPT test_list_command python -m subunit.run discover -t . myproject/tests --list8.4 tox.ini│ Step 5 : testr 读取 .testr/config 获取 test_command │ │ 读取 .testr/history 准备 failing-first 调度 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 6 : testr 调用 test_list_command 列出全部测试 ID │ │ 按 group_regex 切分,分发给 N 个并行 worker │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 7 : 每个 worker 执行: │ │ python -m subunit.run discover -t . $TESTS_DIR test_ids │ │ 输出 subunit 流到 stdout │ └─────────────────────────────────────────────────────┬───────┘ │ ┌───────────────────────────────────────────────┘ │ (多个 worker 的 subunit 流汇入) ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 8 : testr 把 subunit 流 tee 到: │ │ - .testr/last (上次完整流) │ │ - .testr/history (累积历史,供下次调度) │ │ - 实时渲染到终端 (subunit2py 或 subunit2html) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 9 : testr 统计 pass/fail/skip,以 exit code 返回 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Step 10 : tox 接收到 exit code, │ │ 非 0 则标记 env 失败,汇总到最终结果 │ └─────────────────────────────────────────────────────────────┘九、failing-first:核心加速技巧testr 的.testr/history记录了每个测试最近的通过/失败状态。下次跑时:testr run --failing会优先调度上次失败的测试,先跑、先报错。如果开发者在改动的就是那部分代码,几秒就能看到红;如果之前的失败已经修好,剩余测试再继续跑。这相当于把「全量回归」的反馈时间从「全部跑完才看到失败」变成「失败先暴露」,在大型项目里收益巨大。十、常见坑坑 1:testr init未执行第一次跑testr run会报No testrepository found。# 解决testr inittox.ini里可以加一行自动初始化:commands testr init testr run {posargs}坑 2:并行下 fixture 冲突例如setUpClass里建表,多个 worker 同时建,数据库报错。解决:把不可并行的 fixture 改成模块级单例 锁或降低test_concurrency1退回串行或用group_regex把相关测试归到同一 worker坑 3:subunit.run找不到测试subunit.run discover默认从当前目录发现,如果TESTS_DIR没设对,会跑空。[DEFAULT] test_command python -m subunit.run discover -t . myproject/tests $LISTOPT # ^^^^^^^^^^^^^^^^^ 明确指定目录坑 4:覆盖率和 testr 一起跑不能直接coverage run testr run,因为 testr 会 fork 子进程,coverage 抓不到。正确做法:[testenv:cover] setenv PYTHONcoverage run --source myproject -m testr run commands coverage erase {env:PYTHON} coverage report把 coverage 作为python 解释器前缀注入,让每个 worker 都在 coverage 下跑。坑 5:Windows 兼容性testr 在 Windows 上偶发问题(并行进程、路径分隔符)。如果必须在 Windows 跑,建议:[testenv] setenv TESTRUN_CONCURRENCY1退回串行,稳定性优先。十一、和 pytest 的对比维度tox testrpytest pytest-xdist环境隔离tox 原生支持需配合 tox 或 nox并行testr 调度,基于 subunitpytest-xdist,worker 进程结果持久化.testr/history,可 failing-first需 pytest-cache 或 --lf流式协议subunit,可流式管道处理原生流式但不可管道组合生态OpenStack 系,偏传统现代主流配置复杂度高(tox.ini .testr.conf)低(pyproject.toml 一处)学习曲线陡平建议:老项目、OpenStack 系、需要 subunit 流生态 → 继续 tox testr新项目、Python 3.10、追求简洁 →tox pytest或nox pytest十二、最佳实践清单.testr/加入.gitignore,但.testr.conf提交CI 中先testr init再tox,避免空仓库报错本地用--failing做快速回归,CI 用全量跑group_regex一定要设,否则并行下共享 fixture 测试会炸覆盖率和并行分开跑,coverage 走单独 envsubunit2html产物归档,方便回溯历史失败不要在 test_command 里写死 worker 数,用环境变量注入,方便 CI 调定期清理.testr/history,长期累积会变慢CI 用testr last --subunit | subunit2junitxml生成 JUnit 报告,Jenkins/GitLab 原生解析大型项目把单元/集成测拆成不同 env,testr 只管单测,集成测用别的工具十三、速查表# 项目初始化 testr init # 全量跑 tox -e py311 # 只跑失败 tox -e py311 -- --failing # 跑指定测试 tox -e py311 -- mypackage.tests.test_foo.TestBar.test_baz # 看上次结果 testr last # 看最慢的 20 个 testr slowest # 转 HTML testr last --subunit | subunit2html report.html # 转 JUnit XML testr last --subunit | subunit2junitxml junit.xml # 重置仓库 rm -rf .testr testr init十四、结语tox testr 的组合,核心是三层解耦:工具优势tox隔离测试环境避免污染系统 Python–sitepackages 复用预装包加速testr支持测试并行按 group_regex 分组自动重跑失败用例subunit 流归档可复现subunit与语言无关的标准测试流格式能被多种工具trace/junitxml/html二次加工这套组合是 OpenStack 生态的标准实践脚本能复用现成的 subunit2junitxml、subunit2html、xunitmerge 工具链。理解了这三层,后续无论是接入 CI、做覆盖率、生成 HTML 报告,都是「在管道上接插件」的事。testr 在新项目里被 pytest取代是大势所趋,但读懂它的设计,对理解大型 Python 项目的测试架构仍有价值。

相关新闻

半导体工艺中氧化铝刻蚀技术解析与应用

半导体工艺中氧化铝刻蚀技术解析与应用

1. 半导体工艺中的氧化铝刻蚀概述 在半导体制造工艺中,氧化铝(Al₂O₃)作为一种重要的介电材料,因其优异的绝缘性能、高热稳定性和化学惰性而被广泛应用。但正是这些优良特性,使得氧化铝的刻蚀成为工艺中的一大挑战。与…

2026/7/18 19:49:59阅读更多 →
C++ 内存安全之争:Rust 崛起下的应对与进化

C++ 内存安全之争:Rust 崛起下的应对与进化

当“安全”成为系统编程的必选项内存安全漏洞——野指针、缓冲区溢出、释放后使用——几十年来一直占据 CVE 排行榜的前列,更不用说由此引发的无数安全事件。C 和 C 凭借零开销抽象称霸系统编程数十年,但“不安全”的标签也越来越刺眼。而 Rust 携所有权…

2026/7/18 19:49:59阅读更多 →
EMI频谱图解析与电磁干扰诊断实战指南

EMI频谱图解析与电磁干扰诊断实战指南

1. 什么是EMI频谱图?EMI(Electromagnetic Interference)频谱图是电磁兼容性测试中最直观的数据呈现形式。它就像一张电磁环境的"体检报告",横轴代表频率范围,纵轴表示干扰信号的强度,通常以dBμV…

2026/7/18 19:49:59阅读更多 →
当拖车钩成为“照妖镜”,江铃集团新能源易至汽车为何敢做“逆行者”?

当拖车钩成为“照妖镜”,江铃集团新能源易至汽车为何敢做“逆行者”?

近日,一段“拖车一拉,保险杠防撞梁全掉”的视频引发热议,再次将部分新能源车企“悄悄减配”推至风口浪尖。成本与竞争双重压力下,一些品牌选择在消费者不易察觉的细节上动手,从拖车钩到防撞梁,从车身钢材到…

2026/7/18 20:44:21阅读更多 →
思源宋体CN:开源字体工程中的字形设计与渲染优化技术深度解析

思源宋体CN:开源字体工程中的字形设计与渲染优化技术深度解析

思源宋体CN:开源字体工程中的字形设计与渲染优化技术深度解析 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 在数字化排版领域,中文字体的技术实现复杂度远超西…

2026/7/18 20:44:21阅读更多 →
思源宋体TTF版本:免费开源中文排版解决方案的完整实战指南

思源宋体TTF版本:免费开源中文排版解决方案的完整实战指南

思源宋体TTF版本:免费开源中文排版解决方案的完整实战指南 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 还在为商业项目寻找专业又免费的中文字体而烦恼吗?So…

2026/7/18 20:44:21阅读更多 →
暑期实践日志 Day5:完成开题报告提交,继续学习视频剪辑操作

暑期实践日志 Day5:完成开题报告提交,继续学习视频剪辑操作

一、今日目标 今天是课题准备的最后一天,首要任务是整理、完善并正式提交本课题的暑期实践开题报告,敲定课题整体规划、实践方向和后续进度安排。 同时按照昨天制定的学习计划,推进一下剪映实操练习,在掌握字幕、降噪、画面裁剪的…

2026/7/18 20:44:21阅读更多 →
EP_局域网传输音视频的主流方案

EP_局域网传输音视频的主流方案

EP:Engineering and Project 一、实时低延迟流媒体(直播 / 实时对讲首选) RTSP RTP/RTCP 架构:RTSP 做信令控制(握手、启停流),RTP 承载音视频裸流,RTCP 做丢包 / 延时反馈底层&…

2026/7/18 20:44:21阅读更多 →
AI时代企业哪家强?深度对比评测:广州裕富科技如何用四大模型颠覆直播行业

AI时代企业哪家强?深度对比评测:广州裕富科技如何用四大模型颠覆直播行业

引言:AI时代的十字路口,企业如何选对合作伙伴? 2025年,AI技术已从概念走向全面落地。无论是电商直播、短视频营销,还是企业数字化运营,“AI智播”、“智能体”等词汇频繁出现在商业决策者的视野中。然而&am…

2026/7/18 20:42:20阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/18 10:49:13阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/18 8:49:08阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/18 14:49:24阅读更多 →
从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

更多请点击: https://kaifayun.com 第一章:从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则 在Claude驱动的产品需求文档(PRD)生成实践中,原始业务意图往往以自然语言片…

2026/7/18 0:00:14阅读更多 →
Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

更多请点击: https://codechina.net 第一章:Cursor配置生成失效?3大隐藏陷阱4行修复代码,资深工程师连夜整理的紧急补救清单 Cursor 配置生成突然失效,是近期高频报障场景。表面看是 cursor.config.json 未更新或 LSP…

2026/7/18 0:00:14阅读更多 →
某智驾大牛创业

某智驾大牛创业

作者:钟声编辑:Mark出品:红色星际头图:智能驾驶图片据悉,国内某头部智驾公司端到端模型技术大牛Z投身创业,并且已经拿到融资。Z不仅是该头部公司内部最年轻的对标阿里P10级别技术负责⼈,更是业内…

2026/7/18 0:00:14阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/17 22:48:46阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/18 14:49:24阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/18 18:49:35阅读更多 →