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.
Files changed (92) hide show
  1. package/Coverage.podspec +20 -0
  2. package/LICENSE +202 -0
  3. package/README.md +125 -0
  4. package/android/build.gradle +51 -0
  5. package/android/rn-coverage-jacoco.gradle +132 -0
  6. package/android/rn-coverage.gradle +68 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/coverage/CoverageModule.kt +53 -0
  9. package/android/src/main/java/com/coverage/CoveragePackage.kt +35 -0
  10. package/app.plugin.js +1 -0
  11. package/bin/rn-coverage.js +11 -0
  12. package/cocoapods/coverage_post_install.rb +197 -0
  13. package/docs/cli.md +51 -0
  14. package/docs/config.md +27 -0
  15. package/docs/index.md +16 -0
  16. package/docs/integration/android.md +83 -0
  17. package/docs/integration/ci-appium.md +41 -0
  18. package/docs/integration/e2e-timing.md +20 -0
  19. package/docs/integration/ios.md +77 -0
  20. package/docs/integration/js.md +38 -0
  21. package/docs/pattern-c.md +5 -0
  22. package/docs/releasing.md +88 -0
  23. package/docs.json +24 -0
  24. package/ios/Coverage.h +5 -0
  25. package/ios/Coverage.mm +47 -0
  26. package/ios/CoverageConfig.h +21 -0
  27. package/ios/CoverageProfile.h +22 -0
  28. package/ios/CoverageProfile.mm +358 -0
  29. package/lib/module/NativeCoverage.js +10 -0
  30. package/lib/module/NativeCoverage.js.map +1 -0
  31. package/lib/module/assert-coverage.js +181 -0
  32. package/lib/module/assert-coverage.js.map +1 -0
  33. package/lib/module/cli/index.js +229 -0
  34. package/lib/module/cli/index.js.map +1 -0
  35. package/lib/module/config.js +83 -0
  36. package/lib/module/config.js.map +1 -0
  37. package/lib/module/exit-codes.js +22 -0
  38. package/lib/module/exit-codes.js.map +1 -0
  39. package/lib/module/index.js +35 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/js-coverage.js +172 -0
  42. package/lib/module/js-coverage.js.map +1 -0
  43. package/lib/module/node.js +15 -0
  44. package/lib/module/node.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/path-rewrite.js +44 -0
  47. package/lib/module/path-rewrite.js.map +1 -0
  48. package/lib/module/process-ios-native-coverage.js +258 -0
  49. package/lib/module/process-ios-native-coverage.js.map +1 -0
  50. package/lib/module/pull-native-coverage.js +210 -0
  51. package/lib/module/pull-native-coverage.js.map +1 -0
  52. package/lib/typescript/example-dynamic/App.d.ts +6 -0
  53. package/lib/typescript/example-dynamic/App.d.ts.map +1 -0
  54. package/lib/typescript/package.json +1 -0
  55. package/lib/typescript/src/NativeCoverage.d.ts +16 -0
  56. package/lib/typescript/src/NativeCoverage.d.ts.map +1 -0
  57. package/lib/typescript/src/assert-coverage.d.ts +53 -0
  58. package/lib/typescript/src/assert-coverage.d.ts.map +1 -0
  59. package/lib/typescript/src/cli/index.d.ts +4 -0
  60. package/lib/typescript/src/cli/index.d.ts.map +1 -0
  61. package/lib/typescript/src/config.d.ts +93 -0
  62. package/lib/typescript/src/config.d.ts.map +1 -0
  63. package/lib/typescript/src/exit-codes.d.ts +16 -0
  64. package/lib/typescript/src/exit-codes.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +16 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/js-coverage.d.ts +36 -0
  68. package/lib/typescript/src/js-coverage.d.ts.map +1 -0
  69. package/lib/typescript/src/node.d.ts +14 -0
  70. package/lib/typescript/src/node.d.ts.map +1 -0
  71. package/lib/typescript/src/path-rewrite.d.ts +12 -0
  72. package/lib/typescript/src/path-rewrite.d.ts.map +1 -0
  73. package/lib/typescript/src/process-ios-native-coverage.d.ts +61 -0
  74. package/lib/typescript/src/process-ios-native-coverage.d.ts.map +1 -0
  75. package/lib/typescript/src/pull-native-coverage.d.ts +38 -0
  76. package/lib/typescript/src/pull-native-coverage.d.ts.map +1 -0
  77. package/package.json +184 -0
  78. package/plugin/build/index.d.ts +21 -0
  79. package/plugin/build/index.js +153 -0
  80. package/react-native-coverage.config.js.example +47 -0
  81. package/react-native.config.js +9 -0
  82. package/src/NativeCoverage.ts +16 -0
  83. package/src/assert-coverage.ts +260 -0
  84. package/src/cli/index.ts +420 -0
  85. package/src/config.ts +189 -0
  86. package/src/exit-codes.ts +20 -0
  87. package/src/index.tsx +35 -0
  88. package/src/js-coverage.ts +213 -0
  89. package/src/node.ts +52 -0
  90. package/src/path-rewrite.ts +52 -0
  91. package/src/process-ios-native-coverage.ts +425 -0
  92. package/src/pull-native-coverage.ts +269 -0
