This is the full developer documentation for Canyon (JavaScript code coverage platform). # Start of Canyon documentation # 覆盖率累积 ## 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%` # 代码插桩 本章介绍前端测试中代码插桩的基本概念、常见插桩方式,以及在单元测试和端到端测试(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 i pnpm -g - pnpm i - pnpm run build - npm i @canyonjs/cli -g - canyon -h - canyon upload --dsn=https://app.canyonjs.io/api/coverage/map/init - mv dist/* public artifacts: paths: - public only: - main ``` ## 准备就绪 现在检查页面上的 window.coverage 对象,如截图所示。window.coverage 不再包含 map 数据,这可以大大降低 UI 自动化覆盖率收集过程中的大量传输成本。 ![](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/jietu1.png) 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 // #endregion }, ], ], }; ``` | 配置项 | 描述 | 是否必填 | 默认值 | | ------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------- | | dsn | 覆盖率报告地址,CI 流水线变量 KEY 为 DSN | 是(根据需要在 CI 变量配置中填写或手动显式配置) | None | | reporter | 用于区分不同用户的用户令牌,CI 流水线变量 KEY 为 REPORTER | 是(根据需要在 CI 变量配置中填写或手动显式配置) | None | | repoID | 仓库 ID | 一般无需手动配置(自动检测 CI Provider) | None | | sha | Git 提交 SHA | 一般无需手动配置(自动检测 CI Provider) | None | | branch | Git 仓库分支 | 一般无需手动配置(自动检测 CI Provider) | None | | reportID | 用于区分不同测试用例 | 可选 | None | | compareTarget | 比较目标,作为当前 SHA 的基准,用于计算变更行覆盖率 | 可选 | None | | keepMap | 保留覆盖率地图,可选,默认为 true,当为 false 时,将生成 .canyon_output 文件 | 可选 | true | | instrumentCwd | 插桩工作目录,在多仓库模式下可能需要手动配置 | 可选 | process.cwd() | | provider | 源代码提供商(可选),默认为 gitlab | 可选 | gitlab | | oneByOne | 配置代理服务器,可选,默认为 false。当为 true 时,会在编译过程中逐个报告每个文件的初始覆盖率数据。也可以是代理服务器配置 | 可选 | false | 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/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/next/examples/report-html-vitest) | | `Jest` | [report-html-jest](https://github.com/canyon-project/canyon/tree/next/examples/report-html-jest) | | `RSTest` | [report-html-rstest](https://github.com/canyon-project/canyon/tree/next/examples/report-html-rstest) | | `Mocha` | [report-html-mocha](https://github.com/canyon-project/canyon/tree/next/examples/report-html-mocha) | | `Test Runner` | [report-html-test-runner](https://github.com/canyon-project/canyon/tree/next/examples/report-html-test-runner) | 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 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 等框架项目,能够准确追踪打包后代码的覆盖率情况。 ## 以新项目的形式开始 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 i pnpm -g - pnpm i - pnpm run build - npm i @canyonjs/cli -g - canyon -h - canyon upload --dsn=https://app.canyonjs.io/api/coverage/map/init - mv dist/* public artifacts: paths: - public only: - main ``` ### 报告覆盖率数据 等待 CI 完成后,将页面部署到测试环境。为了收集人工测试的覆盖率数据,您需要在 HTML 文件中引入 Canyon 收集脚本: ```html copy ``` 配置完成后,随后的测试就会不断收集上报覆盖率数据到服务端。 # 介绍 ## 概述 Canyon(意为“峡谷”,发音 /ˈkænjən/)是一个 JavaScript 代码覆盖率收集平台。我们解决了开发人员和 QA 工程师在端到端测试期间收集测试用例代码覆盖率时遇到的困难。它由三个主要组件组成: - 一系列[插件](https://github.com/canyon-project/canyon/tree/main/plugins),负责适配各种 CI 提供商并检测环境变量。 - 一个[Node.js 服务](https://github.com/canyon-project/canyon/tree/main/packages/canyon-backend),负责收集和处理覆盖率数据,提供后端 API。 - 一个[React 前端应用](https://github.com/canyon-project/canyon/tree/main/packages/canyon-platform),负责显示覆盖率数据。 ![](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 插件 | | [tools-cli] | [![tools-cli-status]][tools-cli-package] | 扫描本地 `.canyon_output` 数据并上传到服务器 | | [tools-collect] | [![tools-collect-status]][tools-collect-package] | 用于网站自动收集覆盖率数据的脚本包 | | [canyon-extension] | [![canyon-extension-status]][canyon-extension-package] | 用于手动报告覆盖率数据的 Chrome 扩展 | [@canyonjs/babel-plugin]: /docs/ecosystem/babel-plugin [vite-plugin-canyon]: https://github.com/canyon-project/vite-plugin-canyon [tools-cli]: /docs/ecosystem/tools-cli [tools-collect]: /docs/ecosystem/tools-collect [canyon-extension]: /docs/ecosystem/canyon-extension [@canyonjs/babel-plugin-status]: https://img.shields.io/npm/v/@canyonjs/babel-plugin.svg [vite-plugin-canyon-status]: https://img.shields.io/npm/v/vite-plugin-canyon.svg [tools-cli-status]: https://img.shields.io/github/v/release/canyon-project/canyon [tools-collect-status]: https://img.shields.io/npm/v/@canyonjs/collect.svg [canyon-extension-status]: https://img.shields.io/chrome-web-store/v/omnpafdjidgpdmlimbangcjjaaodbeof.svg [@canyonjs/babel-plugin-package]: https://npmjs.com/package/@canyonjs/babel-plugin [vite-plugin-canyon-package]: https://npmjs.com/package/babel-plugin-vite [tools-cli-package]: https://github.com/canyon-project/canyon/releases [tools-collect-package]: https://npmjs.com/package/@canyonjs/collect [canyon-extension-package]: https://chrome.google.com/webstore/detail/canyon/omnpafdjidgpdmlimbangcjjaaodbeof ## 架构 下图显示了 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 / GraphQL 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] > > 目前仅支持 Babel 插件。SWC、Vite 和其他插件仍在开发中。[插件](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", "canyon"], }; ``` ### 完成了 继续查看[首次获取覆盖率数据 - 验证](/docs/getting-started/first-coverage#%E6%A3%80%E6%9F%A5)。 # Vite import { Steps } from "nextra/components"; 安装和配置 Vite。 ### 创建项目 首先,使用以下命令创建一个新的 React 项目 `vite`。 > [!TIP] > > 目前仅支持 Vite 的 React 模板 ```sh npm2yarn copy npm create vite@latest ``` ### 添加 Canyon 和配置 ```sh npm2yarn copy npm install babel-plugin-istanbul @canyonjs/babel-plugin -D ``` ### 配置 Babel 插件 ``` ts filename="vite.config.ts" copy {7,8,9} import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react({ babel:{ plugins:["istanbul","canyon"] } })], }) ``` ### 完成了 继续查看[首次获取覆盖率数据 - 验证](/docs/getting-started/first-coverage#%E6%A3%80%E6%9F%A5)。 # 安装与构建 学习如何安装和构建 Canyon 社区版。 ## 配置环境 开始安装前,您需要配置环境变量。 ```yaml # Prisma 配置 DATABASE_URL=postgres://username:password@localhost:5432/dbname ``` ## Node.js 克隆项目 ```shell git clone https://github.com/canyon-project/canyon ``` 安装依赖并构建项目 ```shell pnpm install && pnpm run build ``` 修改 .env 文件并创建数据库表 ```shell pnpm run migrate ``` 使用 Node.js 运行项目 ```shell node packages/canyon-backend/dist/main.js ``` ## Docker 要使用 Docker 运行 Canyon,只需指定环境变量文件路径和端口号 ```shell docker run -d -p 8080:8080 -v /your/path/.env:/app/.env zhangtao25/canyon:main ``` ## 成功启动 成功启动后,将有两个主要服务: - Canyon 前端 - Canyon 后端 访问 `http://localhost:8000/login` 登录 Canyon。 # 前置条件 在自己的基础设施上安装 Canyon 的前置条件。 Canyon 是一个自托管的 API 开发平台,打包为一组 Docker 容器。您可以在任何能够运行 [Docker Engine](https://docs.docker.com/engine) 的操作系统上安装和运行 Canyon。您可以在本地计算机或您选择的云提供商上使用 Canyon。 ## 系统要求 Canyon 设计为在小型和大型部署中都能良好运行。运行 Canyon 的最低要求是支持 Docker 的操作系统,生成构建镜像需要 4 个 CPU 核心 + 4GB RAM,托管生成的输出文件至少需要 1 个 CPU 核心 + 2GB RAM。 ### 安装 Node.js、npm、pnpm #### Node.js + npm 安装 [`Node.js`](https://nodejs.org/en) (v18+) 和 [`npm`](https://www.npmjs.com) (v9+)。 - [Node.js + npm 安装指南](https://nodejs.org/en/download) ### pnpm 安装 [`pnpm`](https://pnpm.io)(推荐 v9)。 - [pnpm 安装指南](https://pnpm.io/installation) ### Docker 安装 [`Docker`](https://www.docker.com) (v20+)。 - [Docker 安装指南](https://docs.docker.com/engine/install) ### Postgres 数据库 Canyon 使用 Postgres 数据库存储基础数据(需要 14.x 或更高版本)。您可以使用任何您选择的 Postgres 数据库提供商 - 本地托管或云提供商。确保您有以下格式的有效 Postgres 数据库 URL: ``` postgres://username:password@localhost:5432/dbname ``` #### 创建表 ```shell pnpm run migrate ``` 或直接使用表创建 [SQL](https://github.com/canyon-project/canyon/blob/main/packages/canyon-backend/prisma/create_table.sql)。 # 配置与访问 学习如何配置和访问 Canyon 社区版。 ## 系统设置 要使用 Canyon 的所有功能,您必须配置以下数据表: 1. `CANYON_SERVER`: Canyon 服务器地址 2. `GITLAB_SERVER`: GitLab 服务器地址 3. `GITLAB_CLIENT_ID`: GitLab 客户端 ID 4. `GITLAB_CLIENT_SECRET`: GitLab 客户端密钥 注意:回调地址为 `/oauth`,请确保在 GitLab 中正确配置。 import { Callout } from 'nextra/components'; ```sql copy insert into public.sys_setting (id, key, value) values ('1', 'CANYON_SERVER', 'https://canyonjs.org'), ('2', 'GITLAB_SERVER', 'https://gitlab.com'), ('3', 'GITLAB_CLIENT_ID', 'xxx'), ('4', 'GITLAB_CLIENT_SECRET', 'xxx'); ``` [将 GitLab 配置为 OAuth 2.0 认证身份提供商](https://docs.gitlab.com/ee/integration/oauth_provider.html) 目前仅支持 GitLab,未来将支持更多 Git 服务提供商。 ```sql copy insert into public.git_provider (id, url, type, name, disabled, private_token) values ('gitlab', 'https://gitlab.com', 'gitlab', 'GitLab', false, 'xxx'); ``` [生成 GitLab 私有令牌](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token) ## 管理员登录 创建以下用户。您可以使用以下凭据登录:邮箱:canyon,密码:123456 ```sql copy INSERT INTO "user" (id, email, password, nickname, avatar, favor) VALUES ('canyon', 'canyon@canyon.com', '123456', 'Canyon', '/avatar.jpg', ''); ``` ## GitLab OAuth2 登录 ![](https://cdn.jsdelivr.net/gh/canyon-project/assets/docs/static/docs/self-host/community-edition/login.png) # 视频演示 [Bilibili](https://www.bilibili.com/video/BV13sXHYDEn6) [YouTube](https://www.youtube.com/watch?v=-2IRQ_pmEjI)

CANYON

更准确的 JavaScript 代码覆盖率数据收集 Canyon(意为“峡谷”,发音 /ˈkænjən/)通过简单的 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)! # License Canyon is licensed under the MIT License. ```text MIT License Copyright (c) Canyon Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ```