monocart-coverage-reports 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,7 @@
14
14
 
15
15
  * [Usage](#usage)
16
16
  * [Best Practices](#best-practices)
17
+ * [AI Agent Skill](#ai-agent-skill)
17
18
  * [Options](#options)
18
19
  * [Available Reports](#available-reports)
19
20
  * [Compare Reports](#compare-reports)
@@ -118,20 +119,21 @@ For detailed setup instructions and examples, see the [best-practices guide](ski
118
119
 
119
120
  ## AI Agent Skill
120
121
 
121
- For AI-assisted development in a source checkout, start with [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md). It describes the source layout, development commands, validation, and safety boundaries. `.github/copilot-instructions.md` provides a self-contained summary of the core rules for GitHub Copilot. These checkout-only guides are not included in the npm package and are separate from the reusable integration skill below.
122
+ **Reliable AI-assisted development starts with automated testing.** Test results validate behavior; coverage highlights testing gaps. With **`mcr test`**, AI coding agents can run Node.js tests, collect native V8 coverage, and generate reports without instrumentation. This feedback supports iterative development and CI/CD quality gates, whether an agent assists a developer or works autonomously.
122
123
 
123
- The reusable [Monocart Coverage Reports skill](skills/monocart-coverage-reports/SKILL.md) helps AI coding agents collect coverage, configure the CLI, debug sourcemaps and filters, merge raw reports, and enforce CI thresholds. It uses a main `SKILL.md` with task-specific references loaded on demand, targeting MCR 3.x only and checking its runtime requirements.
124
+ The [Monocart Coverage Reports skill](skills/monocart-coverage-reports/SKILL.md) guides agents through coverage collection, CLI configuration, sourcemap troubleshooting, report merging, and CI thresholds. It targets MCR 3.x and loads task-specific references on demand.
124
125
 
125
- The npm package includes `skills/monocart-coverage-reports/`. Copy this **entire directory**, including `references/`, into your agent's supported skills location, or ask the agent to read `SKILL.md` directly. Installing MCR does not automatically register the skill or modify agent settings. The instructions are in English; requests and responses can use any language.
126
+ The skill ships in the npm package. Copy the **entire `skills/monocart-coverage-reports/` directory**, including `references/`, into your agent's skills directory, or have the agent read `SKILL.md` directly. Installation does not automatically register the skill. The instructions are in English; you can use any language with your agent.
126
127
 
127
- From the host project root, locate the installed skill and package manifest (older releases may not include the skill):
128
+ Locate the installed skill from your project root:
128
129
 
129
130
  ```sh
130
131
  node -p "require.resolve('monocart-coverage-reports/skills/monocart-coverage-reports/SKILL.md')"
131
- node -p "require.resolve('monocart-coverage-reports/package.json')"
132
132
  ```
133
133
 
134
- Read the resolved skill, then follow its relative reference links. Use the installed package's runtime, API, and version requirements rather than assuming all examples apply to older releases. These commands resolve files; they do not import Markdown as JavaScript.
134
+ Follow the skill's reference links and the installed package's version and runtime requirements. Older releases may not include the skill.
135
+
136
+ For development of MCR itself, see [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md).
135
137
 
136
138
  ## Options
137
139
  - Default options: [lib/default/options.js](./lib/default/options.js)
@@ -868,6 +870,53 @@ const coverageOptions = {
868
870
  };
869
871
  ```
870
872
 
873
+ ### `onTestEnd` — `mcr test` only
874
+
875
+ `onTestEnd(testResults)` runs once after the test runner exits and its JSON results are collected, before publishing the test results cache, `onReady`, and coverage generation. It is awaited even when assertions fail. Other commands, API calls, and runs skipped because no targets were supplied do not call it. The existing `onEnd(coverageResults)` is unchanged.
876
+
877
+ `mcr test` always collects structured test results using an internal JSON reporter, preserving terminal output and user-specified reporter/destination pairs. `onTestEnd` is optional; omitting it does not disable result collection. To retain results for later use, enable `raw` reports or debug logging; normal cache cleanup still applies.
878
+
879
+ ```js
880
+ // mcr.config.js
881
+ export default {
882
+ test: './test/**/*.test.js',
883
+ onTestEnd: (testResults) => {
884
+ console.log(testResults.summary);
885
+ for (const test of testResults.tests) {
886
+ console.log(test.name, test.status, test.error);
887
+ }
888
+ }
889
+ };
890
+ ```
891
+
892
+ - `id`: a collection ID generated with the same `Util.uid()` mechanism as coverage cache data.
893
+ - `framework`: `'node:test'`.
894
+ - `complete`: whether the final runner summary was received, not whether tests passed.
895
+ - `summary`: `{ tests, suites, passed, failed, cancelled, skipped, todo, duration, success }`; `null` if incomplete. Counts come from the runner's final summary; `tests` excludes suites. Durations are in milliseconds.
896
+ - `tests`: completed test and suite records, including nested tests: `{ name, type, nesting, testNumber, file?, line?, column?, duration, status, skip?, todo?, error? }`. Status is `passed`, `failed`, `cancelled`, `skipped`, or `todo`. Errors always include string `name` and `message` (defaults: `'Error'` and `''`). Other own enumerable data properties are copied only when their values are strings, booleans, finite numbers, or `null`; BigInts become decimal strings. Getters/setters, objects/arrays, functions, symbols, `undefined`, `NaN`, and infinities are omitted without recursive traversal. Apart from the two fixed fields, inherited/non-enumerable properties are not collected, so normal `stack` is omitted and fields inside `cause` are not flattened. Explicitly enumerable string `stack` is copied like any other string. Reserved keys `__proto__`, `constructor`, and `prototype` are excluded; the fixed fields cannot be overwritten by extra properties.
897
+
898
+ Test failures, incomplete/missing/invalid reports, and hook errors keep the command nonzero. An incomplete report is delivered with `complete: false`; a missing or invalid JSON report cannot be delivered and is logged as a collection error. A nonzero `process.exitCode` set by `onTestEnd` cannot be cleared by later coverage hooks. Hook/collection errors do not prevent MCR from attempting coverage generation. If the hook throws, test results are not published or exported, rather than falling back to possibly unredacted data. Test results are independent of coverage, so this hook can run even if coverage generation later fails.
899
+
900
+ Test results use the same ID and filename mechanism as coverage cache data: `outputDir/.cache/test-results-<id>.json`, with the same `id` stored in the JSON. The reporter streams records into `.json.tmp` with its collection ID already included. A small `.meta.tmp` completion record verifies the final byte count. Without a hook, MCR reads only this completion record and renames the JSON, without loading or rewriting the full report. With a hook, MCR reads the full result object and publishes its mutations after the hook completes. Temporary output and metadata are removed before coverage generation. Finalized JSON follows the coverage cache lifecycle: normally cleaned after generation, retained in debug mode, or exported by `raw` (including ZIP). It is not embedded in HTML/coverage JSON reports, and importing raw data does not yet merge test results. Save results yourself if needed; files written into `outputDir` by this hook may be removed by the subsequent coverage cleanup when `clean: true`.
901
+
902
+ **Sensitive data:** Primitive does not mean non-sensitive: a string `token`, `password`, or response body is also copied. Messages, test names, and paths may contain credentials or personal data; large strings can also increase report size. MCR does not redact them automatically. Keep the output directory private and review raw/debug artifacts before uploading them. Mutate results in `onTestEnd` to sanitize the persisted JSON; this does not redact Node's terminal output or data already written to temporary files. For example, adapt the following rule to your application's data:
903
+
904
+ ```js
905
+ export default {
906
+ onTestEnd: (results) => {
907
+ for (const test of results.tests) {
908
+ if (test.error) {
909
+ for (const [key, value] of Object.entries(test.error)) {
910
+ if (typeof value === 'string') {
911
+ test.error[key] = value.replace(/(Bearer\s+)\S+/gi, '$1[REDACTED]');
912
+ }
913
+ }
914
+ }
915
+ }
916
+ }
917
+ };
918
+ ```
919
+
871
920
  ### `onStart` / `onReady` — CLI-only
872
921
  Only available when using `mcr <command>`:
873
922
  - `onStart(coverageReport)` — runs before the child process is spawned.
@@ -985,6 +1034,7 @@ The options below are only available when using the CLI (`mcr <command>`):
985
1034
  | `--require` | `string` | `null` | Deprecated alias for `--import`; uses ESM loading, not CommonJS resolution |
986
1035
  | `--env` | `string` | `null` | Load environment variables from a dotenv file (defaults to `.env`). |
987
1036
  | `test` (config only) | `string \| string[] \| null` | `null` | Default test file paths or glob patterns accepted by `node --test`, passed unchanged when `mcr test` has no positional targets; ignored by other commands and the API. Requires Node.js `>= 22.15.0` |
1037
+ | `onTestEnd` (config only) | `(testResults) => void \| Promise<void>` | `null` | Only `mcr test`: runs after test result collection, before `onReady` and coverage generation; see [Hooks](#hooks) |
988
1038
  | `onStart` | `(coverageReport) => Promise<void>` | `null` | Runs before the child process is spawned; see [Hooks](#hooks) |
989
1039
  | `onReady` | `(coverageReport, nodeV8CoverageDir, subprocess) => Promise<void>` | `null` | Runs after the child exits, before MCR reads coverage; see [Hooks](#hooks) |
990
1040
 
@@ -1103,6 +1153,9 @@ The following usage scenarios may require merging coverage reports:
1103
1153
  ### Manual Merging
1104
1154
  If the reports cannot be merged automatically, then here is how to manually merge the reports.
1105
1155
  First, using the `raw` report to export the original coverage data to the specified directory.
1156
+
1157
+ **Before uploading raw/debug artifacts (3.2.0+):** `mcr test` also exports `test-results-<id>.json` in raw reports (including ZIP) and retains it in debug caches, even without a hook. Test names, paths and error strings may contain secrets or personal data. Review artifacts and use [`onTestEnd`](#ontestend--mcr-test-only) to redact persisted test results before publication; this does not redact terminal output or temporary files already written. Raw imports merge coverage only, not test results.
1158
+
1106
1159
  - For example, we have `raw` coverage data from `unit test`, which is output to `./coverage-reports/unit/raw`. Unit test examples:
1107
1160
  - `Jest` + [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage)
1108
1161
  - `Vitest` + [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage)
package/README.zh-Hans.md CHANGED
@@ -14,6 +14,7 @@
14
14
 
15
15
  * [用法](#usage)
16
16
  * [最佳实践](#best-practices)
17
+ * [AI Agent Skill](#ai-agent-skill)
17
18
  * [选项配置](#options)
18
19
  * [所有支持的报告类型](#available-reports)
19
20
  * [比较两种报告](#compare-reports)
@@ -118,20 +119,21 @@ mcr node my-app.js -r v8,console-details
118
119
 
119
120
  ## AI Agent Skill
120
121
 
121
- 使用 AI 辅助开发本仓库源码时,请先阅读 [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md),了解源码结构、开发命令、验证流程和安全边界。`.github/copilot-instructions.md` 为 GitHub Copilot 提供可独立使用的核心规则摘要。这些指南仅用于源码仓库,不随 npm 包发布,与下面可复用的集成技能相互独立。
122
+ **可靠的 AI 辅助开发离不开自动化测试。** 测试结果验证代码行为,覆盖率帮助识别测试缺口。通过 **`mcr test`**,AI 编程助手可直接运行 Node.js 测试、采集原生 V8 覆盖率并生成报告,无需插桩。测试与覆盖率反馈支持持续迭代,并可结合 CI/CD 质量门禁,用于 AI 辅助或自主开发。
122
123
 
123
- 项目提供可复用的 [Monocart Coverage Reports 技能](skills/monocart-coverage-reports/SKILL.md),帮助 AI coding agent 采集覆盖率、配置 CLI、排查 sourcemap 和过滤问题、合并 raw 报告及设置 CI 阈值。结构为主 `SKILL.md` 加按需加载的专题参考文档,仅面向 MCR 3.x,并检查其运行环境要求。
124
+ [Monocart Coverage Reports 技能](skills/monocart-coverage-reports/SKILL.md) 指导 AI 配置覆盖率采集和 CLI、排查 sourcemap 问题、合并报告及设置 CI 阈值,适用于 MCR 3.x,专题参考文档按需加载。
124
125
 
125
- npm 包包含 `skills/monocart-coverage-reports/`。请将**整个目录**(包括 `references/`)复制到 agent 支持的 skills 目录,或直接让 agent 读取 `SKILL.md`。安装 MCR 不会自动注册技能或修改 agent 配置。技能说明使用英文,但提问和回复可以使用中文或其他语言。
126
+ 技能随 npm 包发布。将**整个 `skills/monocart-coverage-reports/` 目录**(含 `references/`)复制到 AI 工具的技能目录,或让 AI 直接读取 `SKILL.md`。安装 MCR 不会自动注册技能。技能说明使用英文,与 AI 交流可使用任意语言。
126
127
 
127
- 在使用 MCR 的项目根目录中定位已安装的技能和包配置(旧版本可能尚未包含技能):
128
+ 在项目根目录定位已安装的技能:
128
129
 
129
130
  ```sh
130
131
  node -p "require.resolve('monocart-coverage-reports/skills/monocart-coverage-reports/SKILL.md')"
131
- node -p "require.resolve('monocart-coverage-reports/package.json')"
132
132
  ```
133
133
 
134
- 读取定位到的技能文件,再按相对链接读取专题文档。请以已安装包的运行时、API 和版本要求为准,不要假设所有示例都适用于旧版本。以上命令用于定位文件,不是将 Markdown 作为 JavaScript 导入。
134
+ 按技能中的链接查阅参考文档,并以已安装包的版本和运行环境要求为准。旧版本可能不包含该技能。
135
+
136
+ 开发 MCR 本身时,请参阅 [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md)。
135
137
 
136
138
  ## Options
137
139
  - 默认选项: [lib/default/options.js](./lib/default/options.js)
@@ -871,6 +873,53 @@ const coverageOptions = {
871
873
  };
872
874
  ```
873
875
 
876
+ ### `onTestEnd` — 仅 `mcr test` 可用
877
+
878
+ `onTestEnd(testResults)` 在测试运行器退出、JSON 测试结果采集完成后触发一次,早于测试结果缓存发布、`onReady` 和覆盖率报告生成。支持异步等待,测试断言失败时也会触发。其他命令、API 调用,以及因未提供目标而跳过的测试,不触发该 hook。现有 `onEnd(coverageResults)` 保持不变。
879
+
880
+ `mcr test` 始终通过内部 JSON reporter 采集结构化测试结果,同时保留终端输出和用户指定的 reporter/destination 配对。`onTestEnd` 为可选回调,不配置也不会关闭采集。如需留待后续使用,请启用 `raw` 报告或 debug 日志;默认的缓存清理规则仍然生效。
881
+
882
+ ```js
883
+ // mcr.config.js
884
+ export default {
885
+ test: './test/**/*.test.js',
886
+ onTestEnd: (testResults) => {
887
+ console.log(testResults.summary);
888
+ for (const test of testResults.tests) {
889
+ console.log(test.name, test.status, test.error);
890
+ }
891
+ }
892
+ };
893
+ ```
894
+
895
+ - `id`:采用与覆盖率缓存数据相同的 `Util.uid()` 机制生成的采集 ID。
896
+ - `framework`:`'node:test'`。
897
+ - `complete`:是否收到运行器最终汇总,不代表测试是否通过。
898
+ - `summary`:`{ tests, suites, passed, failed, cancelled, skipped, todo, duration, success }`,报告不完整时为 `null`。计数采用运行器最终汇总,`tests` 不包含 suite,耗时单位为毫秒。
899
+ - `tests`:已完成的测试及 suite 记录,包含嵌套测试:`{ name, type, nesting, testNumber, file?, line?, column?, duration, status, skip?, todo?, error? }`。状态为 `passed`、`failed`、`cancelled`、`skipped` 或 `todo`。错误固定包含字符串 `name`、`message`,不可用或非字符串时分别使用 `'Error'`、`''`。其余只复制自有、可枚举的数据属性,值仅限字符串、布尔值、有限数字和 `null`;BigInt 转为十进制字符串。不递归遍历,跳过 getter/setter、对象/数组、函数、Symbol、`undefined`、`NaN` 和无穷数。除两个固定字段外,不采集继承或不可枚举属性,因此通常不包含 `stack`,也不展开 `cause` 内的字段;明确设为可枚举的字符串 `stack` 仍按普通字符串复制。过滤 `__proto__`、`constructor`、`prototype` 等特殊键,额外属性不会覆盖固定字段。
900
+
901
+ 测试失败、报告不完整/缺失/损坏或 hook 抛错,均保持非零退出状态。不完整报告以 `complete: false` 交付;缺失或无效的 JSON 无法交付,会明确记录采集错误。`onTestEnd` 设置的非零 `process.exitCode` 不会被后续覆盖率 hook 清零。hook 或采集失败后仍尝试生成覆盖率报告。hook 抛错时不会发布或导出测试结果,避免回退导出可能尚未脱敏的数据。测试结果独立于覆盖率,因此即使后续覆盖率生成失败,此 hook 仍可执行。
902
+
903
+ 测试结果复用覆盖率缓存的 ID 和文件命名机制:`outputDir/.cache/test-results-<id>.json`,JSON 内容保存同一个 `id`。reporter 将记录逐条写入 `.json.tmp`,输出时即包含采集 ID,并通过很小的 `.meta.tmp` 完成记录校验最终字节数。没有 hook 时,MCR 只读取完成记录并重命名 JSON,不会加载或重写完整报告;配置 hook 时,读取完整结果对象,在 hook 完成后将修改后的数据发布为正式缓存。临时输出和元数据均在生成覆盖率前清理。正式 JSON 遵循覆盖率缓存生命周期:正常生成后清理、调试模式保留,或由 `raw` 报告导出(包括 ZIP)。它不会嵌入 HTML/覆盖率 JSON 报告,导入 raw 数据目前也不会合并测试结果。如需持久化,请自行保存;当 `clean: true` 时,此 hook 写入 `outputDir` 的文件可能被后续覆盖率清理流程删除。
904
+
905
+ **敏感信息提醒:** 基础类型不等于非敏感数据,字符串 `token`、`password` 或响应正文同样会被复制。错误消息、测试名称和路径也可能包含凭据或个人信息,大字符串仍可能增大报告体积,MCR 不会自动脱敏。请保护输出目录,并在上传 raw/debug 产物前审查内容。可在 `onTestEnd` 中修改结果,对正式保存的 JSON 脱敏;这不会处理 Node 的终端输出或已经写入临时文件的数据。以下规则仅为示例,请按业务数据调整:
906
+
907
+ ```js
908
+ export default {
909
+ onTestEnd: (results) => {
910
+ for (const test of results.tests) {
911
+ if (test.error) {
912
+ for (const [key, value] of Object.entries(test.error)) {
913
+ if (typeof value === 'string') {
914
+ test.error[key] = value.replace(/(Bearer\s+)\S+/gi, '$1[REDACTED]');
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ };
921
+ ```
922
+
874
923
  ### `onStart` / `onReady` — 仅CLI可用
875
924
  只在使用 `mcr <command>` 时有效:
876
925
  - `onStart(coverageReport)` — 子进程启动前触发
@@ -988,6 +1037,7 @@ npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
988
1037
  | `--require` | `string` | `null` | `--import` 的已弃用别名;使用 ESM 加载,不使用 CommonJS 解析 |
989
1038
  | `--env` | `string` | `null` | 从 dotenv 文件加载环境变量(默认 `.env`)。|
990
1039
  | `test`(仅配置文件) | `string \| string[] \| null` | `null` | 未指定命令行目标时,原样传给 `node --test` 的默认测试文件路径或 glob,**仅 `mcr test` 使用**;其他命令和 API 忽略。需要 Node.js `>= 22.15.0` |
1040
+ | `onTestEnd`(仅配置文件) | `(testResults) => void \| Promise<void>` | `null` | 仅 `mcr test`:测试结果采集完成后、`onReady` 和覆盖率生成前触发;参见 [Hooks](#hooks) |
991
1041
  | `onStart` | `(coverageReport) => Promise<void>` | `null` | 子进程启动前触发;参见 [Hooks](#hooks) |
992
1042
  | `onReady` | `(coverageReport, nodeV8CoverageDir, subprocess) => Promise<void>` | `null` | 子进程退出后、MCR 读取覆盖率前触发;参见 [Hooks](#hooks) |
993
1043
 
@@ -1105,6 +1155,9 @@ npx mcr -c ./mcr.config.js -- test "../tests/**/*.test.js" -- --test-reporter=sp
1105
1155
 
1106
1156
  ### Manual Merging
1107
1157
  手动合并覆盖率报告需要使用`raw`报告来导出原始的覆盖率数据到指定的目录
1158
+
1159
+ **上传 raw/debug 产物前请注意(3.2.0+):** 即使未配置 hook,`mcr test` 也会在 raw 报告(包括 ZIP)中导出 `test-results-<id>.json`,并在 debug 缓存中保留该文件。测试名称、路径及错误字符串可能包含凭据或个人信息。请先审查产物,并通过 [`onTestEnd`](#ontestend--仅-mcr-test-可用) 在发布前对正式保存的测试结果脱敏;这不会处理终端输出或已写入的临时文件。导入 raw 数据只合并覆盖率,不会合并测试结果。
1160
+
1108
1161
  - 比如,单元测试保存到`./coverage-reports/unit/raw`,见例子
1109
1162
  - `Jest` + [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage)
1110
1163
  - `Vitest` + [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage)
package/lib/cli.js CHANGED
@@ -9,6 +9,7 @@ import { program } from 'commander';
9
9
  import { execa } from 'execa';
10
10
 
11
11
  import { MCR } from './index.js';
12
+ import { getTestCommand, createTestRunner } from './test/test-runner.js';
12
13
  import { Util } from './utils/util.js';
13
14
  import pkg from '../package.json' with { type: 'json' };
14
15
 
@@ -85,17 +86,6 @@ const mergeCoverage = async (cliOptions) => {
85
86
  await coverageReport.generate();
86
87
  };
87
88
 
88
- // Only check whether targets were supplied. Node owns all target parsing,
89
- // discovery and validation; preserve each argument exactly as provided.
90
- const getTestTargets = (test) => {
91
- if (!test || (Array.isArray(test) && !test.length)) {
92
- Util.logInfo('No test targets configured; skipping tests.');
93
- return;
94
- }
95
-
96
- return Array.isArray(test) ? test : [test];
97
- };
98
-
99
89
  // Preserve signals and failing commands even if a hook sets exitCode=0.
100
90
  const getExitStatus = (code, signal) => signal || code || process.exitCode || 0;
101
91
 
@@ -177,24 +167,19 @@ const executeCommand = async ([command, ... commandArgs], cliOptions, testArgs =
177
167
 
178
168
  // console.log(options);
179
169
  const coverageReport = MCR(cliOptions);
180
- const isTest = command === 'test';
181
170
  await coverageReport.loadConfig(cliOptions.config);
182
171
 
183
172
  const coverageOptions = coverageReport.options;
184
- let childArgs;
185
- if (isTest) {
186
- const targets = getTestTargets(coverageOptions.test);
187
- if (!targets) {
173
+ let testRunner;
174
+ if (command === 'test') {
175
+ testRunner = createTestRunner(coverageOptions, testArgs);
176
+ if (!testRunner) {
188
177
  return;
189
178
  }
190
- childArgs = ['--test', ... testArgs, ... targets];
191
179
  }
192
180
  coverageReport.cleanCache();
193
181
  const nodeV8CoverageDir = initNodeV8CoverageDir(coverageOptions);
194
- if (isTest) {
195
- // Tests may spawn children with a different working directory.
196
- process.env.NODE_V8_COVERAGE = path.resolve(nodeV8CoverageDir);
197
- }
182
+ testRunner?.initCoverageDir(nodeV8CoverageDir);
198
183
 
199
184
  // =========================================
200
185
  // onStart hook
@@ -204,22 +189,26 @@ const executeCommand = async ([command, ... commandArgs], cliOptions, testArgs =
204
189
  }
205
190
  // =========================================
206
191
 
207
- const onClose = async (code, signal, subprocess) => {
192
+ const generateCoverage = async (subprocess) => {
193
+ // onReady hook before adding coverage data. Some children need
194
+ // additional time to finish writing their coverage files.
195
+ const onReady = coverageOptions.onReady;
196
+ if (typeof onReady === 'function') {
197
+ await onReady(coverageReport, nodeV8CoverageDir, subprocess);
198
+ }
199
+ await coverageReport.addFromDir(nodeV8CoverageDir);
200
+ return coverageReport.generate();
201
+ };
208
202
 
209
- // generate coverage even it is failed. code != 0
203
+ const onClose = async (code, signal, subprocess) => {
210
204
 
205
+ // Generate coverage even when the child failed (code != 0).
211
206
  try {
212
- // onReady hook before adding coverage data. Some children need
213
- // additional time to finish writing their coverage files.
214
- const onReady = coverageOptions.onReady;
215
- if (typeof onReady === 'function') {
216
- await onReady(coverageReport, nodeV8CoverageDir, subprocess);
217
- }
218
-
219
- await coverageReport.addFromDir(nodeV8CoverageDir);
220
- const report = await coverageReport.generate();
221
- if (isTest && !report) {
222
- throw new Error('No coverage report was generated');
207
+ const generate = () => generateCoverage(subprocess);
208
+ if (testRunner) {
209
+ await testRunner.onClose(generate);
210
+ } else {
211
+ await generate();
223
212
  }
224
213
  } catch (error) {
225
214
  Util.logError(`Coverage report generation failed: ${error.message}`);
@@ -233,8 +222,8 @@ const executeCommand = async ([command, ... commandArgs], cliOptions, testArgs =
233
222
  return getExitStatus(code, signal);
234
223
  };
235
224
 
236
- if (isTest) {
237
- return executeSubprocess(process.execPath, childArgs, onClose);
225
+ if (testRunner) {
226
+ return testRunner.execute(executeSubprocess, onClose);
238
227
  }
239
228
  return executeSubprocess(command, commandArgs, onClose);
240
229
 
@@ -261,30 +250,6 @@ process.argv.forEach((it) => {
261
250
  }
262
251
  });
263
252
 
264
- // Also recognize `mcr -c config -- test targets -- runner-options` without
265
- // falling through to the shell's unrelated `test` command. Only MCR options
266
- // before the first separator are parsed in that form.
267
- const getTestCommand = (args, subArgs) => {
268
- if (args[0] === 'test') {
269
- return {
270
- targets: args.slice(1), runnerArgs: subArgs.slice(1)
271
- };
272
- }
273
- if (args.length || subArgs[1] !== 'test') {
274
- return;
275
- }
276
- const remaining = subArgs.slice(2);
277
- const index = remaining.indexOf('--');
278
- if (index === -1) {
279
- return {
280
- targets: remaining, runnerArgs: []
281
- };
282
- }
283
- return {
284
- targets: remaining.slice(0, index), runnerArgs: remaining.slice(index + 1)
285
- };
286
- };
287
-
288
253
  program
289
254
  .name('mcr')
290
255
  .description('CLI to generate coverage reports')
@@ -489,6 +489,7 @@ const viteImportDefaultHandler = (node, group) => {
489
489
 
490
490
  const collectNodes = (ast) => {
491
491
  const functionNodes = [];
492
+ const initializerNodes = [];
492
493
  const statementNodes = [];
493
494
  const blockNodes = [];
494
495
  const branchMap = new Map();
@@ -514,6 +515,12 @@ const collectNodes = (ast) => {
514
515
  });
515
516
  },
516
517
 
518
+ PropertyDefinition: (node) => {
519
+ if (node.value) {
520
+ initializerNodes.push(node);
521
+ }
522
+ },
523
+
517
524
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
518
525
  // as a function "functionName": "<static_initializer>",
519
526
  StaticBlock: (node, parents) => {
@@ -646,6 +653,7 @@ const collectNodes = (ast) => {
646
653
 
647
654
  return {
648
655
  functionNodes,
656
+ initializerNodes,
649
657
  statementNodes,
650
658
  blockNodes,
651
659
  branchMap
@@ -735,12 +743,30 @@ const findFunctionRange = (item, coverageInfo) => {
735
743
  const collectAstInfo = (ast, coverageInfo) => {
736
744
 
737
745
  const {
738
- functionNodes, statementNodes, blockNodes, branchMap
746
+ functionNodes, initializerNodes, statementNodes, blockNodes, branchMap
739
747
  } = collectNodes(ast);
740
748
 
741
749
  // root function state
742
750
  const rootState = getRootFunctionState(ast, coverageInfo);
743
751
 
752
+ // V8 groups field values into implicit functions. Use their counts and
753
+ // block ranges without adding synthetic functions to the report. Keys
754
+ // execute at class definition time, so only the value gets this state.
755
+ initializerNodes.forEach((node) => {
756
+ const ranges = node.static ? coverageInfo.functionStaticRanges : coverageInfo.functionInstanceRanges;
757
+ const { value } = node;
758
+ const range = Util.findInRanges(value.start, value.end, ranges, 'startOffset', 'endOffset');
759
+ if (range) {
760
+ value._state = {
761
+ isFunction: true,
762
+ count: range.count,
763
+ range
764
+ };
765
+ }
766
+ });
767
+
768
+ // Explicit functions (including arrow-valued fields) keep their own
769
+ // execution state, overriding any initializer state on the value node.
744
770
  const functions = [];
745
771
  functionNodes.forEach((item) => {
746
772
  const { node } = item;
@@ -9,6 +9,7 @@ const getCoverageInfo = (coverageList) => {
9
9
  const functionMap = new Map();
10
10
  const functionNameMap = new Map();
11
11
  const functionStaticRanges = [];
12
+ const functionInstanceRanges = [];
12
13
  const functionUncoveredRanges = [];
13
14
 
14
15
  let rootRange;
@@ -66,6 +67,9 @@ const getCoverageInfo = (coverageList) => {
66
67
  if (functionName === '<static_initializer>') {
67
68
  functionStaticRanges.push(functionRange);
68
69
  }
70
+ if (functionName === '<instance_members_initializer>') {
71
+ functionInstanceRanges.push(functionRange);
72
+ }
69
73
 
70
74
  // cache uncovered
71
75
  if (functionRange.count === 0) {
@@ -76,11 +80,14 @@ const getCoverageInfo = (coverageList) => {
76
80
 
77
81
  // sort ranges
78
82
  Util.sortOffsetRanges(functionUncoveredRanges);
83
+ Util.sortOffsetRanges(functionStaticRanges);
84
+ Util.sortOffsetRanges(functionInstanceRanges);
79
85
 
80
86
  return {
81
87
  functionMap,
82
88
  functionNameMap,
83
89
  functionStaticRanges,
90
+ functionInstanceRanges,
84
91
  functionUncoveredRanges,
85
92
  rootRange
86
93
  };
@@ -220,7 +227,19 @@ const addRule = (item, coverageList, coveredRanges, count = 0) => {
220
227
  const endOffset = end.offset;
221
228
 
222
229
  if (!count) {
223
- const coveredRange = Util.findInRanges(startOffset, endOffset, coveredRanges, 'startOffset', 'endOffset');
230
+ let matchEnd = endOffset;
231
+ if (item.type === 'rule') {
232
+ const child = item.nodes?.find((node) => node.type === 'rule' || node.type === 'atrule');
233
+ if (child) {
234
+ // Chromium 158 may stop the used parent range before a nested
235
+ // rule. Keep legacy whole-rule accounting, matching only the
236
+ // parent's prefix rather than requiring its entire subtree.
237
+ // TODO: Track native nested rules and trailing declarations
238
+ // separately, including their sparse sourcemap boundaries.
239
+ matchEnd = startOffset + source.input.css.slice(startOffset, child.source.start.offset).trimEnd().length;
240
+ }
241
+ }
242
+ const coveredRange = Util.findInRanges(startOffset, matchEnd, coveredRanges, 'startOffset', 'endOffset');
224
243
  if (coveredRange) {
225
244
  count = 1;
226
245
  }
@@ -761,31 +761,25 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
761
761
  }
762
762
 
763
763
  const { decodedMappings } = originalState;
764
- const len = decodedMappings.length;
765
- if (len < 2) {
766
- return;
767
- }
768
- // sort by original line/column
769
- const startMapping = decodedMappings[0];
770
- const endMapping = decodedMappings[len - 1];
771
- // console.log(startMapping, endMapping);
772
- const startOffset = startMapping.generatedOffset;
773
-
774
- if (!endMapping.generatedEndOffset) {
775
- // line last one
776
- const line = state.locator.getLine(endMapping.generatedLine + 1);
777
- // could be no line found
778
- if (line) {
779
- // last column
780
- endMapping.generatedEndOffset = line.end;
781
- } else {
782
- endMapping.generatedEndOffset = endMapping.generatedOffset;
764
+ // The generated range the file's own mappings start in. Leave out the
765
+ // added line-end mappings: the last one is at the end of its line, which
766
+ // in a minified bundle is the end of the whole script, not of the wrapper
767
+ // the file is in.
768
+ let startOffset = Infinity;
769
+ let endOffset = -Infinity;
770
+ decodedMappings.forEach((mapping) => {
771
+ if (mapping.end) {
772
+ return;
783
773
  }
784
- }
785
- const endOffset = endMapping.generatedEndOffset;
774
+ startOffset = Math.min(startOffset, mapping.generatedOffset);
775
+ endOffset = Math.max(endOffset, mapping.generatedOffset);
776
+ });
786
777
 
787
- // console.log('===========================================================', originalState.sourcePath);
788
- // console.log(originalState.source.length, endMapping);
778
+ // A single real mapping is enough; auxiliary line-end mappings are not
779
+ // guaranteed for every source. Do not infer coverage without mappings.
780
+ if (startOffset === Infinity) {
781
+ return;
782
+ }
789
783
 
790
784
  checkList.push({
791
785
  originalState,
@@ -819,7 +813,9 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
819
813
  if (range.count > 0) {
820
814
  continue;
821
815
  }
822
- if (startOffset >= range.start && endOffset <= range.end) {
816
+ // endOffset is the last mapping's start, not an exclusive end.
817
+ // V8 ranges exclude range.end, so a mapping there is outside.
818
+ if (startOffset >= range.start && endOffset < range.end) {
823
819
  // console.log('------------', 'added');
824
820
  originalState.bytes.push({
825
821
  start: 0,
@@ -111,6 +111,11 @@ const defaultOptions = {
111
111
  // onEntry: async (entry) => {}
112
112
  onEntry: null,
113
113
 
114
+ // (mcr test only) {function} Optional hook before test result publication, onReady and coverage generation.
115
+ // Results are always collected. Redact sensitive messages/names here before raw/debug export.
116
+ // onTestEnd: async (testResults) => {}
117
+ onTestEnd: null,
118
+
114
119
  // {function} onEnd hook
115
120
  // onEnd: async (reportData) => {}
116
121
  onEnd: null
package/lib/generate.js CHANGED
@@ -303,8 +303,9 @@ const addZipData = async (mcr, dataList, sourceCache, dir) => {
303
303
  }
304
304
  const entryName = entry.name;
305
305
  const filename = path.basename(entryName);
306
- // console.log('============================', filename);
307
- if (filename.endsWith('.json')) {
306
+ // Do not decompress or parse test results (or other unrelated JSON)
307
+ // when merging coverage. Match the cache reader's accepted prefixes.
308
+ if (filename.endsWith('.json') && (filename.startsWith('coverage-') || filename.startsWith('source-'))) {
308
309
  const buf = await zip.entryData(entryName);
309
310
  const json = JSON.parse(buf.toString('utf-8'));
310
311
  await addJsonData(mcr, dataList, sourceCache, json, filename);