news 2026/9/15 10:34:23

Chainlit 项目 E2E 测试并行化研究报告

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Chainlit 项目 E2E 测试并行化研究报告

Chainlit 项目 E2E 测试并行化研究报告

【免费下载链接】chainlitBuild Conversational AI in minutes ⚡️项目地址: https://gitcode.com/GitHub_Trending/ch/chainlit

<output_article>

Chainlit E2E 测试并行化改造指南:从严格串行到多分片执行

导读

本文基于 Chainlit 仓库中的研究文档 e2e-parallel-execution.md,系统梳理该仓库 E2E 测试当前"严格串行"的架构根因、Cypress 并行化能力边界,以及两条可行的并行化改造路径。文中所有结论均结合当前仓库中的 cypress.config.ts、run.ts、config.py、e2e-tests.yaml 等真实源码交叉验证——事实上,研究文档提出的"Strategy A:CI 矩阵分片"方案已在当前仓库落地。读完本文,你将掌握 E2E 测试并行化的瓶颈分析思路、分片方案选型依据,以及 Chainlit 仓库的具体落地形态。

当前架构:严格串行及其四个结构性根因

研究文档明确指出:在当前架构下,E2E 测试无法并行运行。这不是配置开关的问题,而是由四个相互叠加的结构性设计共同决定的。

1. 单一硬编码端口8000

每个测试后端都启动在端口8000上。该端口定义在 backend/chainlit/config.py:

DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 8000

它被RunSettings模型作为默认值消费(config.py):

class RunSettings(BaseModel): module_name: Optional[str] = None host: str = DEFAULT_HOST port: int = DEFAULT_PORT ...

而启动测试后端的 cypress/support/run.ts 在调用uv run chainlit run时并未传递--port参数,因此每个后端都绑定到默认端口:

const command = 'uv'; const args = [ '--project', CHAILIT_DIR, 'run', 'chainlit', 'run', entryPointPath, '-h', '--ci' ];

同样的硬编码也出现在 cypress.config.ts 中,并被用作baseUrl

export const CHAINLIT_APP_PORT = 8000; // ... baseUrl: `http://127.0.0.1:${CHAINLIT_APP_PORT}`,

2. 每个 spec 文件的 "kill → 启动 → 测试 → kill" 生命周期

cypress.config.ts 通过 Cypress 的setupNodeEvents注册了严格的进程编排:

on('before:spec', async (spec) => { await killChainlit(); await runChainlit(spec); }); on('after:spec', async () => { await killChainlit(); });

其中killChainlit()是基于端口的进程终止(cypress.config.ts):

async function killChainlit() { await fkill(`:${CHAINLIT_APP_PORT}`, { force: true, silent: true }); }

这里使用fkill(:8000)按端口号杀死进程。研究文档特别指出其风险:如果同时存在两个测试运行器,其中一个会把另一个的后端进程杀掉。这正是串行架构的典型特征——进程生命周期与端口强耦合,天然排斥并发。

此外,配置中还注册了SIGTERM/SIGINT/SIGHUP/SIGBREAK信号处理(cypress.config.ts),确保测试进程退出时也能兜底清理 Chainlit 后端。

3. 每个 spec 拥有独立的 Chainlit 应用

仓库的 E2E 测试采用"一测试一应用"的组织方式:每个测试目录cypress/e2e/<test_name>/下都包含:

  • main.py—— 该测试专用的 Chainlit 应用;
  • .chainlit/config.toml—— 该测试的独立配置。

run.ts 通过spec.absolute定位测试目录,并以环境变量的方式把目录注入 Chainlit:

const testDir = spec ? dirname(spec.absolute) : SAMPLE_DIR; const entryPointFileName = spec ? spec.name.startsWith('async') ? 'main_async.py' : spec.name.startsWith('sync') ? 'main_sync.py' : 'main.py' : 'hello.py'; const entryPointPath = join(testDir, entryPointFileName);

启动时的环境变量注入(run.ts):

