This is the tiny developer documentation for Canyon (core concepts and getting started). # Start of Canyon documentation # 前端工程的端到端测试代码覆盖率收集方案 ## 背景:Playwright 覆盖率方案的局限 [`Playwright`](https://playwright.dev/) 是当下最主流的端到端(E2E)测试框架之一,但其官方推荐的代码覆盖率收集方案存在一个根本性局限——它默认被测站点运行的是未经打包的原生 JavaScript。 对于经由 [`Vite`](https://vite.dev/)、[`Webpack`](https://webpack.js.org/) 等构建工具处理后的生产产物,Playwright 原生收集到的覆盖率数据难以映射回源代码,最终生成的报告无法准确反映真实的代码覆盖情况。 --- ## 业界通行方案及其缺陷 目前较为常见的做法是直接在构建流程中集成 [`Istanbul.js`](https://istanbul.js.org/) 的插桩能力:在项目中配置插桩 → 运行 Playwright 用例 → 收集覆盖率数据 → 通过命令行工具生成报告。 然而,这一方案有两个前提条件,在实际工程中往往难以同时满足: **前提一:测试用例必须与前端工程同仓库**。Istanbul.js 的覆盖率报告依赖本地源码进行符号解析,一旦 E2E 用例与前端工程分属不同代码仓库,报告便无从生成。 **前提二:测试过程必须是自动化的**。然而现实情况是,并非所有前端业务代码都能像成熟开源项目那样拥有规范的项目结构和完善的 CI/CD 流水线。许多企业内部的 UI 自动化平台,其 E2E 用例与前端工程天然是解耦分离的;更常见的情况是,团队完全依赖手工测试来保障代码质量,根本不存在自动化用例。 针对上述痛点,我们构建了 **[`Canyon`](https://github.com/canyon-project/canyon)**。 --- ## Canyon 的设计思路 Canyon 的核心能力建立在 Istanbul.js 成熟的 AST 插桩技术之上。在构建阶段,Istanbul.js 通过解析 JavaScript 语法树,在语句、函数、分支的起始位置注入计数器,从而精确记录代码在运行时的执行路径。 围绕"E2E 用例与前端工程分离"这一核心问题,Canyon 从两个维度给出了解法。 ### 第一维度:源代码解绑 Canyon 定义了 `buildHash` 机制,并开发了配套的 [`@canyonjs/babel-plugin`](https://github.com/canyon-project/canyon/tree/main/plugins/babel-plugin) 插件。 在 CI 流水线阶段,插件自动侦测当前流水线的关键环境变量——`repoID` 与 `CommitSHA`——并将其哈希值写入覆盖率对象(`coverage`)中。这样,在后续的报告生成阶段,服务端便可凭借这组标识通过 [`GitLab API`](https://docs.gitlab.com/ee/api/) 拉取对应版本的源码,完成符号映射与报告渲染。 **源代码不再需要与测试工程共存**,这是 Canyon 与传统方案最本质的差异。 ### 第二维度:分布式覆盖率收集 对于没有自动化用例、依赖手工测试的团队,覆盖率数据的来源是分散的——它们产生于不同测试人员的浏览器会话中,时间不一,环境各异。 Canyon 将覆盖率数据的收集架构设计为**类分布式系统**: - **磁盘消息队列**:缓冲并异步处理来自多个客户端的覆盖率上报,避免峰值流量造成数据丢失; - **幂等消费**:保证同一份数据重复上报时不会导致统计结果偏差; - **分布式锁**:在并发场景下确保覆盖率数据合并操作的原子性; 上述机制共同保障了覆盖率数据的实时性与准确性,使得报告能够在测试执行过程中动态更新。 此外,Canyon 还提供了**快照(Snapshot)功能**:可对指定的覆盖率 Tag 单独生成时间点快照。结合 Case ID 使用,测试人员可以精准还原某条具体测试用例执行后的代码覆盖状态,为缺陷定位和覆盖分析提供有力支撑。 ## Canyon 解决了什么问题 | 痛点 | Canyon 的解法 | |---|---| | 打包后 JS 无法映射源码 | Istanbul.js AST 插桩 + buildHash 源码解绑 | | E2E 用例与前端工程分离 | 服务端凭 repoID/CommitSHA 独立拉取源码 | | 没有自动化用例,只有手工测试 | 分布式收集架构,支持多浏览器实例并发上报 | | 覆盖率数据合并准确性 | 磁盘队列 + 幂等消费 + 分布式锁 | | 单条 Case 的覆盖率溯源 | 快照功能,按 Case ID 定点生成报告 | --- ## 小结 Canyon 通过两个关键设计——**构建时 buildHash 插桩**与**分布式覆盖率收集服务**——将代码覆盖率的采集范围从"必须有自动化用例且用例与工程同仓库"这一苛刻前提,扩展到了几乎任何前端研发场景:无论是接入了 UI 自动化平台的团队,还是仍以手工测试为主的团队,都可以通过 Canyon 获得准确的、可追溯到源代码级别的覆盖率视图。 帮助测试人员发现尚未被触达的代码路径,正是 Canyon 的核心价值所在。 ## 相关链接 - Canyon 官网: - Canyon GitHub: # 覆盖率累积 ## 1. 问题背景(Why) 在当前端到端测试实践中: - UI 自动化覆盖率低,测试以人工为主 - 一个 Merge Request 往往包含 **多个 commit** - 不同 commit 可能分别触发不同的测试路径 **问题:** > 单个 commit 的覆盖率,无法反映 _整个 MR 在测试阶段实际覆盖过哪些逻辑_。 因此需要一种方式,将 **一段 commit 区间内的所有测试命中结果进行累积**,用于评估: - 回归测试是否可以减少 - MR 范围内是否存在完全未被触达的代码 --- ## 2. 核心概念(What) ### 2.1 Commit 区间(Commit Range) 一个 Accumulative 覆盖率统计基于以下集合: - **基线提交(Base Commit)** - **终态提交(Head Commit)** - Base → Head 之间的 **所有中间 commit** 这些 commit **共同构成一个逻辑测试周期**。 --- ### 2.2 Accumulative Coverage 定义 > **Accumulative Coverage** 指: > 在一个 commit 区间内,只要某段代码在 _任意一次提交对应的测试中被执行过_,就认为它在该区间内被覆盖。 ⚠️ 注意: 这是对“测试过程”的描述,而不是对“最终代码版本”的真实性断言。 --- ## 3. 累积模型(How) Accumulative 覆盖率以 **Head Commit 的代码结构** 作为最终展示基线。 合并过程分为两个阶段: --- ### 3.1 文件级累积(File-level Accumulation) 对 **未发生变更的文件**: - 直接对所有 commit 的命中次数进行累加 - 文件内容一致,不存在语义歧义 --- ### 3.2 语句块级累积(Block-level Accumulation) 对 **发生变更的文件**: - 以 Head Commit 的语句块结构为基准 - 在 commit 区间内查找: - **位置一致的语句块** - 若该语句块在任意 commit 中被命中: - 则在最终结果中标记为命中 约束规则: - 不进行跨位置合并 - 不尝试语义等价判断 - 同一位置存在多个语句块时,不进行合并 --- ## 4. 示例(非常重要) ### 场景示例 ```text commit A: a.ts line 10: hit commit B: a.ts line 10: unchanged, not hit commit C (head): a.ts line 10: unchanged ``` **Accumulative 结果:** ```text a.ts line 10 => covered ``` 因为: > 在 commit A 的测试中,该语句块曾被执行。 --- ### 风险示例(必须单独列) ```text commit A: a.ts line 10: hit commit B: a.ts line 10: deleted commit C (head): a.ts line 10: restored ``` **Accumulative 结果:** ```text a.ts line 10 => covered(但不保证最终版本被执行) ``` 这是 Accumulative 模型的**已知取舍**。 --- ## 5. 适用场景(When) Accumulative Coverage 适用于: - 评估 **MR 测试阶段的整体触达范围** - 辅助判断 **是否需要补充回归测试** - 覆盖率作为“测试参考指标”,而非发布门禁 **不适用于:** - 严格的发布准入判定 - 安全 / 合规类强约束场景 --- ## 6. 对比说明(防误用) | 维度 | 单 Commit 覆盖率 | Accumulative 覆盖率 | | -------- | ---------------- | -------------------- | | 覆盖语义 | 当前提交被测情况 | 提交区间内是否曾被测 | | 偏向 | 精确 | 完整 | | 风险 | 低 | 存在语义推断 | | 适合用途 | 发布门禁 | 回归评估 | --- # 变更代码覆盖率 定义 Canyon 对「变更代码覆盖率」的明确计算标准,用于评估一次变更(从 `base` 到 `head`)中新增加代码的测试覆盖情况,并指导 CI 校验与报告展示。 --- ## 一、概述(Why) 变更代码覆盖率衡量的是本次变更中新增加代码在测试(单元 / E2E / 集成)下被执行的比例。不同于仓库整体覆盖率,变更覆盖率聚焦 **"本次改动新增的可执行代码"**,能更直接反映变更风险与测试有效性,从而用于 MR 质量门控、review 指标与回归风险评估。 --- ## 二、数据源(Data Sources) 1. **代码变更(diff)** - 数据来源:Git 两个提交(`base` → `head`)之间的差异(`git diff` 或 CI 的合并变更范围)。 - 我们只关心 **新增(added)行**,即 diff 中以 `+` 标记的行(不包含上下文与删除行)。 - 对象范围:每个变更文件(通常只考虑源码文件,如 `.js`, `.ts`, `.jsx`, `.tsx` 等)。 2. **覆盖率数据(coverage)** - 数据来源:测试运行后导出的 Istanbul 风格覆盖率 JSON(例如 `nyc`/`istanbul` 输出),或等价结构。 - 我们只关注 **“语句(statements)”** 维度(Statement-level coverage)。理由见后文。 > 前提:覆盖率数据需要能够映射到**源码行号**(即插桩/构建阶段应保持 source-map 或直接对源码插桩),否则无法准确把新增行归属到语句范围。 --- ## 三、定义(Definitions) - **新增代码行集合 `L_f`(for file f)**:在 `file f` 从 `base` 到 `head` 中被新增的行号集合(相对于文件在 `head` 的行号空间)。 - **文件语句集合 `S_f`**:由覆盖率数据中 `statementMap` 给出的所有语句范围(每个语句为一段行区间,含起始行与结束行)。语句通常带有一个 id(statementId);对应的执行次数由 `s[statementId]` 给出。 - **落到语句范围的语句(impacted statements)`I_f`**:对于文件 `f`,所有满足 `statementRange ∩ L_f ≠ ∅` 的语句集合。即:任何新增行落入该语句的行区间,则该语句被认为“影响到本次变更”。 - **已覆盖的影响语句集合 `C_f`**:在 `I_f` 中,执行次数 > 0(或定义为被覆盖)的语句集合。 - **变更代码覆盖率(文件级)**: ### 文件级变更覆盖率计算 ```math coverage_f = \dfrac{|C_f|}{|I_f|} \quad (|I_f| > 0) ``` ### 总体变更覆盖率计算 ```math coverage_{change} = \dfrac{\sum_f |C_f|}{\sum_f |I_f|} \quad (\sum_f |I_f| > 0) ``` --- ## 四、详细计算逻辑(Step-by-step) **输入**:`base` commit、`head` commit、coverage JSON(基于 `head` 的源码行号) **输出**:每个变更文件的变更覆盖率,以及合并后的变更覆盖率 1. **获取变更文件与新增行** - 使用 `git diff --unified=0 base..head -- ` 或 CI 提供的 changed-files 接口。 - 从 diff 中解析所有新增行段(hunk header `@@ -a,b +c,d @@`,新增行从 `c` 开始,长度为 `d`,或以 `+` 行前缀收集行号)。 - 结果:对每个文件 `f`,得到新增行号集合 `L_f = {l1, l2, ...}`。 2. **加载文件的覆盖率信息**(Istanbul JSON) - coverage[filePath] 包含 `statementMap`(语句范围)与 `s`(语句执行次数)。 - `statementMap`:例如 `{ "1": { "start": {line, column}, "end": {line, column} }, ... }` - `s`:例如 `{ "1": 0, "2": 3, ... }`。 3. **计算 impacted statements `I_f`** - 对每个 `statementId`,取其行区间 `[start.line, end.line]`。 - 如果该区间与 `L_f` 有交集(存在 l ∈ L_f 使得 start.line ≤ l ≤ end.line),则把该 `statementId` 加入 `I_f`。 4. **判定覆盖** - 对 `I_f` 中每个 `statementId`,若 `s[statementId] > 0` 则认为该语句被覆盖,加入 `C_f`。 5. **计算文件级和合并覆盖率** - 文件级:`coverage_f = |C_f| / |I_f|` - 全量:合并分子与分母求比。 6. **输出结果与字段** - 每个文件:`{ file, impacted_statements: |I_f|, covered_statements: |C_f|, coverage: coverage_f }` - 合并:`{ total_impacted: Σ|I_f|, total_covered: Σ|C_f|, change_coverage: Σ|C_f|/Σ|I_f| }` --- ## 五、示例(示意) 假设文件 `foo.js` 的覆盖率中有 3 个语句: - stmt1: lines 1–3, execCount = 2 - stmt2: lines 4–6, execCount = 0 - stmt3: lines 7–10, execCount = 1 `git diff` 发现新增行集合 `L_foo = {2, 5}`,则: - stmt1 intersects line 2 → impacted - stmt2 intersects line 5 → impacted - stmt3 不受影响 于是 `I_foo = {stmt1, stmt2}`,`C_foo = {stmt1}`(因为 stmt2 执行次数 0) 文件级 coverage: `coverage_foo = 1 / 2 = 50%` 若这是唯一变更文件,则 `change_coverage = 50%` # Compare、快照与变更覆盖率报告 通过「创建 Compare」得到的覆盖率视图具有 **[覆盖率累积](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/core-concepts/accumulative-coverage)** 特性:在 Base → Head 这一区间内,只要某段代码在任一已上报覆盖率的 commit 对应的测试里被执行过,就会在该对比结果中计为已覆盖。下文说明在控制台中的典型操作流程。 --- ## 一、创建 Compare - **Base**:作为对比起点的历史版本 **Commit SHA**(通常选分支公共祖先或合并目标)。 - **Head**:在 Commit 列表中 **已经上报过覆盖率** 的 **Commit SHA**。 填写并创建 Compare 后,系统会基于该区间聚合覆盖率数据。 ![创建 Compare:填写 Base 与 Head](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/core-concepts/compare-workflow/create-compare.png) --- ## 二、创建快照 Compare 创建成功后,打开 **查看覆盖率报告** 即可看到基于当前区间的实时覆盖率。 当本轮测试结束后,可通过 **创建快照** 将当前视图固化为 **离线报告包**,便于归档、分享或与后续版本对照。快照概念详见 [快照(Snapshot)](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/core-concepts/snapshot)。 ![创建快照:固化当前 Compare 覆盖率视图](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/core-concepts/compare-workflow/create-snapshot.png) --- ## 三、查看覆盖率报告 在快照列表中进入 **查看快照记录**,下载并解压报告包后打开其中的报告页面。 在报告中可通过 **Change Statements** 查看当前 Compare 下 **变更语句** 的覆盖情况(哪些变更行曾被测试命中)。 ![报告中通过 Change Statements 查看变更语句覆盖率](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/core-concepts/compare-workflow/view-report.png) --- ## 四、快速定位未覆盖代码块 在报告界面中结合变更视图与未覆盖高亮,可以快速缩小 **尚未被测试触达的代码块** 范围,用于补充用例或评审风险。 ![快速定位未覆盖代码块](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/core-concepts/compare-workflow/locate-uncovered.png) # 代码插桩 本章介绍前端测试中代码插桩的基本概念、常见插桩方式,以及在单元测试和端到端测试(E2E)中的实际应用方案。Canyon 的覆盖率能力依赖于稳定、可回溯的插桩结果,因此理解插桩机制是使用 Canyon 的前提。 --- ## 什么是代码插桩 **代码插桩(Code Instrumentation)**,是指在源码或构建产物中插入“探针代码”,用于记录代码在运行过程中是否被执行、执行次数等信息。 覆盖率工具正是依赖这些探针,在测试结束后统计出: - 哪些文件被执行过 - 哪些函数 / 语句 / 分支被执行 - 哪些代码从未被覆盖 无论是**单元测试**还是**端到端测试(E2E)**,只要需要覆盖率数据,都绕不开代码插桩。 --- ## 插桩方式分类 在前端领域,主流的代码插桩方式可以分为两类: ### 1. V8 原生覆盖率插桩 - 基于 V8 引擎 - 在运行时从 **字节码执行信息** 中获取覆盖率 - 不修改源码或构建产物 - 常见于 Node.js、Chromium 浏览器环境 **特点:** - 无需改动构建流程 - 性能开销较低 --- ### 2. Istanbul 插桩(静态插桩) - 在 **构建阶段** 对代码进行 AST 转换 - 通过插入计数器的方式记录执行情况 - 常见实现: - `babel-plugin-istanbul` - `vite-plugin-istanbul` **特点:** - 插桩发生在源码 → 构建产物阶段 - 结果稳定、可控 - 构建产物体积会变大 --- ## E2E 测试中的插桩问题 端到端测试(如 Playwright)与单元测试最大的不同在于: > **代码运行在真实浏览器内核中** 这会直接影响覆盖率的采集方式。 --- ### Playwright + V8 覆盖率的局限 Playwright 支持通过 Chromium 的 V8 接口获取 JS 覆盖率,这在以下场景中是可行的: - 原生 HTML / CSS / JS - 未打包、未压缩的脚本 - Demo 或简单页面 但在真实生产项目中,往往存在: - 构建工具打包(Vite / Webpack / Rspack) - 代码压缩、混淆 - 多模块合并 - 动态加载 此时: - 覆盖率基于 **打包后的 JS** - 与源码结构严重偏离 - Source Map 还原成本极高 - 结果不可控、不可验证 --- ## Istanbul 插桩在 E2E 中的解决方案 在现代 SPA 项目中,**推荐在构建阶段使用 Istanbul 插桩**,再在 E2E 测试中直接收集浏览器内存中的覆盖率数据。 ### 核心思路 1. **构建阶段** - 使用 `babel-plugin-istanbul` 等工具 - 在 AST 转换时插入探针 - 生成带插桩的构建产物 2. **运行阶段(Playwright)** - 浏览器执行插桩代码 - 覆盖率数据累积在 `window.__coverage__` 3. **测试阶段** - Playwright 从浏览器上下文中读取覆盖率对象 - 上报给 Canyon 做聚合分析 # 还原源代码覆盖率 前端应用有各种编译形式。在很多情况下,预编译的 AST 会交给 Babel 进行转换,这使得无法直接将覆盖率映射到源代码。因此,我们需要还原源代码覆盖率。 ## Source Map > 在前端开发中,JavaScript 代码通常会被压缩、混淆或使用预处理器(如 TypeScript、Babel)进行转换,以提高性能和兼容性。这些过程使得部署到生产环境的代码与原始源代码非常不同,增加了调试难度。Source Map 通过记录编译后代码与原始代码之间的映射关系来解决这个问题,允许开发者在浏览器开发者工具中查看和调试原始代码。 -- [web.dev](https://web.dev/articles/source-maps) 在大多数情况下,您不需要手动获取 sourceMap 文件,因为大多数构建工具在`将预编译的 AST 交给 Babel 进行转换`的过程中会传递 sourceMap 文件。 然而,在某些情况下,您可能需要配置 sourceMap 文件。 ## 需要启用 sourceMap 选项的情况 ```js filename="webpack.config.js" {4} const path = require("path"); module.exports = { entry: "./build/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { test: /\.(js|jsx)$/, use: ["babel-loader"], exclude: "/node_modules/", }, ], }, }; ``` 在这个例子中,webpack 的入口文件是 `./build/main.js`,这是 tsc 编译 TypeScript 文件的输出。我们需要在 tsconfig.json 中将 `sourceMap` 设置为 `true` **以确保传递 sourceMap 数据。** ```json filename="tsconfig.json" {3} { "compilerOptions": { "sourceMap": true } } ``` ## 需要手动设置 sourceMap 的情况 当您的 sourceMap 生成方式属于以下情况时,您需要手动设置 sourceMap。 | 类别 | devtool | 描述 | | ---------------------------------- | -------------------- | ----------------------- | | 生成 source map 文件,不显示源代码 | hidden-source-map | 文件末尾不引用 map 文件 | | 生成 source map 文件,不显示源代码 | nosources-source-map | 文件末尾不引用 map 文件 | ### 如何手动设置 sourceMap 在这种情况下,您需要使用[分离 Hit 和 Map](/docs/core-concepts/separate-hit-and-map)。通过 Tools CLI,在构建阶段检测本地 sourceMap 文件,Canyon 会将它们与初始覆盖率数据进行匹配并上传。 > [!NOTE] > > 这也是 JavaScript 过于灵活的一个缺点。为了收集准确的源代码覆盖率数据,我们需要将这些 sourceMap 文件链接在一起,以便代码插桩能够正确映射到源代码。 import { Callout } from "nextra/components"; # 分离 Hit 和 Map 在 CI 中收集 Babel 编译期间生成的初始覆盖率源文件 ## 原因 1. **收集完整的编译文件**:在编译过程中,通过 `@canyonjs/babel-plugin` 插件,分析并解析每个编译文件的初始覆盖率文件,并将其保存到 `.canyon_out` 文件夹中。 2. **早期收集以减轻压力**:如果不报告初始覆盖率文件,这些覆盖率文件将在 UI 自动化测试期间被报告,这会产生巨大的传输压力。早期收集可以减少超过 90% 的传输压力。 ## 报告初始覆盖率数据 @canyonjs/babel-plugin 插件在编译过程中为编译后的文件生成初始覆盖率文件,并将其保存到 .canyon_output 文件夹中。我们提供了 Tools CLI 命令行工具,您可以在 CI 中使用它向 Canyon 服务器报告。 ```yml filename=".gitlab-ci.yml" {19,20} copy pages: image: node:20 stage: deploy script: - npm run build - npx @canyonjs/cli upload --dsn=https://app.canyonjs.io/api/coverage/map/init only: - main ``` ## 准备就绪 现在检查页面上的 window.coverage 对象,如截图所示。window.coverage 不再包含 map 数据,这可以大大降低 UI 自动化覆盖率收集过程中的大量传输成本。 ![](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/jietu1.png) # 快照(Snapshot) Snapshot 是 Canyon 对某个覆盖率视图的“冻结产物”。它会把当时的覆盖率数据与可直接打开的报告页面一起打包保存,便于后续对比、归档和共享。 --- ## 一、核心价值 - **可追溯**:把某一时刻的覆盖率结果固化,不受后续分支变化影响。 - **可下载**:直接下载报告包(`artifact_zip`),本地解压即可查看 `index.html`。 - **可对比**:同一仓库可按 `commit` 或 `compare` 维度留存多份快照。 --- ## 二、支持的 subject Snapshot 支持两类 subject: - `commit`:某个 commit 的全量覆盖率快照 - `compare`:`base...head` 对比视图的覆盖率快照 这也是前端列表中“总体覆盖率”和“变更代码覆盖率”互斥显示的原因: - `commit`:重点看总体覆盖率 - `compare`:重点看变更代码覆盖率 --- ## 三、状态流转 创建快照后,状态会按以下流程变化: - `generating`:生成中 - `completed`:生成完成,可下载 - `failed`:生成失败 - `timeout`:超时(默认 120 秒) 系统会在查询列表/详情时对超时任务做状态修正,避免长期停留在 `generating`。 --- ## 四、生成产物内容 Snapshot 不是只存一份原始 JSON,而是生成可视化报告包: 1. 拉取覆盖率 map(按 `subject/subjectID/buildTarget`) 2. 通过 Git Provider 拉取对应 commit 的源码内容 3. 生成 `report-data.js` 4. 用 `@canyonjs/report-html` 的 `dist` 产物替换 `data/report-data.js` 5. 打包为 zip,写入 `artifact_zip` 最终下载后可直接打开 `index.html` 浏览报告。 --- ## 五、落库指标(用于统计/看板) 生成完成时,Snapshot 会写入以下关键指标: - `statements_covered` - `statements_total` - `changestatements_covered` - `changestatements_total` - `duration_ms`(报告生成耗时,毫秒) 百分比可以按需自行计算: - 总体覆盖率:`statements_covered / statements_total` - 变更覆盖率:`changestatements_covered / changestatements_total` import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/babel-plugin
一个用于检测 CI 环境变量的 Babel 插件。与 istanbuljs 配合使用,完成代码插桩。 ## 使用方法 安装: ```sh npm install --save-dev @canyonjs/babel-plugin ``` 在 `babel.config.js` 中添加这些配置: ```js module.exports = { plugins: process.env.CI_COMMIT_REF_NAME === "test-coverage" ? ["istanbul", "canyon"] : [], // 注意插件顺序:canyon 插件应在 istanbul 插件之后 }; ``` 它做了两件事: 1. 检测 CI 流水线变量 2. 收集参与编译的文件覆盖率初始数据 ## 配置 babel.config.js ```js module.exports = { plugins: [ "istanbul", [ "canyon", { // #region == Step 2: CI Provider auto-detection, generally no manual configuration needed, see Support Provider documentation for details repoID: "230614", // Repository ID sha: "xxxxxxxxx", // Git Commit SHA // #endregion // #region == Step 4: Separate hit and map data (optional) keepMap: false, // Keep coverage map, optional, default is false. // #endregion // #region == Step 5: Other configuration (optional) instrumentCwd: "/path/to", // Instrumentation working directory, may need manual configuration in multi-repo mode provider: "gitlab", // Source code provider (optional), default is gitlab include: ["src/**/*.ts", "src/**/*.tsx"], // Include glob patterns (optional) exclude: ["**/*.test.ts", "**/*.spec.ts"], // Exclude glob patterns (optional) extensions: [".ts", ".tsx", ".js"], // Extensions participating in include/exclude matching (optional) // #endregion }, ], ], }; ``` | 配置项 | 描述 | 是否必填 | 默认值 | | ------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | ------------- | | repoID | 仓库 ID | 一般无需手动配置(自动检测 CI Provider) | None | | sha | Git 提交 SHA | 一般无需手动配置(自动检测 CI Provider) | None | | branch | Git 仓库分支 | 一般无需手动配置(自动检测 CI Provider) | None | | keepMap | 保留覆盖率地图,可选,默认为 true,当为 false 时,将生成 .canyon_output 文件 | 可选 | true | | instrumentCwd | 插桩工作目录,在多仓库模式下可能需要手动配置 | 可选 | process.cwd() | | provider | 源代码提供商(可选),默认为 gitlab | 可选 | gitlab | | include | 需要插桩的文件匹配规则(glob),语义与 babel-plugin-istanbul 一致 | 可选 | [] | | exclude | 不需要插桩的文件匹配规则(glob),语义与 babel-plugin-istanbul 一致 | 可选 | [] | | extensions | include/exclude 匹配时使用的扩展名列表 | 可选 | .js/.ts/... | import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/cli
Canyon CLI 是一个用于将覆盖率数据上传到 Canyon 服务器的命令行工具。它从本地 `.canyon_output` 目录读取并聚合覆盖率数据,然后将其上传到配置的 Canyon 服务器。 ## 安装 通过 npm 安装: ```bash npm install -g @canyonjs/cli ``` 或者从 [GitHub Release](https://github.com/canyon-project/canyon/releases) 下载最新版本。 ## 特性 - 从本地 `.canyon_output` 目录读取并聚合覆盖率数据 - 支持按路径过滤覆盖率文件 - 支持基于场景的覆盖率报告 - 自动检测 CI 环境变量(GitLab CI) - 将多个覆盖率文件合并为单个报告 ## 使用方法 ### 基本用法 ```bash canyon upload --dsn=http://your-canyon-server/api/coverage/client ``` 使用 `--scene` 添加场景元数据以过滤覆盖率数据: ```bash canyon upload --dsn=$DSN \ --scene='{"source":"automation","type":"e2e","env":"test"}' ``` ## 命令选项 | 选项 | 描述 | 是否必填 | 默认值 | | ------------------ | ---------------------------- | -------- | --------------- | | `--dsn` | Canyon 服务器端点 URL | 是 | None | | `--repo_id` | 仓库 ID | 否\* | `CI_PROJECT_ID` | | `--sha` | Git 提交 SHA | 否\* | `CI_COMMIT_SHA` | | `--provider` | 源代码提供商 (gitlab/github) | 否 | `gitlab` | | `--build_target` | 构建目标标识符 | 否 | 空字符串 | | `--instrument_cwd` | 插桩工作目录 | 否 | `process.cwd()` | | `--filter` | 按路径子串过滤覆盖率文件 | 否 | None | | `--scene` | JSON 格式的场景元数据 | 否 | None | | `--debug` | 启用调试模式 | 否 | `false` | \* 如果未提供,将从 CI 环境变量自动检测 ## 示例:GitLab CI 集成 ```yaml coverage: stage: test script: - npm test - canyon upload --dsn=$CANYON_DSN --scene='{"type":"unit","env":"ci"}' artifacts: paths: - .canyon_output/ expire_in: 1 week ``` import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/collect
`@canyonjs/collect` 是一个用于 Web 站点的覆盖率收集脚本包。它通常以脚本形式注入页面,在浏览器运行阶段按需采集覆盖率并上报到 Canyon 服务端。 ## 安装 ```bash npm install @canyonjs/collect ``` ## 适用场景 - 需要在真实浏览器环境采集覆盖率 - 通过页面脚本方式接入,而不是 CLI 上传 - 与 E2E 自动化流程联动,按场景上报覆盖率 ## 代码位置 - 包目录:`tools/collect` - 入口:`tools/collect/src/index.ts` ## 说明 当前该包以脚本采集场景为主,具体接入方式可结合业务页面注入方案和测试框架流程实现。 import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/git-diff
@canyonjs/git-diff 是一个用于在 CI/CD 流水线中检测代码变更并生成 Git Diff 内容的 NPM 包 ## 使用 ### 在 CI/CD 流水线中使用 ```bash npx @canyonjs/git-diff ``` ### 输出 在当前目录下生成diff.txt文件 ```text filename="diff.txt" diff --git a/fixtures/sum.js b/fixtures/sum.js index 1234567..abcdefg 100644 --- a/fixtures/sum.js +++ b/fixtures/sum.js @@ -1,3 +1,5 @@ export function sum(a, b) { + // 新增的注释 return a + b; + // 另一个新增的注释 } diff --git a/fixtures/add.js b/fixtures/add.js index 1234567..abcdefg 100644 --- a/fixtures/add.js +++ b/fixtures/add.js @@ -1,3 +1,6 @@ export function sum(a, b) { + // 新增的注释行 + console.log('计算中...'); return a + b; + // 结束注释 } ``` ## 原理 @canyonjs/git-diff 通过检测当前流水线环境变量,确定 CI/CD 提供商,然后根据提供商的特定环境变量判断当前是 MR 还是普通 Commit,从而确定需要进行 Git Diff 比较的 commit 范围。 ### 检测逻辑 1. **平台检测**:首先检测 CI/CD 平台特定的环境变量,确定当前运行环境 2. **事件类型判断**:根据平台特定的环境变量判断是 MR/PR 还是普通 Commit 3. **分支确定**: - 对于 MR/PR:获取源分支和目标分支 - 对于普通 Commit:获取当前分支和上一个 Commit 4. **Diff 生成**:根据确定的分支或 Commit 范围生成 Git Diff 内容 ### Git Diff 范围确定 | 事件类型 | 比较范围 | 命令示例 | | ----------- | ---------------------------- | --------------------------------------- | | MR/PR | 源分支 vs 目标分支 | `git diff origin/target..origin/source` | | 普通 Commit | 当前 Commit vs 上一个 Commit | `git diff HEAD~1..HEAD` | import { Button } from "antd"; # @canyonjs/playwright
为 Playwright 测试提供浏览器端 JavaScript 覆盖率收集。 import { GithubOutlined } from "@ant-design/icons"; ### 前置条件 - 你的应用程序必须经过 istanbuljs 代码插桩 --- ## 安装 ```bash npm2yarn npm install --save-dev @canyonjs/playwright ``` ## 使用 扩展 Playwright 的 context 功能以支持覆盖率收集: ```js filename="playwright.config.ts" import { test as baseTest } from "@playwright/test"; import { createCoverageContextFixture } from "@canyonjs/playwright"; export const test = baseTest.extend({ context: createCoverageContextFixture({ outputDir: ".canyon_output", }), }); export const expect = test.expect; ``` 在你的 E2E 测试文件中使用此 `test` 导出,而不是 Playwright 的默认 `test`。 --- ### 这行代码做了什么 - 从已插桩的应用程序中收集 **浏览器端 JavaScript 覆盖率** - 在以下时机刷新覆盖率数据: - 页面卸载时 - 测试拆卸时 - 将覆盖率文件写入指定的输出目录 - 通过扩展 Playwright fixtures 在 **用户代码层面** 完全实现 > 此集成 **不会** 修补或替换 Playwright 的测试运行器。 --- ### 输出 覆盖率文件将被写入: ```text .canyon_output/ ├─ coverage-1700000000000.json ├─ coverage-1700000001234.json └─ ... ``` 这些文件可以: - 与 `nyc` 合并 - 上传到 Canyon 平台 - 通过 Canyon CLI 工具进行后处理 import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/report
一个覆盖率报告器,适配[istanbul-reports](https://github.com/istanbuljs/istanbuljs/tree/main/packages/istanbul-reports/lib)接口 ## 安装 ```bash npm2yarn npm install --save-dev @canyonjs/report ``` ## 使用 该报告器适配istanbuljs报告器接口,所以支持各种常见的测试框架的覆盖率报告生成,以vitest为例 ```js {7} import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { provider: "v8", reporter: ["@canyonjs/report"], }, }, }); ``` ## 配置 > [!IMPORTANT] > > 支持传入git diff,生成变更代码覆盖率报告!默认读取diff.txt文件内容。在CI中集成请查看 [@canyonjs/git-diff](./git-diff)。 ```js {7} import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { provider: "v8", reporter: [ "json", [ "@canyonjs/report", { diff: diff, // 支持git diff字符串、路径 }, ], ], }, }, }); ``` ## 输出 - index.html,单个html文件,易于传输/部署或直接在任何位置打开。 - data/report-data.js,对应压缩过的覆盖率数据及源码内容。 - js、css等静态资源。 ## 例子 | 测试框架 | 示例链接 | | ------------- | -------------------------------------------------------------------------------------------------------------- | | `Vitest` | [report-html-vitest](https://github.com/canyon-project/canyon/tree/main/examples/report-html-vitest) | | `Jest` | [report-html-jest](https://github.com/canyon-project/canyon/tree/main/examples/report-html-jest) | | `RSTest` | [report-html-rstest](https://github.com/canyon-project/canyon/tree/main/examples/report-html-rstest) | | `Mocha` | [report-html-mocha](https://github.com/canyon-project/canyon/tree/main/examples/report-html-mocha) | | `Test Runner` | [report-html-test-runner](https://github.com/canyon-project/canyon/tree/main/examples/report-html-test-runner) | import { GithubOutlined } from "@ant-design/icons"; import { Button } from "antd"; # @canyonjs/vite-plugin
`@canyonjs/vite-plugin` 是 Vite 场景下的 Canyon 插桩插件,内部直接调用 `@canyonjs/babel-plugin` 完成核心处理逻辑。 因此你只需要关注 Vite 接入方式;参数语义、CI 自动检测等能力与 `@canyonjs/babel-plugin` 一致。 ## 安装 ```sh npm install -D @canyonjs/vite-plugin vite-plugin-istanbul ``` ## 使用方法 建议仅在生产构建中启用: ```ts import { defineConfig } from "vite"; import istanbulPlugin from "vite-plugin-istanbul"; import canyonVitePlugin from "@canyonjs/vite-plugin"; const isProduction = process.env.NODE_ENV === "production"; export default defineConfig({ plugins: [ ...(isProduction ? [ istanbulPlugin({ forceBuildInstrument: true, }), canyonVitePlugin({ repoID: "9050", sha: "xxxxx", provider: "gitlab", ci: true, keepMap: true, }), ] : []), ], }); ``` ## 配置项 `@canyonjs/vite-plugin` 直接透传以下配置给 `@canyonjs/babel-plugin`: - `repoID` - `sha` - `provider` - `ci` - `instrumentCwd` - `keepMap` 不重复展开参数说明,详情请直接查看 [@canyonjs/babel-plugin](./babel-plugin) 文档。 import { Steps, Callout, Cards } from "nextra/components"; # 快速开始 与单元测试不同,端到端测试会启动浏览器。我们的目标是在浏览器内收集覆盖率数据,因此复杂度相对较高。以下是一般流程: - 暴露浏览器全局变量,在适当的时机将覆盖率数据写入本地 - UI自动化完成后,使用[Tools CLI](/docs/ecosystem/tools-cli)读取本地`.canyon_output`目录中的覆盖率数据并聚合上报 - 将caseID与[Report ID](/docs/core-concepts/report-id)关联,确保用例可追溯 ## 框架 ## 注意事项 **服务端渲染(SSR)框架的覆盖率支持范围** 以 Next.js 为代表的服务端渲染(SSR)框架,Canyon 当前仅支持客户端代码的覆盖率统计。 **说明:** SSR 框架同时包含: - 服务端代码(Node.js / Edge Runtime) - 客户端代码(Browser Runtime) Canyon 的覆盖率采集与变更覆盖率计算: - 依赖浏览器侧执行结果 - 依赖 window.__coverage__ 等客户端覆盖率模型 **因此在 SSR 场景下:** ✅ **支持:** - Client Components - 浏览器中执行的页面逻辑、事件处理、Hooks、客户端工具函数等 ❌ **不支持(不计入覆盖率):** - getServerSideProps / getStaticProps - Server Components - API Routes(/api/*) - Edge / Node.js 服务端逻辑 import { Steps } from "nextra/components"; import { Button } from "antd"; import { GithubOutlined } from "@ant-design/icons"; import SetupReactPlaywrightPython from "../../../components/setup-react-playwright-python.mdx"; # Playwright Python 在 [Playwright Python](https://playwright.dev/python/docs/intro)(pytest-playwright)中集成覆盖率收集,生成 `.canyon_output` 供后续报告生成或上报使用。
## 使用该方案的优势 对于端到端测试而言,该方案的核心优势包括: - **支持现代化 Web 应用**:现代化的 Web 应用都会经过 JS bundle 打包,Playwright 提供的默认方案只能对原生 JS 等简单场景使用 V8 收集覆盖率,而该方案通过 Babel 插桩可以准确收集打包后代码的覆盖率 - **确保变更代码覆盖率完整性**:`@canyonjs/babel-plugin` 会在构建时生成一份初始覆盖率数据,确保在 CI 集成时,能够收集到因懒加载而可能丢失的变更代码覆盖率 - **与 pytest-playwright 无缝集成**:通过 pytest plugin 扩展 `page` fixture,在用户代码层面完全实现,不会修补或替换 pytest 测试运行器 - **灵活的覆盖率报告**:`.canyon_output` 中的覆盖率数据可配合 [Tools CLI](/docs/ecosystem/tools-cli) 聚合上报,或用于本地生成 HTML 报告 > [!TIP] > > 该方案特别适合使用现代构建工具(如 Vite、Webpack)的 React、Vue 等框架项目,能够准确追踪打包后代码的覆盖率情况。若你偏好 Python 编写 E2E 测试,可选用 [canyonjs-playwright-python](https://github.com/canyon-project/canyonjs-playwright-python)。 ## 以新项目的形式开始 import { Steps } from "nextra/components"; import { Button } from "antd"; import { GithubOutlined } from "@ant-design/icons"; import SetupReactPlaywright from "../../../components/setup-react-playwright.mdx"; # Playwright 在 [playwright](https://playwright.dev/docs/intro) 中集成覆盖率收集与报告生成
## 使用该方案的优势 对于端到端测试而言,该方案的核心优势包括: - **支持现代化 Web 应用**:现代化的 Web 应用都会经过 JS bundle 打包,Playwright 提供的默认方案只能对原生 JS 等简单场景使用 V8 收集覆盖率,而该方案通过 Babel 插桩可以准确收集打包后代码的覆盖率 - **确保变更代码覆盖率完整性**:`@canyonjs/babel-plugin` 会在构建时生成一份初始覆盖率数据,确保在 CI 集成时,能够收集到因懒加载而可能丢失的变更代码覆盖率 - **与 Playwright 无缝集成**:通过扩展 Playwright 的 context fixture,在用户代码层面完全实现,不会修补或替换 Playwright 的测试运行器 - **灵活的覆盖率报告**:支持本地生成 HTML 报告,也可以上传到 Canyon 服务器进行统一管理和分析 > [!TIP] > > 该方案特别适合使用现代构建工具(如 Vite、Webpack)的 React、Vue 等框架项目,能够准确追踪打包后代码的覆盖率情况。 ## 以新项目的形式开始 # 最常问的问题 ## 问题一:为什么 Compare 中的变更代码文件数量和 GitLab 不一致? Compare 中统计的是 **Babel 参与编译并进入覆盖率计算链路** 的文件,例如 `js`、`jsx`、`ts`、`tsx` 等;而 GitLab 展示的是 Git diff 的文件变化结果。两者统计口径不同,因此数量可能不一致。 例如: - 你在一次提交中改了 10 个文件,其中包含: - 6 个 `ts/tsx` 业务文件 - 2 个 `md` 文档 - 1 个 `json` 配置 - 1 张 `png` 图片 - GitLab 的 compare 会显示 10 个变更文件。 - Compare 页面通常只会聚焦可编译并参与覆盖率计算的代码文件(如上面的 6 个 `ts/tsx`),所以看到的文件数可能是 6。 再比如: - GitLab 中某些文件虽然发生了变更,但属于样式资源、静态资源、文档或未纳入 Babel 处理范围的文件。 - 这些文件不会进入覆盖率计算,因此不会计入 Compare 的变更代码文件统计。 如果你想快速判断“为什么少了几个文件”,可以按下面思路排查: 1. 先在 GitLab compare 中列出全部变更文件类型。 2. 过滤出 `js`、`jsx`、`ts`、`tsx` 等代码文件。 3. 再对照项目的 Babel/插桩配置,确认这些代码文件是否在处理范围内。 4. 对不在范围内的文件(如文档、图片、部分配置文件)视为口径差异导致的正常现象。 import { Steps, Callout } from "nextra/components"; # 首次获取覆盖率数据 > [!NOTE] > > Canyon 提供了[常见框架的安装指南](/docs/installation/getting-started),帮助您快速上手。 按照以下步骤完成您的首次覆盖率数据报告: ## 新项目开始 ### 安装 Babel 是前端工程化和模块化开发中不可或缺的工具。对于 Babel 项目,您只需安装两个 Babel 插件即可快速开始。 ```sh npm2yarn npm install babel-plugin-istanbul @canyonjs/babel-plugin -D ``` 将 [`istanbul`](https://www.npmjs.com/package/babel-plugin-istanbul) 和 [`canyon`](https://www.npmjs.com/package/@canyonjs/babel-plugin) 插件添加到您的 [Babel 配置文件](https://babeljs.io/docs/config-files#configuration-file-types)中: ```js filename="babel.config.js" {3,4} copy module.exports = { plugins: [ "istanbul", "@canyonjs", // 注意插件顺序:canyon 插件应在 istanbul 插件之后 ], }; ``` ### 验证 配置完成后,启动您的项目并在控制台中打印 **window.\_\_coverage\_\_**。如果输出与下图匹配,则代码插桩成功。 ![coverage-canyon-console](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/getting-started/first-coverage/coverage-canyon-console.png) ### 更新 Babel 插件激活条件 **在 CI 阶段,我们需要控制插件激活条件,以防止在生产分支中进行插桩。** ```js {2,3} copy module.exports = { plugins: process.env.CI && process.env.CI_COMMIT_REF_NAME !== "release" ? ["istanbul", "@canyonjs"] : [], }; ``` ### CI中收集初始覆盖率数据 在 CI 环境中,您可以通过运行测试用例来收集初始覆盖率数据。这有助于建立代码覆盖率的基准线。 以下是一个 GitLab CI/CD 配置示例: ```yaml filename=".gitlab-ci.yml" copy pages: image: node:20 stage: deploy script: - npm run build - npx @canyonjs/cli upload --dsn=https://app.canyonjs.io/api/coverage/map/init only: - main ``` ### 报告覆盖率数据 等待 CI 完成后,将页面部署到测试环境。为了收集人工测试的覆盖率数据,您需要在 HTML 文件中引入 Canyon 收集脚本: ```html copy ``` 配置完成后,随后的测试就会不断收集上报覆盖率数据到服务端。 # 介绍 ## 概述 Canyon是一个 JavaScript 代码覆盖率收集平台。我们解决了开发人员和 QA 工程师在端到端测试期间收集测试用例代码覆盖率时遇到的困难。它由三个主要组件组成: - 一系列[插件](https://github.com/canyon-project/canyon/tree/main/plugins),负责适配各种 CI 提供商并检测环境变量。 - 一个[前后端一体化应用](https://github.com/canyon-project/canyon/tree/main/app)(Node.js + React),负责覆盖率数据的采集处理、后端 API 与前端报告展示。 ![](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/getting-started/introduction/home-screen.png) ## 为什么选择 Canyon? Canyon 通过在编译阶段分离 hit 和 map 数据,高效处理 UI 自动化测试生成的大量覆盖率数据。 此外,Canyon 适配常见的 CI 提供商,能够在构建阶段插入探针代码,并在 UI 自动化测试期间收集和报告覆盖率数据。 这使开发团队能够**准确地**、**实时地**了解当前 UI 自动化测试中**每个测试用例**的覆盖情况,从而更好地评估和提高代码质量。 ### 特性 - [准确高效](/docs/core-concepts/separate-hit-and-map):通过分离 hit 和 map,在编译时生成初始覆盖率数据,准确高效地收集覆盖率信息 - [SourceMap](/docs/core-concepts/restore-source-code-coverage):准确还原源代码覆盖率 - [构建工具](/docs/installation/getting-started):为 Next.js、Vite、Webpack 等常用构建工具提供覆盖率解决方案 - [自动化框架](/docs/end-to-end-testing/getting-started):为常见 UI 自动化框架提供集成方案 - [CI 提供商](/docs/reference/provider):适配 GitHub Actions、GitLab Runner 等常见 CI 提供商,自动检测 CI 环境变量 ### 自托管 如果您想完全控制自己的覆盖率数据和测试数据,可以在自己的基础设施上[自托管](/docs/self-host/community-edition/prerequisites) Canyon。 ## 生态系统 | 项目 | 状态 | 描述 | | ------------------------ | ------------------------------------------------------------------ | -------------------------------------------- | | [@canyonjs/babel-plugin] | [![@canyonjs/babel-plugin-status]][@canyonjs/babel-plugin-package] | 用于检测流水线环境变量的 Babel 插件 | | [@canyonjs/cli] | [![@canyonjs/cli-status]][@canyonjs/cli-package] | 扫描本地 `.canyon_output` 数据并上传到服务器 | | [@canyonjs/collect] | [![@canyonjs/collect-status]][@canyonjs/collect-package] | 用于网站自动收集覆盖率数据的脚本包 | [@canyonjs/babel-plugin]: /docs/ecosystem/babel-plugin [@canyonjs/cli]: /docs/ecosystem/cli [@canyonjs/collect]: /docs/ecosystem/collect [vite-plugin-canyon]: https://github.com/canyon-project/vite-plugin-canyon [@canyonjs/babel-plugin-status]: https://img.shields.io/npm/v/@canyonjs/babel-plugin.svg [@canyonjs/cli-status]: https://img.shields.io/npm/v/@canyonjs/cli.svg [@canyonjs/collect-status]: https://img.shields.io/npm/v/@canyonjs/collect.svg [vite-plugin-canyon-status]: https://img.shields.io/npm/v/vite-plugin-canyon.svg [@canyonjs/babel-plugin-package]: https://npmjs.com/package/@canyonjs/babel-plugin [@canyonjs/cli-package]: https://npmjs.com/package/@canyonjs/cli [@canyonjs/collect-package]: https://npmjs.com/package/@canyonjs/collect [vite-plugin-canyon-package]: https://npmjs.com/package/babel-plugin-vite ## 架构 下图显示了 Canyon 及其生态系统的一些组件的架构: 1. Babel 插件在 CI/CD 流水线中完成代码插桩。 2. 项目部署到测试环境,用户可以选择 UI 自动化或手动测试来触发插桩代码探针。 3. 生成的覆盖率数据报告到 Canyon 服务器。 4. Canyon 服务器消费数据,并结合 GitLab 提供的源代码信息生成覆盖率报告。 ```mermaid %%{init: { "theme": "base", "themeVariables": { "primaryColor": "#FFF7ED", "primaryBorderColor": "#FB923C", "lineColor": "#9CA3AF", "fontFamily": "Inter, system-ui" } }}%% flowchart LR classDef client fill:#EEF2FF,stroke:#6366F1,color:#1E1B4B; classDef test fill:#F1F5F9,stroke:#94A3B8,color:#020617; classDef core fill:#FFF7ED,stroke:#FB923C,color:#7C2D12; classDef storage fill:#ECFEFF,stroke:#06B6D4,color:#083344; classDef infra fill:#F0FDF4,stroke:#22C55E,color:#14532D; classDef external fill:#FAFAFA,stroke:#D1D5DB,color:#111827; %% Clients UI[UI Automation] WebUI[Canyon Web UI] API[API Client] class UI,WebUI,API client %% Test Test[Test Environment] Pipeline[CI / CD Pipeline] class Test,Pipeline test UI --> Test Pipeline --> Test %% Canyon Core subgraph Canyon["Canyon Server"] MQ[Message Queue] DB[(Postgres)] HTTP[HTTP Server] end class MQ,HTTP core class DB storage Test --> MQ MQ --> DB DB --> HTTP %% Infra subgraph Deploy["Service Deployment"] K8s[Kubernetes] Node[Node.js] end class K8s,Node infra Canyon --> Deploy %% External GitLab[GitLab] class GitLab external Canyon -.-> GitLab WebUI -.-> HTTP API -.-> HTTP ``` import { Cards } from "nextra/components"; # 快速开始 如何将 Canyon 插件安装到您的应用程序中。 > [!NOTE] > > 当前推荐使用 `@canyonjs/babel-plugin` 与 `@canyonjs/vite-plugin`。更多插件请查看[插件目录](https://github.com/canyon-project/canyon/tree/main/plugins)。 ## 框架 # React Native import { Steps } from "nextra/components"; 安装和配置 React Native。 ### 创建项目 React Native [文档](https://reactnative.dev/) ### 添加 Canyon 和配置 > [!TIP] > > 您需要了解 [react-native-babel-preset](https://reactnative.dev/docs/javascript-environment#javascript-syntax-transformers)。 ```sh npm2yarn copy npm install babel-plugin-istanbul @canyonjs/babel-plugin -D ``` ### 配置 Babel 插件 ```js filename="babel.config.js" {3} copy module.exports = { presets: ["module:@react-native/babel-preset"], plugins: ["istanbul", "@canyonjs"], }; ``` ### 完成了 继续查看[首次获取覆盖率数据 - 验证](/docs/getting-started/first-coverage#%E6%A3%80%E6%9F%A5)。 # Vite import { Steps } from "nextra/components"; 安装和配置 Vite。 ### 创建项目 首先,使用以下命令创建一个新的 React 项目 `vite`。 ```sh npm2yarn copy npm create vite@latest ``` ### 添加 Canyon 和配置 ```sh npm2yarn copy npm install -D @canyonjs/vite-plugin vite-plugin-istanbul ``` ### 配置 Vite 插件 ```ts filename="vite.config.ts" copy import { defineConfig } from "vite"; import istanbulPlugin from "vite-plugin-istanbul"; import canyonVitePlugin from "@canyonjs/vite-plugin"; const isProduction = process.env.NODE_ENV === "production"; // https://vite.dev/config/ export default defineConfig({ plugins: isProduction ? [ istanbulPlugin({ forceBuildInstrument: true, }), canyonVitePlugin({ repoID: "your-repo-id", sha: "your-commit-sha", provider: "gitlab", ci: true, keepMap: true, }), ] : [], }); ``` ### 完成了 继续查看[首次获取覆盖率数据 - 验证](/docs/getting-started/first-coverage#%E6%A3%80%E6%9F%A5)。 # istanbuljs 覆盖率数据字典 ## 概述 istanbuljs 是一个JavaScript代码覆盖率的黄金标准,本数据字典描述了覆盖率数据的主要结构和字段。 ## 主要数据结构 ### 1. Coverage Object (覆盖率对象) 顶级覆盖率数据结构,包含所有文件的覆盖率信息。 ```json { "path/to/file.js": { // FileCoverage 对象 } } ``` ### 2. FileCoverage (文件覆盖率) 单个文件的完整覆盖率数据。 | 字段 | 类型 | 描述 | |------|------|------| | `path` | string | 文件的绝对路径 | | `statementMap` | Object | 语句映射表 | | `fnMap` | Object | 函数映射表 | | `branchMap` | Object | 分支映射表 | | `s` | Object | 语句执行计数 | | `f` | Object | 函数执行计数 | | `b` | Object | 分支执行计数 | ### 3. StatementMap (语句映射) 描述代码中每个语句的位置信息。 | 字段 | 类型 | 描述 | |------|------|------| | `start` | Location | 语句开始位置 | | `end` | Location | 语句结束位置 | ### 4. FunctionMap (函数映射) 描述代码中每个函数的信息。 | 字段 | 类型 | 描述 | |------|------|------| | `name` | string | 函数名称(匿名函数为"(anonymous_N)") | | `decl` | Location | 函数声明位置 | | `loc` | Location | 函数体位置 | | `line` | number | 函数声明行号 | ### 5. BranchMap (分支映射) 描述代码中每个分支的信息。 | 字段 | 类型 | 描述 | |------|------|------| | `loc` | Location | 分支位置 | | `type` | string | 分支类型(if, switch, cond-expr, binary-expr) | | `locations` | Array<Location> | 分支条件的所有可能位置 | ### 6. Location (位置信息) 描述源代码中的位置。 | 字段 | 类型 | 描述 | |------|------|------| | `start` | Position | 开始位置 | | `end` | Position | 结束位置 | ### 7. Position (坐标位置) 源代码中的具体坐标。 | 字段 | 类型 | 描述 | |------|------|------| | `line` | number | 行号(从1开始) | | `column` | number | 列号(从0开始) | ## 计数器对象 ### 8. Statement Counters (语句计数器) 记录每个语句的执行次数。 ```json { "0": 5, // 语句ID 0 执行了5次 "1": 0, // 语句ID 1 未执行 "2": 3 // 语句ID 2 执行了3次 } ``` ### 9. Function Counters (函数计数器) 记录每个函数的调用次数。 ```json { "0": 2, // 函数ID 0 被调用了2次 "1": 0, // 函数ID 1 未被调用 "2": 1 // 函数ID 2 被调用了1次 } ``` ### 10. Branch Counters (分支计数器) 记录每个分支条件的执行次数。 ```json { "0": [5, 2], // 分支ID 0: true分支5次,false分支2次 "1": [0, 3], // 分支ID 1: true分支0次,false分支3次 "2": [1, 1] // 分支ID 2: 两个分支各执行1次 } ``` ## 分支类型说明 ### Branch Types (分支类型) | 类型 | 描述 | 示例 | |------|------|------| | `if` | if语句 | `if (condition) { ... }` | | `switch` | switch语句 | `switch (value) { case 1: ... }` | | `cond-expr` | 三元运算符 | `condition ? value1 : value2` | | `binary-expr` | 逻辑运算符 | `a && b`, `x \|\| y` | ## 报告摘要数据 ### 11. Summary (摘要信息) 每种覆盖率类型的统计摘要。 | 字段 | 类型 | 描述 | |------|------|------| | `total` | number | 总数量 | | `covered` | number | 已覆盖数量 | | `skipped` | number | 跳过数量 | | `pct` | number | 覆盖率百分比 | ### 12. Coverage Summary (覆盖率摘要) 文件或整体的覆盖率摘要。 | 字段 | 类型 | 描述 | |------|------|------| | `lines` | Summary | 行覆盖率摘要 | | `functions` | Summary | 函数覆盖率摘要 | | `statements` | Summary | 语句覆盖率摘要 | | `branches` | Summary | 分支覆盖率摘要 | ## 实际示例 ### math.js 文件内容 ```javascript function add(a, b) { return a + b; } function subtract(a, b) { if (a > b) { return a - b; } else { return b - a; } } function multiply(a, b) { return a * b; } function divide(a, b) { if (b === 0) { throw new Error('Division by zero'); } return a / b; } module.exports = { add, subtract, multiply, divide }; ``` ### 完整的FileCoverage示例 ```json { "path": "/project/src/math.js", "statementMap": { "0": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 25}}, "1": {"start": {"line": 2, "column": 2}, "end": {"line": 2, "column": 13}}, "2": {"start": {"line": 5, "column": 0}, "end": {"line": 5, "column": 30}}, "3": {"start": {"line": 6, "column": 2}, "end": {"line": 6, "column": 25}}, "4": {"start": {"line": 8, "column": 4}, "end": {"line": 8, "column": 15}}, "5": {"start": {"line": 10, "column": 4}, "end": {"line": 10, "column": 15}}, "6": {"start": {"line": 13, "column": 0}, "end": {"line": 13, "column": 25}}, "7": {"start": {"line": 14, "column": 2}, "end": {"line": 14, "column": 13}}, "8": {"start": {"line": 17, "column": 0}, "end": {"line": 17, "column": 30}}, "9": {"start": {"line": 18, "column": 2}, "end": {"line": 18, "column": 25}}, "10": {"start": {"line": 20, "column": 4}, "end": {"line": 20, "column": 15}}, "11": {"start": {"line": 22, "column": 4}, "end": {"line": 22, "column": 15}}, "12": {"start": {"line": 25, "column": 0}, "end": {"line": 25, "column": 25}} }, "fnMap": { "0": { "name": "add", "decl": {"start": {"line": 1, "column": 9}, "end": {"line": 1, "column": 12}}, "loc": {"start": {"line": 1, "column": 20}, "end": {"line": 3, "column": 1}}, "line": 1 }, "1": { "name": "subtract", "decl": {"start": {"line": 5, "column": 9}, "end": {"line": 5, "column": 17}}, "loc": {"start": {"line": 5, "column": 25}, "end": {"line": 12, "column": 1}}, "line": 5 }, "2": { "name": "multiply", "decl": {"start": {"line": 13, "column": 9}, "end": {"line": 13, "column": 16}}, "loc": {"start": {"line": 13, "column": 24}, "end": {"line": 15, "column": 1}}, "line": 13 }, "3": { "name": "divide", "decl": {"start": {"line": 17, "column": 9}, "end": {"line": 17, "column": 15}}, "loc": {"start": {"line": 17, "column": 23}, "end": {"line": 24, "column": 1}}, "line": 17 } }, "branchMap": { "0": { "loc": {"start": {"line": 6, "column": 2}, "end": {"line": 6, "column": 25}}, "type": "if", "locations": [ {"start": {"line": 6, "column": 2}, "end": {"line": 6, "column": 25}}, {"start": {"line": 6, "column": 2}, "end": {"line": 6, "column": 25}} ] }, "1": { "loc": {"start": {"line": 18, "column": 2}, "end": {"line": 18, "column": 25}}, "type": "if", "locations": [ {"start": {"line": 18, "column": 2}, "end": {"line": 18, "column": 25}}, {"start": {"line": 18, "column": 2}, "end": {"line": 18, "column": 25}} ] } }, "s": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 3, "5": 2, "6": 1, "7": 1, "8": 1, "9": 1, "10": 1, "11": 1, "12": 1}, "f": {"0": 5, "1": 3, "2": 2, "3": 1}, "b": {"0": [3, 2], "1": [1, 0]} } ``` # 安装与构建 学习如何在本机安装并启动 Canyon 社区版(Node.js 部署方式)。 ## 1. 克隆仓库并进入 app ```bash git clone https://github.com/canyon-project/canyon cd canyon/app ``` ## 2. 安装依赖 ```bash pnpm install ``` ## 3. 配置环境变量 创建 `app/.env`(可从现有 `.env` 模板复制),至少包含: ```bash DATABASE_URL=postgres://username:password@host:5432/dbname ``` ## 4. 生成并执行建表 SQL 在 `app` 目录执行: ```bash pnpm run migrate:sql ``` 该命令会生成 `app/schema.sql`。然后手动执行该 SQL(示例): ```bash psql "postgres://username:password@host:5432/dbname" -f schema.sql ``` ## 5. 启动服务 开发模式: ```bash pnpm dev ``` 生产模式(可选): ```bash pnpm build pnpm start ``` ## 6. 访问 默认访问地址: - 开发模式(`pnpm dev`): - 前端页面:`http://localhost:3000` - API:`http://localhost:3000/api` - Swagger UI:`http://localhost:3000/api/ui` - OpenAPI JSON:`http://localhost:3000/api/doc` - 生产模式(`pnpm start`): - 前端页面:`http://localhost:8080` - API:`http://localhost:8080/api` - Swagger UI:`http://localhost:8080/api/ui` - OpenAPI JSON:`http://localhost:8080/api/doc` # 前置条件 本文介绍以 **Node.js + PostgreSQL** 方式部署 Canyon 社区版所需的前置条件(不使用 Docker)。 ## 运行环境 - 操作系统:Linux / macOS / Windows(可正常运行 Node.js 与 PostgreSQL) - Node.js:建议 `20+` - pnpm:建议 `9+` - PostgreSQL:建议 `14+` ## 安装 Node.js 与 pnpm 先安装 Node.js,再安装 pnpm。 - [Node.js 下载地址](https://nodejs.org/en/download) - [pnpm 安装指南](https://pnpm.io/installation) 安装完成后可验证: ```bash node -v pnpm -v ``` ## 准备 PostgreSQL Canyon 使用 PostgreSQL 存储数据,请准备好可访问的数据库并记录连接串: ```text postgres://username:password@host:5432/dbname ``` 后续在 `app/.env` 中通过 `DATABASE_URL` 使用该连接串。 # 配置与访问 学习如何配置和访问 Canyon 社区版。 ## 1. 初始化 Git 提供商配置(GitLab) 如果你使用 GitLab,请先导入 `app/prisma/import-infra.sql` 中的配置到数据库。 该文件默认包含两项: - `GITLAB_BASE_URL` - `GITLAB_PRIVATE_TOKEN` 执行前请先将 `GITLAB_PRIVATE_TOKEN` 修改为你自己的 Token。 示例: ```bash cd app psql "postgres://username:password@host:5432/dbname" -f prisma/import-infra.sql ``` ## 2. 启动并访问系统 启动服务后,打开: - 开发模式:`http://localhost:3000` - 生产模式:`http://localhost:8080` API 文档入口: - Swagger UI:`/api/ui` - OpenAPI JSON:`/api/doc` 可在页面中查看仓库、commit、compare 及覆盖率报告能力。 ## 3. 重要说明 - 当前社区版文档优先覆盖 GitLab 场景。 - 如果你使用自建 GitLab,请将 `GITLAB_BASE_URL` 改为你的实例地址。 - 如果 Token 权限不足,会影响 compare、源码拉取、快照生成等能力。

CANYON

更准确的 JavaScript 代码覆盖率数据收集 Canyon 通过简单的 Babel 配置实现 JavaScript 代码覆盖率收集和实时报告生成,解决了端到端测试中的覆盖率收集难题。
[快速开始](/docs/getting-started/introduction) · [GitHub 仓库](https://github.com/canyon-project/canyon)
## 特性 Canyon 专为 JavaScript 端到端测试覆盖率收集而设计,具有以下特性: - [准确高效](/docs/core-concepts/separate-hit-and-map):通过分离 hit 和 map 数据,在编译时生成初始覆盖率数据,准确高效地收集覆盖率信息 - [SourceMap](/docs/core-concepts/restore-source-code-coverage):准确还原源代码覆盖率 - [构建工具](/docs/installation/getting-started):为 Next.js、Vite、Webpack 等常用构建工具提供覆盖率解决方案 - [自动化框架](/docs/end-to-end-testing/getting-started):为常见 UI 自动化框架提供集成方案 - [CI 提供商](/docs/reference/provider):适配 GitHub Actions、GitLab Runner 等常见 CI 提供商,自动检测 CI 环境变量 ## 社区
stars
downloads
Canyon 由 [travzhang](https://github.com/travzhang) 创建。 关注 [@wr_zhang25](https://twitter.com/wr_zhang25) 获取项目最新动态。 欢迎加入 [GitHub Discussions](https://github.com/canyon-project/canyon/discussions)!