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

资讯详情

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

Vitest 自定义 Pool(Custom Pool)完全指南:基于 `PoolRunnerInitializer` 编写私有测试运行池

Vitest 自定义 Pool(Custom Pool)完全指南:基于 `PoolRunnerInitializer` 编写私有测试运行池 Vitest 自定义 PoolCustom Pool完全指南基于PoolRunnerInitializer编写私有测试运行池【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitestVitest 通过Pool池来调度和执行测试文件内置了threads、forks、vmThreads、browser、typescript等运行器。当内置池无法满足特定运行场景例如自定义进程模型、私有调度策略、非标准执行环境时Vitest 暴露了实验性的低层 API允许你通过PoolRunnerInitializer编写自己的池运行器。本文以 docs/guide/advanced/pool.md 为核心结合仓库源码packages/vitest/src/node/pools/与packages/vitest/src/runtime/workers/深入讲解自定义池的完整实现链路从配置接入、生命周期接口到主进程与 worker 之间的消息协议与调度原理。::: warning 前置警告 这是一套advanced、实验性且非常底层的 API。如果你只是想运行测试绝大多数情况下并不需要它。本文内容主要面向库作者library authors与需要深度定制 Vitest 运行时的开发者。官方仓库同时提供了一个vitest-pool-example示例包可作为自定义池运行器实现的最佳参考起点。 :::一、Pool 是什么Vitest 的测试执行单元Vitest 将测试文件的执行工作交给Pool完成。Pool 决定了两件事测试代码跑在什么样的进程/线程模型里以及主进程与执行体之间如何通信。仓库中内置了如下几种池运行器Pool 名称底层技术隔离机制threadsnode:worker_threads通过全新的 worker context 提供隔离forksnode:child_process通过新的child_process.fork进程提供隔离vmThreadsnode:worker_threads使用vm模块提供隔离而非新 worker contextbrowser浏览器 Provider通过浏览器提供隔离typescript类型检查器对测试执行类型检查typechecking这五类池的运行器实现可以在 packages/vitest/src/node/pools/workers/ 目录中看到——threadsWorker.ts、forksWorker.ts、vmThreadsWorker.ts、vmForksWorker.ts、typecheckWorker.ts各自对应一种运行模型它们实现了统一的PoolWorker接口见 types.ts。需要强调的是Pool 是项目级project-level的配置。在test.pool中配置的值会默认应用于该项目下的所有测试文件若要在不同测试文件集合中使用不同池则通过projects特性组合多个项目分别配置。二、接入方式pool配置项与projects组合2.1 全局替换用自定义池运行所有文件pool选项的类型定义在 config.ts它接受内置池名称threads | forks | vmThreads | vmForks或一个PoolRunnerInitializer函数。将自定义初始化器直接传给pool即可让每个测试文件默认走你的自定义池import { defineConfig } from vitest/config import customPool from ./my-custom-pool.ts export default defineConfig({ test: { // 默认情况下每个文件都会用自定义池运行 pool: customPool({ customProperty: true, }) }, })2.2 分区组合不同池运行不同测试如果只是部分测试需要自定义池请使用projects特性。每个 project 拥有独立的test配置Vitest 会按 project 分别解析执行环境import customPool from ./my-custom-pool.ts export default defineConfig({ test: { projects: [ { test: { pool: threads, // 普通测试继续用内置线程池 }, }, { test: { pool: customPool({ customProperty: true, // 特殊测试走自定义池 }) } } ], }, })这样你可以保留默认池的高性能执行同时将需要特殊运行模型的测试文件例如需要自定义全局状态、私有沙箱或专用通信通道的用例隔离到自定义池中。三、主进程侧 APIPoolRunnerInitializer与PoolWorker3.1 工厂函数PoolRunnerInitializer自定义池的本质是一个返回PoolRunnerInitializer的函数。PoolRunnerInitializer的类型定义如下见 types.tsexport interface PoolRunnerInitializer { readonly name: string createPoolWorker: (options: PoolOptions) PoolWorker }name自定义池运行器的名称必须与你的 worker 中的name属性完全一致——主进程正是通过这个名字来匹配自定义池的见下文调度匹配部分。createPoolWorker(options)接收PoolOptions返回一个PoolWorker实例。PoolOptions包含了该次任务的关键上下文types.tsdistPathVitest 产物路径、project当前测试项目、methodrun或collect、cacheFs、environment、execArgv、env等。一个最小实现如下完整见 docs/guide/advanced/pool.mdimport type { PoolRunnerInitializer } from vitest/node export function customPool(customOptions: CustomOptions): PoolRunnerInitializer { return { name: custom-pool, createPoolWorker: options new CustomPoolWorker(options, customOptions), } }3.2 Worker 生命周期接口PoolWorkerPoolWorker是主进程眼中一个可执行任务的 worker的抽象你的CustomPoolWorker需要实现全部必需方法见 types.tsimport type { PoolOptions, PoolWorker, WorkerRequest } from vitest/node class CustomPoolWorker implements PoolWorker { name custom-pool // 必须与 PoolRunnerInitializer.name 一致 private customOptions: CustomOptions constructor(options: PoolOptions, customOptions: CustomOptions) { this.customOptions customOptions } send(message: WorkerRequest): void { // 提供向 worker 发送消息的方式 } on(event: string, callback: (arg: any) void): void { // 提供监听 worker 事件的方式例如 message、error、exit } off(event: string, callback: (arg: any) void): void { // 提供取消 on 监听的方式 } async start() { // worker 启动时要做什么 } async stop() { // 清理状态 } deserialize(data) { return data } }各方法职责梳理start()/stop()分别负责 worker 的创建启动与销毁清理。从源码看Pool会在启动 worker 时设置 90 秒启动超时WORKER_START_TIMEOUTPoolRunner内部则对start/stop响应分别有 60 秒超时见 poolRunner.ts超时会以[vitest-pool]: Timeout starting/terminating ...报错。send(message)向 worker 发送WorkerRequest消息start、stop、run、collect、cancel五种类型见 types.ts。on/off(event, callback)订阅 worker 的messageworker 回传的WorkerResponse、error、exit事件。PoolRunner依赖message事件判断started/testfileFinished/stopped等关键响应。deserialize(data)将底层通道收到的原始数据反序列化为可识别的消息对象默认透传。3.3 你掌控调度与通信你的CustomPoolWorker将全权控制自定义测试运行器 worker 的生命周期与通信通道。例如你的实现可以启动一个node:worker_threads的Worker并通过Worker.postMessage与parentPort建立双向通信也可以 fork 一个子进程、连接 WebSocket、甚至复用某个远程执行服务——只要兑现PoolWorker的契约Vitest 主进程并不关心通道背后的实现细节。四、Worker 侧 API从vitest/worker导入辅助工具在 worker 文件中你可以从vitest/worker导入运行时辅助函数该入口由 public/worker.ts 导出init、runBaseTests、setupEnvironmentimport { init, runBaseTests, setupEnvironment } from vitest/worker init({ post: (response) { // 提供将此消息作为 message 事件发送给 CustomPoolRunner 的 onWorker 的方式 }, on: (callback) { // 提供监听 CustomPoolRunner postMessage 调用的方式 }, off: (callback) { // 可选提供移除由 on 添加的监听器的方式 }, teardown: () { // 可选提供 teardown worker 的方式例如取消所有 on 监听器 }, serialize: (value) { // 可选为 post 调用提供自定义序列化器 }, deserialize: (value) { // 可选为 on 回调提供自定义反序列化器 }, runTests: (state, traces) runBaseTests(run, state, traces), collectTests: (state, traces) runBaseTests(collect, state, traces), setup: setupEnvironment, })4.1init做了什么init是 worker 端的协议入口实现在 packages/vitest/src/runtime/workers/init.ts。它把你的on回调注册为消息处理器然后内部维护一个状态机循环处理WorkerRequeststart设置VITEST_POOL_ID、VITEST_WORKER_ID环境变量初始化 OpenTelemetry Traces然后调用你传入的setup即setupEnvironment执行测试环境安装完成后回发{ type: started }。run/collect调用你传入的runTests/collectTests内部即runBaseTests(run | collect, ...)执行完毕后通过post回发{ type: testfileFinished, error, usedMemory }。stop等待当前运行结束执行运行时 teardown、持久化编译缓存Module.flushCompileCache回发{ type: stopped }最后触发你提供的teardown。此外init还内置了防重入保护若 worker 已在运行中又收到新的run/collect会直接回发testfileFinished并携带[vitest-worker]: Worker is already running tests错误避免并发执行造成状态污染。4.2runBaseTests与setupEnvironmentrunBaseTests(method, state, traces)真正的测试执行器入口负责按run/collect两种模式驱动测试运行定义于 packages/vitest/src/runtime/runBaseTests.ts内部会启动 Module RunnerTestModuleRunner见 packages/vitest/src/runtime/moduleRunner/。setupEnvironment即setupBaseEnvironment安装测试环境定义于 packages/vitest/src/runtime/workers/base.ts。它负责按配置选择 Vite Module Runner 或 Native Module Runner、加载测试环境loadEnvironment、安装 Chai 配置与错误捕获监听listenForErrors等并返回一个 teardown 函数。4.3 默认实现的对照参考内置池的 worker 端实现是最好的对照教材threads与forks的 worker 入口分别位于 packages/vitest/src/runtime/workers/threads.ts 与 packages/vitest/src/runtime/workers/forks.ts它们都用init(...)包装相同的post/on/off/runTests/collectTests/setup参数差异只在通信原语MessagePortvsprocess.send。阅读这些文件可以直观理解自定义池需要对接的边界。五、源码级原理主进程如何调度自定义池5.1 调度器Pool主进程侧的核心调度器是Pool类packages/vitest/src/node/pools/pool.ts。它维护一个任务队列queue与活跃任务列表activeTasks受maxWorkers并发上限约束每个测试文件被包装成PoolTask含worker名称、所属project、isolate、env、execArgv、memoryLimit等见 types.ts后进入队列。getPoolRunner(task, method)负责把任务映射到运行器内置forks/vmForks/threads/vmThreads/typescript走内置分支其他名称则会去读取task.project.config.poolRunner当customPool.name task.worker时调用customPool.createPoolWorker(options)构造自定义 worker 并包装进PoolRunner见 pool.ts。若poolRunner不存在或名字不匹配调度器会抛出Runner ${task.worker} is not supported.——这就是为什么自定义池的name必须与 worker 的name严格一致。5.2 运行器PoolRunner状态机与 RPCPoolRunnerpackages/vitest/src/node/pools/poolRunner.ts封装了一个PoolWorker并在其上叠加了完整的状态机idle → starting → started → stopping → stopped外加start_failure失败态。它的职责包括RPC 层通过birpc建立主进程 ↔ worker 的调用通道createBirpcRunnerRPC, RuntimeRPCworker 端用createRuntimeRpc反向建立相同协议。生命周期编排start()时向 worker 发送{ type: start }消息并等待started响应60 秒超时stop()时发送{ type: stop }并等待stopped响应支持force强制退出用于用户连续按两次CTRLc的非优雅退出场景见 poolRunner.ts。异常上报worker 意外退出会通过emitUnexpectedExit生成Worker exited unexpectedly with exit code ... during ... state错误并触发error事件让调度器及时拒绝对应任务避免僵尸 worker 与悬挂 Promise。5.3 Worker 复用机制Pool还实现了非隔离isolate: false场景下的 worker 复用当一个任务完成后如果下一个排队任务同样非隔离、worker 名称与 project 相同且环境一致isEnvironmentEqual比较环境名与选项深度相等调度器会把当前PoolRunner放入sharedRunners直接复用见 pool.ts。自定义PoolWorker可以通过可选的canReuse(task)方法提供更精细的复用判定types.ts。六、消息协议速查主进程与 worker 的通信契约自定义池本质上是实现这套消息协议。主进程发送给 worker 的WorkerRequest与 worker 回传的WorkerResponse均带有一个哨兵字段用于校验身份方向类型说明主进程 → worker{ type: start, poolId, workerId, options, context, traces }初始化 worker设置环境变量、安装环境、初始化 Traces主进程 → worker{ type: run, context }/{ type: collect, context }执行 / 收集某个测试文件的用例主进程 → worker{ type: stop }优雅停止等待运行结束、teardown、持久化编译缓存主进程 → worker{ type: cancel }取消任务worker → 主进程{ type: started, error? }启动完成或携带启动错误worker → 主进程{ type: testfileFinished, error?, usedMemory? }单个测试文件执行结束若开启reportMemory则附带堆内存用量供memoryLimit判定worker → 主进程{ type: stopped, error? }停止完成完整类型定义见 types.ts。其中usedMemory会被Pool用于内存上限判定message.usedMemory task.memoryLimit时不再复用 worker见 pool.ts。七、实战建议与注意事项先从官方示例起步官方维护的vitest-pool-example包演示了一个完整的自定义池运行器实现配置接入、worker 生命周期、通信通道建议在动手前对照阅读再结合threads/forks的内置实现packages/vitest/src/node/pools/workers/ 与 packages/vitest/src/runtime/workers/理解差异。名称一致性是硬约束PoolRunnerInitializer.name、PoolWorker.name、worker 侧逻辑中的池标识必须一致否则调度器会报Runner ... is not supported。保持协议兼容WorkerRequest/WorkerResponse的类型结构含__vitest_worker_request__/__vitest_worker_response__哨兵字段是PoolRunner与init判定消息的依据自定义序列化时务必保留这些字段。善用可选能力serialize/deserialize可对接自定义二进制协议teardown用于释放 worker 独占资源reportMemory与memoryLimit配合可实现按内存触发 worker 重建canReuse可优化非隔离模式下的 worker 复用。理解隔离的边界threads用新 worker context 隔离、vmThreads用vm模块隔离、forks用独立进程隔离——你的自定义池选择何种隔离模型直接决定测试环境的健壮性与资源开销务必在文档中明确说明。这是实验性 API接口可能随 Vitest 版本演进而变化。紧跟仓库中packages/vitest/src/node/pools/types.ts的类型定义开发并在发布库时声明对应 Vitest 版本兼容范围。结语自定义 Pool 是 Vitest 为需要完全掌控测试执行模型的进阶场景保留的底层接口。通过实现PoolRunnerInitializer→PoolWorker主进程侧契约配合vitest/worker导出的init/runBaseTests/setupEnvironmentworker 侧辅助工具你可以将任意进程模型、通信通道与调度策略接入 Vitest 的执行管线同时完整复用其测试运行、环境安装、快照与 RPC 基础设施。理解本文的协议与状态机再对照内置threads/forks实现即可着手构建自己的专用测试池。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表