react-native-coverage 0.1.0
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/Coverage.podspec +20 -0
- package/LICENSE +202 -0
- package/README.md +125 -0
- package/android/build.gradle +51 -0
- package/android/rn-coverage-jacoco.gradle +132 -0
- package/android/rn-coverage.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/coverage/CoverageModule.kt +53 -0
- package/android/src/main/java/com/coverage/CoveragePackage.kt +35 -0
- package/app.plugin.js +1 -0
- package/bin/rn-coverage.js +11 -0
- package/cocoapods/coverage_post_install.rb +197 -0
- package/docs/cli.md +51 -0
- package/docs/config.md +27 -0
- package/docs/index.md +16 -0
- package/docs/integration/android.md +83 -0
- package/docs/integration/ci-appium.md +41 -0
- package/docs/integration/e2e-timing.md +20 -0
- package/docs/integration/ios.md +77 -0
- package/docs/integration/js.md +38 -0
- package/docs/pattern-c.md +5 -0
- package/docs/releasing.md +88 -0
- package/docs.json +24 -0
- package/ios/Coverage.h +5 -0
- package/ios/Coverage.mm +47 -0
- package/ios/CoverageConfig.h +21 -0
- package/ios/CoverageProfile.h +22 -0
- package/ios/CoverageProfile.mm +358 -0
- package/lib/module/NativeCoverage.js +10 -0
- package/lib/module/NativeCoverage.js.map +1 -0
- package/lib/module/assert-coverage.js +181 -0
- package/lib/module/assert-coverage.js.map +1 -0
- package/lib/module/cli/index.js +229 -0
- package/lib/module/cli/index.js.map +1 -0
- package/lib/module/config.js +83 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/exit-codes.js +22 -0
- package/lib/module/exit-codes.js.map +1 -0
- package/lib/module/index.js +35 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/js-coverage.js +172 -0
- package/lib/module/js-coverage.js.map +1 -0
- package/lib/module/node.js +15 -0
- package/lib/module/node.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/path-rewrite.js +44 -0
- package/lib/module/path-rewrite.js.map +1 -0
- package/lib/module/process-ios-native-coverage.js +258 -0
- package/lib/module/process-ios-native-coverage.js.map +1 -0
- package/lib/module/pull-native-coverage.js +210 -0
- package/lib/module/pull-native-coverage.js.map +1 -0
- package/lib/typescript/example-dynamic/App.d.ts +6 -0
- package/lib/typescript/example-dynamic/App.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeCoverage.d.ts +16 -0
- package/lib/typescript/src/NativeCoverage.d.ts.map +1 -0
- package/lib/typescript/src/assert-coverage.d.ts +53 -0
- package/lib/typescript/src/assert-coverage.d.ts.map +1 -0
- package/lib/typescript/src/cli/index.d.ts +4 -0
- package/lib/typescript/src/cli/index.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +93 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/exit-codes.d.ts +16 -0
- package/lib/typescript/src/exit-codes.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/js-coverage.d.ts +36 -0
- package/lib/typescript/src/js-coverage.d.ts.map +1 -0
- package/lib/typescript/src/node.d.ts +14 -0
- package/lib/typescript/src/node.d.ts.map +1 -0
- package/lib/typescript/src/path-rewrite.d.ts +12 -0
- package/lib/typescript/src/path-rewrite.d.ts.map +1 -0
- package/lib/typescript/src/process-ios-native-coverage.d.ts +61 -0
- package/lib/typescript/src/process-ios-native-coverage.d.ts.map +1 -0
- package/lib/typescript/src/pull-native-coverage.d.ts +38 -0
- package/lib/typescript/src/pull-native-coverage.d.ts.map +1 -0
- package/package.json +184 -0
- package/plugin/build/index.d.ts +21 -0
- package/plugin/build/index.js +153 -0
- package/react-native-coverage.config.js.example +47 -0
- package/react-native.config.js +9 -0
- package/src/NativeCoverage.ts +16 -0
- package/src/assert-coverage.ts +260 -0
- package/src/cli/index.ts +420 -0
- package/src/config.ts +189 -0
- package/src/exit-codes.ts +20 -0
- package/src/index.tsx +35 -0
- package/src/js-coverage.ts +213 -0
- package/src/node.ts +52 -0
- package/src/path-rewrite.ts +52 -0
- package/src/process-ios-native-coverage.ts +425 -0
- package/src/pull-native-coverage.ts +269 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { CoverageConfig } from './config';
|
|
5
|
+
import { DEFAULT_COVERAGE_CONFIG } from './config';
|
|
6
|
+
import { EXIT_OK, EXIT_STRICT_EMPTY } from './exit-codes';
|
|
7
|
+
|
|
8
|
+
export type AssertResult = {
|
|
9
|
+
code: number;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function failEmpty(strict: boolean, message: string): AssertResult {
|
|
14
|
+
if (strict) {
|
|
15
|
+
return { code: EXIT_STRICT_EMPTY, message };
|
|
16
|
+
}
|
|
17
|
+
return { code: EXIT_OK, message: `(soft) ${message}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type LcovAssertStats = {
|
|
21
|
+
sourceFileCount: number;
|
|
22
|
+
matchedPathHits: number;
|
|
23
|
+
linesHit: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse LCOV and count SF: paths matching configured includes.
|
|
28
|
+
* When `lcovPathIncludes` is empty, any SF: path counts as a match.
|
|
29
|
+
* `linesHit` sums `LH:` only for matched source files.
|
|
30
|
+
*/
|
|
31
|
+
export function analyzeLcov(
|
|
32
|
+
lcovPath: string,
|
|
33
|
+
lcovPathIncludes: string[] = []
|
|
34
|
+
): LcovAssertStats {
|
|
35
|
+
const text = fs.readFileSync(lcovPath, 'utf8');
|
|
36
|
+
let sourceFileCount = 0;
|
|
37
|
+
let matchedPathHits = 0;
|
|
38
|
+
let linesHit = 0;
|
|
39
|
+
let currentMatched = false;
|
|
40
|
+
|
|
41
|
+
for (const line of text.split('\n')) {
|
|
42
|
+
if (line.startsWith('SF:')) {
|
|
43
|
+
sourceFileCount += 1;
|
|
44
|
+
const sf = line.slice(3).replace(/\\/g, '/');
|
|
45
|
+
currentMatched =
|
|
46
|
+
lcovPathIncludes.length === 0 ||
|
|
47
|
+
lcovPathIncludes.some(
|
|
48
|
+
(include) => sf.includes(include) || sf.startsWith(include)
|
|
49
|
+
);
|
|
50
|
+
if (currentMatched) {
|
|
51
|
+
matchedPathHits += 1;
|
|
52
|
+
}
|
|
53
|
+
} else if (line.startsWith('LH:') && currentMatched) {
|
|
54
|
+
linesHit += Number(line.slice(3)) || 0;
|
|
55
|
+
} else if (line.startsWith('end_of_record')) {
|
|
56
|
+
currentMatched = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { sourceFileCount, matchedPathHits, linesHit };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Assert iOS (or any) LCOV has expected path hits with actual line coverage.
|
|
65
|
+
* Exit 2 in strict mode when missing/empty/no matched SF: paths/no LH hits.
|
|
66
|
+
*/
|
|
67
|
+
export function assertIosLcov(
|
|
68
|
+
lcovPath: string,
|
|
69
|
+
strict: boolean,
|
|
70
|
+
config: CoverageConfig = DEFAULT_COVERAGE_CONFIG,
|
|
71
|
+
cwd: string = process.cwd()
|
|
72
|
+
): AssertResult {
|
|
73
|
+
const relative = path.relative(cwd, lcovPath) || lcovPath;
|
|
74
|
+
const includes = config.assert.lcovPathIncludes;
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(lcovPath)) {
|
|
77
|
+
return failEmpty(strict, `iOS LCOV missing: ${relative}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stat = fs.statSync(lcovPath);
|
|
81
|
+
if (stat.size === 0) {
|
|
82
|
+
return failEmpty(strict, `iOS LCOV empty (0 bytes): ${relative}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { sourceFileCount, matchedPathHits, linesHit } = analyzeLcov(
|
|
86
|
+
lcovPath,
|
|
87
|
+
includes
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (matchedPathHits === 0 || linesHit === 0) {
|
|
91
|
+
const includeDesc =
|
|
92
|
+
includes.length > 0 ? includes.join(', ') : '(any SF: path)';
|
|
93
|
+
return failEmpty(
|
|
94
|
+
strict,
|
|
95
|
+
`iOS LCOV has no expected path/line hits [${includeDesc}] (sourceFiles=${sourceFileCount}, matchedPathHits=${matchedPathHits}, linesHit=${linesHit}): ${relative}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
code: EXIT_OK,
|
|
101
|
+
message: `iOS ok: matchedPathHits=${matchedPathHits} sourceFiles=${sourceFileCount} linesHit=${linesHit} (${relative})`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type JacocoAssertStats = {
|
|
106
|
+
packageCount: number;
|
|
107
|
+
lineCovered: number;
|
|
108
|
+
lineMissed: number;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse Jacoco XML package rollups. Matchers are case-insensitive substrings
|
|
113
|
+
* of `<package name="...">`. Empty matchers → all packages.
|
|
114
|
+
*
|
|
115
|
+
* Jacoco uses `/` separators (`com/coverage/fixture`); configs often use Java
|
|
116
|
+
* dots (`coverage.fixture`). Normalize both so either form matches.
|
|
117
|
+
*/
|
|
118
|
+
export function analyzeJacocoXml(
|
|
119
|
+
xmlPath: string,
|
|
120
|
+
packageIncludes: string[] = []
|
|
121
|
+
): JacocoAssertStats {
|
|
122
|
+
const xml = fs.readFileSync(xmlPath, 'utf8');
|
|
123
|
+
const packageBlocks = [
|
|
124
|
+
...xml.matchAll(/<package name="([^"]+)"[^>]*>([\s\S]*?)<\/package>/g),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const normalizePkg = (value: string) =>
|
|
128
|
+
value.toLowerCase().replace(/[./]/g, '/');
|
|
129
|
+
const normalizedIncludes = packageIncludes.map(normalizePkg);
|
|
130
|
+
|
|
131
|
+
let packageCount = 0;
|
|
132
|
+
let lineCovered = 0;
|
|
133
|
+
let lineMissed = 0;
|
|
134
|
+
|
|
135
|
+
for (const match of packageBlocks) {
|
|
136
|
+
const name = match[1]!;
|
|
137
|
+
const body = match[2]!;
|
|
138
|
+
const normalizedName = normalizePkg(name);
|
|
139
|
+
if (
|
|
140
|
+
normalizedIncludes.length > 0 &&
|
|
141
|
+
!normalizedIncludes.some((m) => normalizedName.includes(m))
|
|
142
|
+
) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
packageCount += 1;
|
|
146
|
+
|
|
147
|
+
const trailing = body.match(
|
|
148
|
+
/((?:<counter type="[^"]+" missed="\d+" covered="\d+"\/>\s*)+)$/
|
|
149
|
+
);
|
|
150
|
+
if (!trailing) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const c of trailing[1]!.matchAll(
|
|
154
|
+
/<counter type="(LINE|INSTRUCTION)" missed="(\d+)" covered="(\d+)"\/>/g
|
|
155
|
+
)) {
|
|
156
|
+
if (c[1] === 'LINE') {
|
|
157
|
+
lineMissed += Number(c[2]);
|
|
158
|
+
lineCovered += Number(c[3]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { packageCount, lineCovered, lineMissed };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Assert Android Jacoco XML has expected package LINE hits.
|
|
168
|
+
* Exit 2 in strict mode when missing/empty/no covered lines in matched packages.
|
|
169
|
+
*/
|
|
170
|
+
export function assertAndroidJacoco(
|
|
171
|
+
xmlPath: string,
|
|
172
|
+
strict: boolean,
|
|
173
|
+
config: CoverageConfig = DEFAULT_COVERAGE_CONFIG,
|
|
174
|
+
cwd: string = process.cwd()
|
|
175
|
+
): AssertResult {
|
|
176
|
+
const relative = path.relative(cwd, xmlPath) || xmlPath;
|
|
177
|
+
const includes =
|
|
178
|
+
config.assert.jacocoPackageIncludes.length > 0
|
|
179
|
+
? config.assert.jacocoPackageIncludes
|
|
180
|
+
: config.android.libraryProjectMatchers;
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(xmlPath)) {
|
|
183
|
+
return failEmpty(strict, `Android Jacoco XML missing: ${relative}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const stat = fs.statSync(xmlPath);
|
|
187
|
+
if (stat.size === 0) {
|
|
188
|
+
return failEmpty(strict, `Android Jacoco XML empty (0 bytes): ${relative}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { packageCount, lineCovered, lineMissed } = analyzeJacocoXml(
|
|
192
|
+
xmlPath,
|
|
193
|
+
includes
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (packageCount === 0 || lineCovered === 0) {
|
|
197
|
+
const includeDesc =
|
|
198
|
+
includes.length > 0 ? includes.join(', ') : '(any package)';
|
|
199
|
+
return failEmpty(
|
|
200
|
+
strict,
|
|
201
|
+
`Android Jacoco has empty package hits [${includeDesc}] (packages=${packageCount}, lineCovered=${lineCovered}, lineMissed=${lineMissed}): ${relative}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
code: EXIT_OK,
|
|
207
|
+
message: `Android ok: packages=${packageCount} lineCovered=${lineCovered} lineMissed=${lineMissed} (${relative})`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type AssertCoverageOptions = {
|
|
212
|
+
platform?: 'ios' | 'android' | 'all';
|
|
213
|
+
lcov?: string;
|
|
214
|
+
jacocoXml?: string;
|
|
215
|
+
strict?: boolean;
|
|
216
|
+
config?: CoverageConfig;
|
|
217
|
+
cwd?: string;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Post-pipeline presence check. Returns the highest-priority failure code
|
|
222
|
+
* (prefer EXIT_STRICT_EMPTY over EXIT_OK).
|
|
223
|
+
*/
|
|
224
|
+
export function assertCoverage(
|
|
225
|
+
options: AssertCoverageOptions = {}
|
|
226
|
+
): AssertResult {
|
|
227
|
+
const config = options.config ?? DEFAULT_COVERAGE_CONFIG;
|
|
228
|
+
const strict = options.strict ?? config.strict;
|
|
229
|
+
const cwd = options.cwd ?? process.cwd();
|
|
230
|
+
const platform = options.platform ?? 'all';
|
|
231
|
+
|
|
232
|
+
const lcov = options.lcov ?? path.resolve(cwd, config.assert.defaultLcovPath);
|
|
233
|
+
const jacocoXml =
|
|
234
|
+
options.jacocoXml ?? path.resolve(cwd, config.assert.defaultJacocoXmlPath);
|
|
235
|
+
|
|
236
|
+
const results: AssertResult[] = [];
|
|
237
|
+
|
|
238
|
+
if (platform === 'ios' || platform === 'all') {
|
|
239
|
+
results.push(assertIosLcov(lcov, strict, config, cwd));
|
|
240
|
+
}
|
|
241
|
+
if (platform === 'android' || platform === 'all') {
|
|
242
|
+
results.push(assertAndroidJacoco(jacocoXml, strict, config, cwd));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let code = EXIT_OK;
|
|
246
|
+
const messages: string[] = [];
|
|
247
|
+
for (const result of results) {
|
|
248
|
+
messages.push(result.message);
|
|
249
|
+
if (result.code === EXIT_STRICT_EMPTY) {
|
|
250
|
+
code = EXIT_STRICT_EMPTY;
|
|
251
|
+
} else if (result.code !== EXIT_OK && code === EXIT_OK) {
|
|
252
|
+
code = result.code;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
code,
|
|
258
|
+
message: messages.join('\n'),
|
|
259
|
+
};
|
|
260
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
import { assertCoverage } from '../assert-coverage';
|
|
5
|
+
import type { CoverageConfig } from '../config';
|
|
6
|
+
import { loadCoverageConfig } from '../config';
|
|
7
|
+
import {
|
|
8
|
+
EXIT_ERROR,
|
|
9
|
+
EXIT_OK,
|
|
10
|
+
EXIT_STRICT_EMPTY,
|
|
11
|
+
StrictEmptyError,
|
|
12
|
+
} from '../exit-codes';
|
|
13
|
+
import {
|
|
14
|
+
exportIosLcov,
|
|
15
|
+
reportIosHtml,
|
|
16
|
+
summarizeIos,
|
|
17
|
+
} from '../process-ios-native-coverage';
|
|
18
|
+
import {
|
|
19
|
+
pullAndroidCoverageWithRetry,
|
|
20
|
+
pullIosCoverage,
|
|
21
|
+
resolveAndroidDeviceId,
|
|
22
|
+
runJacocoTestReport,
|
|
23
|
+
} from '../pull-native-coverage';
|
|
24
|
+
import {
|
|
25
|
+
pullAndroidJsCoverage,
|
|
26
|
+
pullIosJsCoverage,
|
|
27
|
+
reportJsCoverage,
|
|
28
|
+
} from '../js-coverage';
|
|
29
|
+
|
|
30
|
+
export { EXIT_OK, EXIT_ERROR, EXIT_STRICT_EMPTY };
|
|
31
|
+
|
|
32
|
+
type RootOpts = {
|
|
33
|
+
config?: string;
|
|
34
|
+
strict?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function applyStrictOverride(
|
|
38
|
+
config: CoverageConfig,
|
|
39
|
+
rootOpts: RootOpts
|
|
40
|
+
): CoverageConfig {
|
|
41
|
+
if (rootOpts.strict === true) {
|
|
42
|
+
config.strict = true;
|
|
43
|
+
} else if (rootOpts.strict === false) {
|
|
44
|
+
config.strict = false;
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rootOptsFrom(cmd: Command): RootOpts {
|
|
50
|
+
// android/ios subcommands: parent.parent is program
|
|
51
|
+
// assert: parent is program
|
|
52
|
+
const parent = cmd.parent;
|
|
53
|
+
if (parent?.parent) {
|
|
54
|
+
return (parent.parent.opts() ?? {}) as RootOpts;
|
|
55
|
+
}
|
|
56
|
+
return (parent?.opts() ?? {}) as RootOpts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mapErrorToExit(error: unknown): number {
|
|
60
|
+
// Exit 2 only for explicit empty/missing coverage-hit guards.
|
|
61
|
+
// Tooling/config failures (e.g. missing app binary) stay exit 1.
|
|
62
|
+
if (error instanceof StrictEmptyError) {
|
|
63
|
+
return EXIT_STRICT_EMPTY;
|
|
64
|
+
}
|
|
65
|
+
return EXIT_ERROR;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main(): Promise<void> {
|
|
69
|
+
const program = new Command();
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.name('rn-coverage')
|
|
73
|
+
.description(
|
|
74
|
+
'Native coverage CLI for React Native (pull / export / report / summary / assert). Pattern C: dedicated test apps only.'
|
|
75
|
+
)
|
|
76
|
+
.option('-c, --config <path>', 'Path to react-native-coverage.config.js')
|
|
77
|
+
.option('--strict', 'Fail with exit 2 when artifacts/hits are empty')
|
|
78
|
+
.option('--no-strict', 'Warn instead of exit 2 on empty artifacts');
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('android')
|
|
82
|
+
.description('Android coverage commands')
|
|
83
|
+
.addCommand(
|
|
84
|
+
new Command('pull')
|
|
85
|
+
.description('Pull .ec coverage from a connected device/emulator')
|
|
86
|
+
.option('--device <id>', 'adb device serial')
|
|
87
|
+
.option('--output <dir>', 'Local output directory')
|
|
88
|
+
.option('--retries <n>', 'Retry count while waiting for .ec', '15')
|
|
89
|
+
.action(async (opts, cmd) => {
|
|
90
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
91
|
+
const config = applyStrictOverride(
|
|
92
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
93
|
+
rootOpts
|
|
94
|
+
);
|
|
95
|
+
try {
|
|
96
|
+
const deviceId = resolveAndroidDeviceId(opts.device);
|
|
97
|
+
const pulled = await pullAndroidCoverageWithRetry(deviceId, {
|
|
98
|
+
softFail: !config.strict,
|
|
99
|
+
outputDir: opts.output,
|
|
100
|
+
retries: Number(opts.retries) || 15,
|
|
101
|
+
config,
|
|
102
|
+
});
|
|
103
|
+
if (!pulled && config.strict) {
|
|
104
|
+
console.error(
|
|
105
|
+
'[rn-coverage] Android coverage.ec missing (strict)'
|
|
106
|
+
);
|
|
107
|
+
process.exitCode = EXIT_STRICT_EMPTY;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
111
|
+
process.exitCode = mapErrorToExit(error);
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
.addCommand(
|
|
116
|
+
new Command('report')
|
|
117
|
+
.description(
|
|
118
|
+
'Run jacocoTestReport then assert Jacoco XML LINE hits (exit 2 if strict empty)'
|
|
119
|
+
)
|
|
120
|
+
.option('--android-dir <path>', 'Android project directory', 'android')
|
|
121
|
+
.option('--jacoco-xml <path>', 'Jacoco XML path to assert after report')
|
|
122
|
+
.action(async (opts, cmd) => {
|
|
123
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
124
|
+
const config = applyStrictOverride(
|
|
125
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
126
|
+
rootOpts
|
|
127
|
+
);
|
|
128
|
+
const result = runJacocoTestReport({
|
|
129
|
+
androidDir: opts.androidDir,
|
|
130
|
+
jacocoXml: opts.jacocoXml,
|
|
131
|
+
config,
|
|
132
|
+
});
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
process.exitCode = result.exitCode;
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command('ios')
|
|
141
|
+
.description('iOS coverage commands')
|
|
142
|
+
.addCommand(
|
|
143
|
+
new Command('pull')
|
|
144
|
+
.description('Pull .profraw from a simulator app container')
|
|
145
|
+
.requiredOption('--device <udid>', 'Simulator UDID')
|
|
146
|
+
.option('--output <dir>', 'Local output directory')
|
|
147
|
+
.action(async (opts, cmd) => {
|
|
148
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
149
|
+
const config = applyStrictOverride(
|
|
150
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
151
|
+
rootOpts
|
|
152
|
+
);
|
|
153
|
+
try {
|
|
154
|
+
const pulled = pullIosCoverage(opts.device, {
|
|
155
|
+
outputDir: opts.output,
|
|
156
|
+
softFail: !config.strict,
|
|
157
|
+
config,
|
|
158
|
+
});
|
|
159
|
+
if (pulled.length === 0 && config.strict) {
|
|
160
|
+
process.exitCode = EXIT_STRICT_EMPTY;
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
164
|
+
process.exitCode = mapErrorToExit(error);
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
.addCommand(
|
|
169
|
+
new Command('export')
|
|
170
|
+
.description(
|
|
171
|
+
'Merge profraw and export LCOV via llvm-cov (asserts path hits; exit 2 if strict empty)'
|
|
172
|
+
)
|
|
173
|
+
.requiredOption('--derived-data <path>', 'Xcode derived data path')
|
|
174
|
+
.option('--configuration <name>', 'Xcode configuration', 'Debug')
|
|
175
|
+
.option('--app-name <name>', 'App product name')
|
|
176
|
+
.option('--output <path>', 'LCOV output path', 'coverage/ios/lcov.info')
|
|
177
|
+
.action(async (opts, cmd) => {
|
|
178
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
179
|
+
const config = applyStrictOverride(
|
|
180
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
181
|
+
rootOpts
|
|
182
|
+
);
|
|
183
|
+
try {
|
|
184
|
+
await exportIosLcov({
|
|
185
|
+
derivedData: opts.derivedData,
|
|
186
|
+
configuration: opts.configuration,
|
|
187
|
+
appName: opts.appName ?? config.app.iosProductName,
|
|
188
|
+
output: opts.output,
|
|
189
|
+
config,
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
193
|
+
process.exitCode = mapErrorToExit(error);
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
.addCommand(
|
|
198
|
+
new Command('report')
|
|
199
|
+
.description('HTML report via llvm-cov show -format=html')
|
|
200
|
+
.requiredOption('--derived-data <path>', 'Xcode derived data path')
|
|
201
|
+
.option('--configuration <name>', 'Xcode configuration', 'Debug')
|
|
202
|
+
.option('--app-name <name>', 'App product name')
|
|
203
|
+
.option(
|
|
204
|
+
'--profdata <path>',
|
|
205
|
+
'Merged profdata path',
|
|
206
|
+
'coverage/ios/profdata'
|
|
207
|
+
)
|
|
208
|
+
.option(
|
|
209
|
+
'--output-dir <path>',
|
|
210
|
+
'HTML output directory',
|
|
211
|
+
'coverage/ios/html'
|
|
212
|
+
)
|
|
213
|
+
.action(async (opts, cmd) => {
|
|
214
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
215
|
+
const config = applyStrictOverride(
|
|
216
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
217
|
+
rootOpts
|
|
218
|
+
);
|
|
219
|
+
try {
|
|
220
|
+
reportIosHtml({
|
|
221
|
+
derivedData: opts.derivedData,
|
|
222
|
+
configuration: opts.configuration,
|
|
223
|
+
appName: opts.appName,
|
|
224
|
+
profdata: opts.profdata,
|
|
225
|
+
outputDir: opts.outputDir,
|
|
226
|
+
config,
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
230
|
+
process.exitCode = mapErrorToExit(error);
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
)
|
|
234
|
+
.addCommand(
|
|
235
|
+
new Command('summary')
|
|
236
|
+
.description('Terminal summary via llvm-cov report')
|
|
237
|
+
.requiredOption('--derived-data <path>', 'Xcode derived data path')
|
|
238
|
+
.option('--configuration <name>', 'Xcode configuration', 'Debug')
|
|
239
|
+
.option('--app-name <name>', 'App product name')
|
|
240
|
+
.option(
|
|
241
|
+
'--profdata <path>',
|
|
242
|
+
'Merged profdata path',
|
|
243
|
+
'coverage/ios/profdata'
|
|
244
|
+
)
|
|
245
|
+
.action(async (opts, cmd) => {
|
|
246
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
247
|
+
const config = applyStrictOverride(
|
|
248
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
249
|
+
rootOpts
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
summarizeIos({
|
|
253
|
+
derivedData: opts.derivedData,
|
|
254
|
+
configuration: opts.configuration,
|
|
255
|
+
appName: opts.appName,
|
|
256
|
+
profdata: opts.profdata,
|
|
257
|
+
config,
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
261
|
+
process.exitCode = mapErrorToExit(error);
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
program
|
|
267
|
+
.command('js')
|
|
268
|
+
.description(
|
|
269
|
+
'Istanbul/NYC TypeScript coverage (source-map remap after Metro e2e)'
|
|
270
|
+
)
|
|
271
|
+
.addCommand(
|
|
272
|
+
new Command('pull')
|
|
273
|
+
.description(
|
|
274
|
+
'Pull coverage-final.json written by flush()/dumpJsCoverage'
|
|
275
|
+
)
|
|
276
|
+
.option('--platform <name>', 'android | ios', 'android')
|
|
277
|
+
.option('--device <id>', 'adb serial or simulator UDID')
|
|
278
|
+
.option('--output <dir>', 'Local output directory', 'coverage/js')
|
|
279
|
+
.action(async (opts, cmd) => {
|
|
280
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
281
|
+
const config = applyStrictOverride(
|
|
282
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
283
|
+
rootOpts
|
|
284
|
+
);
|
|
285
|
+
try {
|
|
286
|
+
if (opts.platform === 'android') {
|
|
287
|
+
const deviceId = resolveAndroidDeviceId(opts.device);
|
|
288
|
+
const pulled = pullAndroidJsCoverage(deviceId, {
|
|
289
|
+
softFail: !config.strict,
|
|
290
|
+
outputDir: opts.output,
|
|
291
|
+
config,
|
|
292
|
+
});
|
|
293
|
+
if (!pulled && config.strict) {
|
|
294
|
+
process.exitCode = EXIT_STRICT_EMPTY;
|
|
295
|
+
}
|
|
296
|
+
} else if (opts.platform === 'ios') {
|
|
297
|
+
if (!opts.device) {
|
|
298
|
+
console.error(
|
|
299
|
+
'[rn-coverage] js pull --platform ios requires --device <udid>'
|
|
300
|
+
);
|
|
301
|
+
process.exitCode = EXIT_ERROR;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const pulled = pullIosJsCoverage(opts.device, {
|
|
305
|
+
softFail: !config.strict,
|
|
306
|
+
outputDir: opts.output,
|
|
307
|
+
config,
|
|
308
|
+
});
|
|
309
|
+
if (!pulled && config.strict) {
|
|
310
|
+
process.exitCode = EXIT_STRICT_EMPTY;
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
console.error(
|
|
314
|
+
`[rn-coverage] js pull: --platform must be android|ios (got ${opts.platform})`
|
|
315
|
+
);
|
|
316
|
+
process.exitCode = EXIT_ERROR;
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
320
|
+
process.exitCode = mapErrorToExit(error);
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
.addCommand(
|
|
325
|
+
new Command('report')
|
|
326
|
+
.description(
|
|
327
|
+
'NYC report from coverage-final.json (sourceMap remap → lcov.info)'
|
|
328
|
+
)
|
|
329
|
+
.requiredOption(
|
|
330
|
+
'--input <path>',
|
|
331
|
+
'coverage-final.json or directory of Istanbul JSON'
|
|
332
|
+
)
|
|
333
|
+
.option('--output <dir>', 'Report directory', 'coverage/js')
|
|
334
|
+
.option('--cwd <path>', 'NYC cwd for include globs', process.cwd())
|
|
335
|
+
.option('--nyc-config <path>', 'Path to nyc.config.js')
|
|
336
|
+
.action(async (opts, cmd) => {
|
|
337
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
338
|
+
const config = applyStrictOverride(
|
|
339
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
340
|
+
rootOpts
|
|
341
|
+
);
|
|
342
|
+
try {
|
|
343
|
+
const result = reportJsCoverage({
|
|
344
|
+
input: opts.input,
|
|
345
|
+
outputDir: opts.output,
|
|
346
|
+
cwd: opts.cwd,
|
|
347
|
+
nycConfig: opts.nycConfig,
|
|
348
|
+
softFail: !config.strict,
|
|
349
|
+
});
|
|
350
|
+
if (!result.ok && config.strict) {
|
|
351
|
+
process.exitCode = EXIT_STRICT_EMPTY;
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
355
|
+
process.exitCode = mapErrorToExit(error);
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
program
|
|
361
|
+
.command('assert')
|
|
362
|
+
.description(
|
|
363
|
+
'Post-pipeline presence check (exit 2 on empty/missing expected hits)'
|
|
364
|
+
)
|
|
365
|
+
.option('--platform <name>', 'ios | android | all (default: all)', 'all')
|
|
366
|
+
.option('--lcov <path>', 'LCOV file to assert non-empty expected hits')
|
|
367
|
+
.option('--jacoco-xml <path>', 'Jacoco XML to assert non-empty LINE hits')
|
|
368
|
+
.action(async (opts, cmd) => {
|
|
369
|
+
const rootOpts = rootOptsFrom(cmd);
|
|
370
|
+
const config = applyStrictOverride(
|
|
371
|
+
await loadCoverageConfig(process.cwd(), rootOpts.config),
|
|
372
|
+
rootOpts
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
if (!['ios', 'android', 'all'].includes(opts.platform)) {
|
|
376
|
+
console.error(
|
|
377
|
+
`[rn-coverage] assert: --platform must be ios|android|all (got ${opts.platform})`
|
|
378
|
+
);
|
|
379
|
+
process.exitCode = EXIT_ERROR;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const result = assertCoverage({
|
|
384
|
+
platform: opts.platform,
|
|
385
|
+
lcov: opts.lcov,
|
|
386
|
+
jacocoXml: opts.jacocoXml,
|
|
387
|
+
strict: config.strict,
|
|
388
|
+
config,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (result.code === EXIT_OK) {
|
|
392
|
+
for (const line of result.message.split('\n')) {
|
|
393
|
+
console.log(`[rn-coverage] assert: ${line}`);
|
|
394
|
+
}
|
|
395
|
+
console.log('[rn-coverage] assert: ok');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const line of result.message.split('\n')) {
|
|
400
|
+
if (config.strict) {
|
|
401
|
+
console.error(`[rn-coverage] assert: ${line}`);
|
|
402
|
+
} else {
|
|
403
|
+
console.warn(`[rn-coverage] assert: ${line}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (result.code === EXIT_STRICT_EMPTY) {
|
|
407
|
+
console.error(
|
|
408
|
+
'[rn-coverage] assert: FAIL empty or missing native coverage (exit 2)'
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
process.exitCode = result.code;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await program.parseAsync(process.argv);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch((error: unknown) => {
|
|
418
|
+
console.error(`[rn-coverage] ${(error as Error).message}`);
|
|
419
|
+
process.exitCode = EXIT_ERROR;
|
|
420
|
+
});
|