This is the tiny developer documentation for Canyon (core concepts and getting started). # 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)!