const options: SpawnOptionsWithoutStdio = { env: { ...process.env, CHAINLIT_APP_ROOT: testDir } };

由于每个测试的main.py完全不同的后端应用,服务端无法复用,必须在 spec 之间重启。

4. Cypress 按 spec 串行执行

标准的cypress run在单个浏览器进程中一次处理一个 spec 文件。仓库的 npm 脚本也印证了这一点(package.json):

"test:e2e": "cypress run"

研究文档指出:Cypress Cloud 提供跨 CI 机器的并行能力,但当时项目并未启用——脚本中没有--parallel--record标志。

特例:测试中途重启(data_layer)

并非所有测试都遵循"每 spec 一次生命周期"。data_layerspec 会在单个测试内部调用cy.task('restartChainlit', Cypress.spec)来杀进程并重新拉起后端,用于验证线程(thread)在服务端重启后的持久化行为(cypress/e2e/data_layer/spec.cy.ts):

it('Verifies thread continuation after server restart and new thread creation', () => { cy.task('restartChainlit', Cypress.spec).then(() => { cy.section('Before server restart'); // ... 登录、开始对话、验证反馈与线程队列 }); cy.task('restartChainlit', Cypress.spec).then(() => { cy.section('After server restart'); verifyContinueThread(); // ... }); });

对应的restartChainlittask 实现在 cypress.config.ts,同样依赖killChainlit() → runChainlit(spec)的端口级编排:

restartChainlit(spec: Cypress.Spec) { return new Promise((resolve) => { killChainlit().then(() => { runChainlit(spec).then(() => { setTimeout(() => resolve(null), 1000); }); }); }); }

测试依赖的线程数据以thread_history.pickle文件形式落盘在测试目录(见 cypress/e2e/data_layer/main.py),spec 通过cleanupThreadHistory在用例前后清理该文件(spec.cy.ts)。这类"共享文件系统"的副作用,是并行化时必须隔离的隐藏依赖。

Cypress 的并行化能力边界

内置--parallel依赖 Cypress Cloud

Cypress并不免费提供开箱即用的本地并行。官方用法是:

cypress run --record --parallel

关键事实:

  • --parallel必须--record搭配使用,后者会把结果上报到 Cypress Cloud(付费服务);
  • Cypress Cloud 扮演编排者角色:它基于历史运行时长,用负载均衡策略把 spec 文件动态分配给可用的 CI 机器;
  • 因此 Cypress 官方并行是跨机器的多机并行,而非本地的多进程并行。

Cypress 与 Playwright 的并行能力对比

特性CypressPlaywright
本地并行不支持内置(--workers=4
CI 分片通过 Cypress Cloud(付费)内置(--shard=1/4
编排方式基于历史时长的动态分配手动均分

免费的 Cypress 并行替代方案

工具说明
手动 CI matrix 分片用 GitHub Actions matrix 任务配合--spec把 spec 文件拆分到多台机器
sorry-cypress开源自托管的 Cypress Cloud 替代品,支持--parallel
cypress-split通过SPLIT/SPLIT_INDEX环境变量在 CI 机器间拆分 spec 的插件
currents.dev提供免费额度的 Cypress Cloud 替代服务

注:以上外部工具仅作能力概述,具体接入方式请以各工具官方文档为准。当前仓库实际选用的是cypress-split(见下文落地现状)。

并行化改造的瓶颈与对策

研究文档将并行化的障碍与解决方案整理如下:

瓶颈解决方案
硬编码端口8000为每个 worker 分配唯一端口(如8000 + workerIndex),向chainlit run传入--port,并动态配置baseUrl
基于端口的进程终止(fkill(:8000)改为基于 PID 的进程管理——保存spawn()返回的子进程 PID 并直接 kill
单一 Cypress 浏览器进程使用 Cypress Cloud--parallel、sorry-cypress、cypress-split,或在 CI matrix 任务间分片
共享文件系统(.chainlit/目录)目录本身已按测试隔离;但像data_layer写入的thread_history.pickle等临时文件必须保持每个 worker 隔离

Strategy A:CI Matrix 分片(最简单)

把约 50 个 spec 文件按 N 组拆分到独立的 GitHub Actions runner 上:

  1. 在 workflow 中增加 matrix 维度,为每个 runner 分配一组 spec;
  2. 每个 runner 仍可使用端口8000(因为机器彼此独立);
  3. 无需修改 Cypress 配置或run.ts——只需用--spec传入子集。

这条路径的吸引力在于:跨机器天然隔离端口与进程,改动面最小。

Strategy B:同机并行(工作量更大)

  1. CHAINLIT_APP_PORT动态化——按 worker 从环境变量读取;
  2. 用 PID 追踪(spawn返回的child.pid)替换fkill(:port)
  3. 用 cypress-split 或 sorry-cypress 把 spec 分发到多个 Cypress 进程,每个进程拥有各自的baseUrl
  4. 每个 Cypress 进程通过独立的CYPRESS_BASE_URL指向自己专属的后端端口。

同机并行的难点在于前两步:必须同时解决端口冲突与端口级杀进程两个耦合点,改造面大、回归风险高。

当前仓库的落地现状:Strategy A + cypress-split

研究文档之后,仓库实际推进到了实施阶段。证据在 .github/workflows/e2e-tests.yaml:

jobs: prepare: runs-on: ubuntu-slim outputs: indexes: ${{ steps.shard-indexes.outputs.indexes }} steps: - id: shard-indexes name: Compute shard indexes run: | json=$(jq -nc --argjson n "${{ inputs.e2e_parallel_shards }}" '[range(1; $n + 1)]') echo "indexes=$json" >> "$GITHUB_OUTPUT" e2e-tests: needs: prepare strategy: matrix: os: [ubuntu-latest, windows-latest] containers: ${{ fromJSON(needs.prepare.outputs.indexes) }}

工作流通过e2e_parallel_shards输入(默认5)动态生成分片索引,在ubuntu-latestwindows-latest两个 OS 上各起 5 个并行分片。运行测试时注入 cypress-split 需要的环境变量:

env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} SPLIT: ${{ inputs.e2e_parallel_shards }} SPLIT_INDEX1: ${{ matrix.containers }} run: pnpm test:e2e

这正是Strategy A(CI matrix 分片)+ cypress-split(spec 自动切分)的组合形态:

  • 每个 runner 是独立机器,端口8000冲突天然不存在;
  • cypress-split在测试运行前把全部 spec 按SPLIT/SPLIT_INDEX动态分配给各分片,无需手工维护--spec子集;
  • Cypress 配置侧已接入插件(cypress.config.ts):
import cypressSplit from 'cypress-split'; // ... cypressSplit(on, config);

对应依赖在 package.json 中声明:"cypress-split": "^1.24.31"

并行分片下的配套工程:Cypress 二进制缓存

并行分片会放大依赖安装成本(5 个分片 × 2 个 OS 都要装 Cypress),因此 e2e-tests.yaml 使用CYPRESS_CACHE_FOLDER把 Linux / Windows 默认不同的 Cypress 缓存目录统一为仓库工作区下的单一路径,配合actions/cache跨分片复用:

env: # Single path for actions/cache on Linux + Windows (default Cypress dirs differ by OS). CYPRESS_CACHE_FOLDER: ${{ github.workspace }}/.cypress-cache steps: - name: Cache Cypress binary uses: actions/cache@v5 with: path: .cypress-cache key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

这与另一份研究文档 ci-cache-optimization.md 中"用actions/cache替换 cypress action、统一跨 OS 缓存路径"的 P1 建议一脉相承。前端与 Python 依赖的缓存则分别由 .github/actions/pnpm-node-install/action.yaml(actions/setup-node@v6.3.0+cache: 'pnpm')与 .github/actions/uv-python-install/action.yaml(astral-sh/setup-uv@v8.0.0+enable-cache: true)提供,与 CI 顶层编排 ci.yaml 配合。

结论与选型建议

回顾整条改造路径,可以提炼出三点可迁移的经验:

  1. 先识别"串行架构"的结构性耦合。Chainlit 仓库的串行并非单一原因,而是端口、进程生命周期、应用隔离、Cypress 执行模型四个因素叠加。逐一拆解后才能评估并行化的真实成本。
  2. 跨机器分片是低风险首选。Strategy A 以"机器隔离"规避了端口与 PID 管理两大难题,仅引入--spec/cypress-split 的切分逻辑,是投入产出比最高的起步方案——当前仓库正是沿此路径落地,并以CYPRESS_CACHE_FOLDER统一了多分片下的二进制缓存。
  3. 同机并行需要更深的工程改造。若未来需要在单机上压缩时间(如本地开发或小型 runner),就必须同时落地动态端口、PID 级进程管理、多 Cypress 进程与独立baseUrl,并额外隔离thread_history.pickle这类共享文件系统副作用。

研究文档 e2e-parallel-execution.md 的完整分析(含 Cypress/Playwright 对比与免费替代工具矩阵)是理解上述改造的原始依据;感兴趣的读者可继续对照 cypress.config.ts、cypress/support/run.ts 与 .github/workflows/e2e-tests.yaml 追踪从研究到实施的完整演进。

</output_article>

【免费下载链接】chainlitBuild Conversational AI in minutes ⚡️项目地址: https://gitcode.com/GitHub_Trending/ch/chainlit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 10:33:36

3分钟跑通洛伦兹吸引子:Python + Manim 混沌系统可视化指南

3分钟跑通洛伦兹吸引子&#xff1a;Python Manim 混沌系统可视化指南 【免费下载链接】videos Code for the manim-generated scenes used in 3blue1brown videos 项目地址: https://gitcode.com/GitHub_Trending/vi/videos 两个从仅相差 0.00001 的起点出发的点&#…

作者头像 李华
网站建设 2026/9/15 10:33:23

Windows虚拟内存与OOM排查:从pagefile.sys到配置调优指南

我接过很多同事和网友的机器排查请求&#xff0c;十次里有三四次是同一个前奏&#xff1a;内存条明明还有空位&#xff0c;Windows 却弹“系统虚拟内存不足”&#xff0c;或者某个程序跑着跑着直接消失&#xff0c;事件日志里躺着一个大大的 OOM。再加内存条之前&#xff0c;建…

作者头像 李华
网站建设 2026/9/15 10:32:01

管状中空芯光纤拉制参数计算与Matlab标定方法

简介&#xff1a;面向光纤通信与特种光纤制备方向的Matlab计算资源&#xff0c;用于求解管状中空芯光纤拉制过程中的关键参数&#xff0c;帮助研究者与相关专业学生建立起从光纤结构到拉制条件的计算路径。代码采用参数化编程&#xff0c;用户可方便调整几何与工艺参数&#xf…

作者头像 李华
网站建设 2026/9/15 10:31:52

UI自动化测试选型生存指南:6大工具底层逻辑与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 10:29:38

房颤信号特征提取:从RR间期到样本熵的MATLAB实现全流程

简介&#xff1a;基于MATLAB的房颤信号特征提取项目&#xff0c;面向生物医学工程、信号处理专业的研究者与学生&#xff0c;围绕心电图&#xff08;ECG&#xff09;中房颤这一常见心律失常&#xff0c;系统实现从原始信号导入、滤波预处理、R波检测到特征参数提取与分类评估的…

作者头像 李华
网站建设 2026/9/15 10:29:26

DeepSeek Harness 技术详解:从入门到实践

摘要&#xff1a;本文系统介绍 DeepSeek Harness 这一面向大规模推理与模型评测的统一框架。文章从批量推理显存与并发管理、评测一致性和多任务维护等痛点出发&#xff0c;解析其“任务、数据集、模型适配器、推理引擎、评测器”的分层架构与配置驱动设计&#xff0c;并结合环…

作者头像 李华