@@ -0,0 +1,88 @@
1
+ # Releasing
2
+
3
+ Operator guide for publishing `react-native-coverage`. Releases are **manual only** (`workflow_dispatch`). There is no auto-publish on push to `main`.
4
+
5
+ ## Prerequisites
6
+
7
+ | Item | Notes |
8
+ |------|--------|
9
+ | GitHub repo | `invertase/react-native-coverage` (see [Create the GitHub repo](#create-the-github-repo) if missing) |
10
+ | Conventional Commits | PR titles enforced by `.github/workflows/pr-title.yml`; squash-merge preferred |
11
+ | Green `main` | Unit + Appium e2e CI from `.github/workflows/ci.yml` |
12
+ | First npm publish | **Human OTP bootstrap** — first human publish, not this release scaffolding |
13
+ | npm OIDC | Configure Trusted Publisher **after** bootstrap; release workflow already has `id-token: write` |
14
+
15
+ ## Create the GitHub repo
16
+
17
+ Local clone has **no `origin` remote** until a human creates the GitHub repository. From the package root:
18
+
19
+ ```sh
20
+ cd /path/to/react-native-coverage
21
+
22
+ # Create under invertase and set origin (public OSS / Apache-2.0):
23
+ gh repo create invertase/react-native-coverage \
24
+ --public \
25
+ --source=. \
26
+ --remote=origin \
27
+ --description "Native code coverage tooling for React Native test apps (Pattern C)"
28
+
29
+ # Push main (after reviewing uncommitted scaffolding / approved commits):
30
+ git push -u origin main
31
+ ```
32
+
33
+ Optional: create empty on GitHub first, then:
34
+
35
+ ```sh
36
+ git remote add origin git@github.com:invertase/react-native-coverage.git
37
+ git push -u origin main
38
+ ```
39
+
40
+ Confirm: `gh repo view invertase/react-native-coverage`.
41
+
42
+ ## Conventional Commits
43
+
44
+ - Prefer **squash merge**; the PR title becomes the release-relevant commit.
45
+ - PR titles must match [Conventional Commits](https://www.conventionalcommits.org/) (enforced in CI).
46
+ - Local check (optional):
47
+
48
+ ```sh
49
+ echo "feat: add assert command" | yarn commitlint
50
+ ```
51
+
52
+ Examples: `feat: …`, `fix: …`, `docs: …`, `chore: …`. Breaking changes: `feat!: …` or a `BREAKING CHANGE:` footer.
53
+
54
+ ## Release tooling
55
+
56
+ | Piece | Path / command |
57
+ |-------|----------------|
58
+ | Config | `release.config.js` |
59
+ | Workflow | `.github/workflows/release.yml` (`workflow_dispatch` only) |
60
+ | Analyzer | Conventional Commits via `@semantic-release/commit-analyzer` |
61
+ | Publish | `@semantic-release/npm` + `@semantic-release/github` |
62
+ | **Not used** | `@semantic-release/git` — dropped so releases do not need a bot push past branch protection; version tags + GitHub Releases carry the release, not in-repo `package.json` bumps |
63
+
64
+ Dry-run (no publish; needs network + git history):
65
+
66
+ ```sh
67
+ yarn release:dry-run
68
+ ```
69
+
70
+ ## Operator: CI release (after bootstrap)
71
+
72
+ Do **not** run a real publish until human bootstrap + OIDC Trusted Publisher setup are complete.
73
+
74
+ 1. Ensure `main` is green.
75
+ 2. GitHub → **Actions** → **Release** → **Run workflow** (branch `main`).
76
+ 3. Confirm the run created a GitHub Release + npm version with provenance.
77
+ 4. If nothing published: semantic-release found no releasable commits since the last tag (expected when only `chore`/`docs` landed).
78
+
79
+ ## First human publish and OIDC setup (do not do via this workflow alone)
80
+
81
+ - First human `npm publish` (OTP), including bootstrap version / dist-tag so tags do not collide with later semantic-release versions
82
+ - npm package settings → **Trusted Publisher** for `invertase/react-native-coverage` workflow `.github/workflows/release.yml`
83
+ - Second publish via this Release workflow to prove OIDC + provenance
84
+
85
+ ## Rollback notes
86
+
87
+ - npm: deprecate or unpublish only within npm policy; prefer a follow-up patch release.
88
+ - Git: delete a mistaken `v*` tag only if it was never consumed; coordinate with maintainers.
package/docs.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "react-native-coverage",
3
+ "description": "Native + JS/TS coverage tooling for React Native test apps",
4
+ "sidebar": [
5
+ {
6
+ "group": "Getting Started",
7
+ "pages": ["index", "pattern-c"]
8
+ },
9
+ {
10
+ "group": "Integration",
11
+ "pages": [
12
+ "integration/android",
13
+ "integration/ios",
14
+ "integration/js",
15
+ "integration/e2e-timing",
16
+ "integration/ci-appium"
17
+ ]
18
+ },
19
+ {
20
+ "group": "Reference",
21
+ "pages": ["cli", "config", "releasing"]
22
+ }
23
+ ]
24
+ }
package/ios/Coverage.h ADDED
@@ -0,0 +1,5 @@
1
+ #import <CoverageSpec/CoverageSpec.h>
2
+
3
+ @interface Coverage : NSObject <NativeCoverageSpec>
4
+
5
+ @end
@@ -0,0 +1,47 @@
1
+ #import "Coverage.h"
2
+ #import "CoverageProfile.h"
3
+
4
+ @implementation Coverage
5
+
6
+ - (void)flush
7
+ {
8
+ int status = CoverageFlushProfile();
9
+ if (status != 0) {
10
+ NSLog(@"[ios-native-coverage] TurboModule flush returned status=%d", status);
11
+ }
12
+ }
13
+
14
+ - (void)dumpJsCoverage:(NSString *)json
15
+ {
16
+ NSArray<NSString *> *paths =
17
+ NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
18
+ NSString *docs = paths.firstObject;
19
+ if (docs == nil) {
20
+ NSLog(@"[ios-native-coverage] dumpJsCoverage: Documents directory missing");
21
+ return;
22
+ }
23
+ NSString *path = [docs stringByAppendingPathComponent:@"coverage-final.json"];
24
+ NSError *error = nil;
25
+ BOOL ok = [json writeToFile:path
26
+ atomically:YES
27
+ encoding:NSUTF8StringEncoding
28
+ error:&error];
29
+ if (!ok) {
30
+ NSLog(@"[ios-native-coverage] dumpJsCoverage failed: %@", error);
31
+ } else {
32
+ NSLog(@"[ios-native-coverage] wrote JS coverage to %@", path);
33
+ }
34
+ }
35
+
36
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
37
+ (const facebook::react::ObjCTurboModule::InitParams &)params
38
+ {
39
+ return std::make_shared<facebook::react::NativeCoverageSpecJSI>(params);
40
+ }
41
+
42
+ + (NSString *)moduleName
43
+ {
44
+ return @"Coverage";
45
+ }
46
+
47
+ @end
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Generated by react-native-coverage CocoaPods helper.
3
+ * Do not edit by hand — regenerate via apply_post_install! /
4
+ * Expo config plugin `frameworkNamePrefixes`.
5
+ */
6
+ #pragma once
7
+
8
+ #ifndef COVERAGE_ENABLED
9
+ #define COVERAGE_ENABLED 1
10
+ #endif
11
+
12
+ #ifndef COVERAGE_PROFILE_FILE_PATTERN
13
+ #define COVERAGE_PROFILE_FILE_PATTERN "coverage-%m.profraw"
14
+ #endif
15
+
16
+ #ifndef COVERAGE_FRAMEWORK_PREFIX_COUNT
17
+ #define COVERAGE_FRAMEWORK_PREFIX_COUNT 1
18
+ static const char *const COVERAGE_FRAMEWORK_PREFIXES[] = {
19
+ "CoverageFixture"
20
+ };
21
+ #endif
@@ -0,0 +1,22 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ #ifdef __cplusplus
4
+ extern "C" {
5
+ #endif
6
+
7
+ /** Set LLVM_PROFILE_FILE / profile filename to Documents/coverage-%m.profraw. */
8
+ void CoverageConfigureProfilePath(void);
9
+
10
+ /**
11
+ * Mode (c) multi-image flush:
12
+ * - LINKEDIT dump for configured framework prefixes (e.g. CoverageFixture*)
13
+ * - LINKEDIT dump for MH_EXECUTE main
14
+ * - direct write for this Pod's own profile runtime last
15
+ *
16
+ * Returns 0 on success (at least one useful write), non-zero otherwise.
17
+ */
18
+ int CoverageFlushProfile(void);
19
+
20
+ #ifdef __cplusplus
21
+ }
22
+ #endif
@@ -0,0 +1,358 @@
1
+ #import "CoverageProfile.h"
2
+ #import "CoverageConfig.h"
3
+
4
+ #import <mach-o/dyld.h>
5
+ #import <mach-o/loader.h>
6
+ #import <mach-o/nlist.h>
7
+ #import <stdlib.h>
8
+ #import <string.h>
9
+
10
+ extern "C" {
11
+ int __llvm_profile_write_file(void);
12
+ void __llvm_profile_set_filename(const char *Name);
13
+ const char *__llvm_profile_get_filename(void);
14
+ }
15
+
16
+ typedef int (*CoverageProfileWriteFn)(void);
17
+ typedef void (*CoverageProfileSetFilenameFn)(const char *);
18
+
19
+ typedef struct {
20
+ const struct mach_header *header;
21
+ intptr_t slide;
22
+ CoverageProfileWriteFn writeFile;
23
+ const char *imageName;
24
+ } CoverageTrackedProfileImage;
25
+
26
+ enum { CoverageMaxTrackedProfileImages = 64 };
27
+
28
+ static CoverageTrackedProfileImage gCoverageTracked[CoverageMaxTrackedProfileImages];
29
+ static uint32_t gCoverageTrackedCount = 0;
30
+
31
+ /**
32
+ * `%m` expands to a unique module signature so each instrumented Mach-O image
33
+ * writes its own profraw instead of overwriting a single shared file.
34
+ */
35
+ static NSString *CoverageProfilePattern(void)
36
+ {
37
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
38
+ return [[paths firstObject] stringByAppendingPathComponent:@COVERAGE_PROFILE_FILE_PATTERN];
39
+ }
40
+
41
+ /**
42
+ * clang_rt.profile symbols are linked into each instrumented framework as local
43
+ * (`t`) symbols — dlsym cannot see them. Resolve via the on-disk symbol table
44
+ * mapped through __LINKEDIT.
45
+ */
46
+ static const void *CoverageFindSymbolInHeader(
47
+ const struct mach_header *header, intptr_t slide, const char *symbolName)
48
+ {
49
+ if (header == NULL || symbolName == NULL) {
50
+ return NULL;
51
+ }
52
+ if (header->magic != MH_MAGIC_64 && header->magic != MH_CIGAM_64) {
53
+ return NULL;
54
+ }
55
+
56
+ const struct mach_header_64 *header64 = (const struct mach_header_64 *)header;
57
+ const uint8_t *cursor = (const uint8_t *)(header64 + 1);
58
+ const struct symtab_command *symtab = NULL;
59
+ const struct segment_command_64 *linkedit = NULL;
60
+
61
+ for (uint32_t i = 0; i < header64->ncmds; i++) {
62
+ const struct load_command *command = (const struct load_command *)cursor;
63
+ if (command->cmd == LC_SYMTAB) {
64
+ symtab = (const struct symtab_command *)command;
65
+ } else if (command->cmd == LC_SEGMENT_64) {
66
+ const struct segment_command_64 *segment = (const struct segment_command_64 *)command;
67
+ if (strcmp(segment->segname, SEG_LINKEDIT) == 0) {
68
+ linkedit = segment;
69
+ }
70
+ }
71
+ cursor += command->cmdsize;
72
+ }
73
+
74
+ if (symtab == NULL || linkedit == NULL || symtab->nsyms == 0) {
75
+ return NULL;
76
+ }
77
+
78
+ const uintptr_t linkeditBase =
79
+ (uintptr_t)slide + (uintptr_t)linkedit->vmaddr - (uintptr_t)linkedit->fileoff;
80
+ const char *strings = (const char *)(linkeditBase + symtab->stroff);
81
+ const struct nlist_64 *symbols =
82
+ (const struct nlist_64 *)(linkeditBase + symtab->symoff);
83
+ const size_t nameLength = strlen(symbolName);
84
+
85
+ for (uint32_t i = 0; i < symtab->nsyms; i++) {
86
+ uint32_t stringIndex = symbols[i].n_un.n_strx;
87
+ if (stringIndex == 0) {
88
+ continue;
89
+ }
90
+ const char *name = strings + stringIndex;
91
+ if (strncmp(name, symbolName, nameLength) != 0 || name[nameLength] != '\0') {
92
+ continue;
93
+ }
94
+ if ((symbols[i].n_type & N_TYPE) == N_UNDF || symbols[i].n_value == 0) {
95
+ continue;
96
+ }
97
+ return (const void *)((uintptr_t)symbols[i].n_value + (uintptr_t)slide);
98
+ }
99
+
100
+ return NULL;
101
+ }
102
+
103
+ static const char *CoverageImageNameForHeader(const struct mach_header *header)
104
+ {
105
+ uint32_t imageCount = _dyld_image_count();
106
+ for (uint32_t index = 0; index < imageCount; index += 1) {
107
+ if (_dyld_get_image_header(index) == header) {
108
+ return _dyld_get_image_name(index);
109
+ }
110
+ }
111
+ return NULL;
112
+ }
113
+
114
+ static bool CoverageIsConfiguredFrameworkImage(const char *imageName)
115
+ {
116
+ if (imageName == NULL) {
117
+ return false;
118
+ }
119
+ #if !COVERAGE_ENABLED
120
+ return false;
121
+ #endif
122
+ if (COVERAGE_FRAMEWORK_PREFIX_COUNT <= 0) {
123
+ return false;
124
+ }
125
+ const char *framework = strstr(imageName, ".framework");
126
+ if (framework == NULL) {
127
+ return false;
128
+ }
129
+ const char *basename = framework;
130
+ while (basename > imageName && basename[-1] != '/') {
131
+ basename -= 1;
132
+ }
133
+ for (int i = 0; i < COVERAGE_FRAMEWORK_PREFIX_COUNT; i++) {
134
+ const char *prefix = COVERAGE_FRAMEWORK_PREFIXES[i];
135
+ if (prefix == NULL) {
136
+ continue;
137
+ }
138
+ size_t prefixLen = strlen(prefix);
139
+ if (prefixLen > 0 && strncmp(basename, prefix, prefixLen) == 0) {
140
+ return true;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * React Native / Expo Debug builds often place app + static-pod object code in
148
+ * `AppName.debug.dylib` (MH_DYLIB), not MH_EXECUTE. Flush that companion image
149
+ * via LINKEDIT so fixture/static-lib counters are not missed when Expo forces
150
+ * MACH_O_TYPE=staticlib on pods under use_frameworks.
151
+ */
152
+ static bool CoverageIsAppCompanionDebugDylib(const char *imageName)
153
+ {
154
+ if (imageName == NULL) {
155
+ return false;
156
+ }
157
+ if (strstr(imageName, ".app/") == NULL) {
158
+ return false;
159
+ }
160
+ return strstr(imageName, ".debug.dylib") != NULL;
161
+ }
162
+
163
+ static bool CoverageShouldTrackImage(const char *imageName)
164
+ {
165
+ return CoverageIsConfiguredFrameworkImage(imageName) ||
166
+ CoverageIsAppCompanionDebugDylib(imageName);
167
+ }
168
+
169
+ static bool CoverageIsMainExecutable(const struct mach_header *header)
170
+ {
171
+ return header != NULL && header->filetype == MH_EXECUTE;
172
+ }
173
+
174
+ static void CoverageSetFilenameOnImage(
175
+ const struct mach_header *header, intptr_t slide, const char *pattern)
176
+ {
177
+ CoverageProfileSetFilenameFn setFilename =
178
+ (CoverageProfileSetFilenameFn)CoverageFindSymbolInHeader(
179
+ header, slide, "___llvm_profile_set_filename");
180
+ if (setFilename != NULL && pattern != NULL) {
181
+ setFilename(pattern);
182
+ return;
183
+ }
184
+ char **filenameVar = (char **)CoverageFindSymbolInHeader(
185
+ header, slide, "___llvm_profile_filename");
186
+ if (filenameVar != NULL && pattern != NULL) {
187
+ *filenameVar = (char *)pattern;
188
+ }
189
+ }
190
+
191
+ static void CoverageTrackProfileImage(const struct mach_header *header, intptr_t slide)
192
+ {
193
+ const char *imageName = CoverageImageNameForHeader(header);
194
+ if (!CoverageShouldTrackImage(imageName)) {
195
+ return;
196
+ }
197
+
198
+ CoverageProfileWriteFn writeFile = (CoverageProfileWriteFn)CoverageFindSymbolInHeader(
199
+ header, slide, "___llvm_profile_write_file");
200
+ // Skip if missing or if it resolves to *this* Pod's runtime (not a distinct image).
201
+ if (writeFile == NULL || writeFile == __llvm_profile_write_file) {
202
+ NSLog(@"[ios-native-coverage] skip image (no distinct profile runtime): %s", imageName);
203
+ return;
204
+ }
205
+
206
+ for (uint32_t i = 0; i < gCoverageTrackedCount; i++) {
207
+ if (gCoverageTracked[i].writeFile == writeFile) {
208
+ return;
209
+ }
210
+ }
211
+
212
+ if (gCoverageTrackedCount >= CoverageMaxTrackedProfileImages) {
213
+ NSLog(@"[ios-native-coverage] tracked profile image cap reached (%d)",
214
+ CoverageMaxTrackedProfileImages);
215
+ return;
216
+ }
217
+
218
+ gCoverageTracked[gCoverageTrackedCount].header = header;
219
+ gCoverageTracked[gCoverageTrackedCount].slide = slide;
220
+ gCoverageTracked[gCoverageTrackedCount].writeFile = writeFile;
221
+ gCoverageTracked[gCoverageTrackedCount].imageName = imageName;
222
+ gCoverageTrackedCount += 1;
223
+ NSLog(@"[ios-native-coverage] tracked profile image[%u]: %s",
224
+ gCoverageTrackedCount - 1,
225
+ imageName);
226
+ }
227
+
228
+ static void CoverageOnAddImage(const struct mach_header *header, intptr_t slide)
229
+ {
230
+ CoverageTrackProfileImage(header, slide);
231
+ }
232
+
233
+ __attribute__((constructor)) static void CoverageProfileInit(void)
234
+ {
235
+ _dyld_register_func_for_add_image(CoverageOnAddImage);
236
+ }
237
+
238
+ static void CoverageRefreshTrackedFromDyld(void)
239
+ {
240
+ uint32_t imageCount = _dyld_image_count();
241
+ for (uint32_t index = 0; index < imageCount; index += 1) {
242
+ CoverageTrackProfileImage(
243
+ _dyld_get_image_header(index), _dyld_get_image_vmaddr_slide(index));
244
+ }
245
+ }
246
+
247
+ static int CoverageFlushTracked(const char *pattern)
248
+ {
249
+ int worstStatus = 0;
250
+ int wrote = 0;
251
+
252
+ for (uint32_t i = 0; i < gCoverageTrackedCount; i++) {
253
+ CoverageSetFilenameOnImage(
254
+ gCoverageTracked[i].header, gCoverageTracked[i].slide, pattern);
255
+ int status = gCoverageTracked[i].writeFile();
256
+ NSLog(
257
+ @"[ios-native-coverage] flush tracked[%u] status=%d image=%s",
258
+ i,
259
+ status,
260
+ gCoverageTracked[i].imageName ?: "(null)");
261
+ if (status != 0) {
262
+ worstStatus = status;
263
+ } else {
264
+ wrote += 1;
265
+ }
266
+ }
267
+
268
+ return wrote > 0 ? 0 : worstStatus;
269
+ }
270
+
271
+ static int CoverageFlushMainViaLinkedit(const char *pattern)
272
+ {
273
+ uint32_t imageCount = _dyld_image_count();
274
+ for (uint32_t index = 0; index < imageCount; index += 1) {
275
+ const struct mach_header *header = _dyld_get_image_header(index);
276
+ intptr_t slide = _dyld_get_image_vmaddr_slide(index);
277
+ if (!CoverageIsMainExecutable(header)) {
278
+ continue;
279
+ }
280
+
281
+ const char *imageName = CoverageImageNameForHeader(header);
282
+ CoverageProfileWriteFn writeFile = (CoverageProfileWriteFn)CoverageFindSymbolInHeader(
283
+ header, slide, "___llvm_profile_write_file");
284
+ NSLog(
285
+ @"[ios-native-coverage] main-exe image=%s writeFile=%p podWriteFile=%p same=%d",
286
+ imageName ?: "(null)",
287
+ (void *)writeFile,
288
+ (void *)__llvm_profile_write_file,
289
+ writeFile == __llvm_profile_write_file ? 1 : 0);
290
+
291
+ if (writeFile == NULL) {
292
+ NSLog(@"[ios-native-coverage] main executable has no ___llvm_profile_write_file via LINKEDIT");
293
+ return -1;
294
+ }
295
+
296
+ CoverageSetFilenameOnImage(header, slide, pattern);
297
+ int status = writeFile();
298
+ NSLog(@"[ios-native-coverage] flush main-via-LINKEDIT status=%d", status);
299
+ return status;
300
+ }
301
+
302
+ NSLog(@"[ios-native-coverage] no MH_EXECUTE image found");
303
+ return -1;
304
+ }
305
+
306
+ extern "C" void CoverageConfigureProfilePath(void)
307
+ {
308
+ const char *pattern = CoverageProfilePattern().UTF8String;
309
+ // Dynamic frameworks often lack set_filename (dead-stripped). getenv is consulted
310
+ // by every runtime copy on write.
311
+ setenv("LLVM_PROFILE_FILE", pattern, 1);
312
+ __llvm_profile_set_filename(pattern);
313
+ NSLog(
314
+ @"[ios-native-coverage] configure pattern=%@ podRuntimePath=%s",
315
+ CoverageProfilePattern(),
316
+ __llvm_profile_get_filename() ?: "(null)");
317
+ }
318
+
319
+ /**
320
+ * Mode (c): Pod LINKEDIT for configured frameworks + main executable, then pod write.
321
+ * Prefer this under use_frameworks! :linkage => :dynamic (05S1 verdict).
322
+ */
323
+ extern "C" int CoverageFlushProfile(void)
324
+ {
325
+ #if !COVERAGE_ENABLED
326
+ NSLog(@"[ios-native-coverage] flush skipped (COVERAGE_ENABLED=0)");
327
+ return -1;
328
+ #endif
329
+
330
+ CoverageConfigureProfilePath();
331
+ CoverageRefreshTrackedFromDyld();
332
+ const char *pattern = getenv("LLVM_PROFILE_FILE");
333
+
334
+ int trackedStatus = CoverageFlushTracked(pattern);
335
+ int mainStatus = CoverageFlushMainViaLinkedit(pattern);
336
+ int podStatus = __llvm_profile_write_file();
337
+
338
+ NSLog(
339
+ @"[ios-native-coverage] flush complete tracked=%u trackedStatus=%d mainStatus=%d podStatus=%d pattern=%s",
340
+ gCoverageTrackedCount,
341
+ trackedStatus,
342
+ mainStatus,
343
+ podStatus,
344
+ pattern ?: "(null)");
345
+
346
+ int okTracked = (trackedStatus == 0 && gCoverageTrackedCount > 0) ? 1 : 0;
347
+ int okMain = (mainStatus == 0) ? 1 : 0;
348
+ // Under Expo static-pod merge, fixture counters live in the same image as this
349
+ // Pod's profile runtime — pod write is the success path.
350
+ int okPod = (podStatus == 0) ? 1 : 0;
351
+ if (okTracked || okMain || okPod) {
352
+ return 0;
353
+ }
354
+ if (mainStatus != 0) {
355
+ return mainStatus;
356
+ }
357
+ return trackedStatus != 0 ? trackedStatus : podStatus;
358
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+
5
+ /**
6
+ * TurboModule surface for native coverage flush (New Architecture only).
7
+ */
8
+
9
+ export default TurboModuleRegistry.getEnforcing('Coverage');
10
+ //# sourceMappingURL=NativeCoverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeCoverage.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;;AAEpE;AACA;AACA;;AAWA,eAAeA,mBAAmB,CAACC,YAAY,CAAO,UAAU,CAAC","ignoreList":[]}