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,35 @@
|
|
|
1
|
+
package com.coverage
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class CoveragePackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(
|
|
12
|
+
name: String,
|
|
13
|
+
reactContext: ReactApplicationContext,
|
|
14
|
+
): NativeModule? =
|
|
15
|
+
if (name == CoverageModule.NAME) {
|
|
16
|
+
CoverageModule(reactContext)
|
|
17
|
+
} else {
|
|
18
|
+
null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun getReactModuleInfoProvider() =
|
|
22
|
+
ReactModuleInfoProvider {
|
|
23
|
+
mapOf(
|
|
24
|
+
CoverageModule.NAME to
|
|
25
|
+
ReactModuleInfo(
|
|
26
|
+
name = CoverageModule.NAME,
|
|
27
|
+
className = CoverageModule.NAME,
|
|
28
|
+
canOverrideExistingModule = false,
|
|
29
|
+
needsEagerInit = false,
|
|
30
|
+
isCxxModule = false,
|
|
31
|
+
isTurboModule = true,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry. Requires `yarn prepare` (bob build) so lib/ exists.
|
|
4
|
+
*/
|
|
5
|
+
import('../lib/module/cli/index.js').catch((error) => {
|
|
6
|
+
console.error(
|
|
7
|
+
'[rn-coverage] Failed to load CLI. Run `yarn prepare` first.\n',
|
|
8
|
+
error
|
|
9
|
+
);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# react-native-coverage CocoaPods helper (safe split).
|
|
4
|
+
#
|
|
5
|
+
# Expo / config plugin owns Android + stable iOS app-target mods.
|
|
6
|
+
# Pod LLVM coverage flags (and optional dynamic-framework restore) live here:
|
|
7
|
+
#
|
|
8
|
+
# require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install'
|
|
9
|
+
# # After use_expo_modules! — wrap Installer so restore runs after Expo's staticlib downgrade:
|
|
10
|
+
# ReactNativeCoverage.install_installer_hooks!
|
|
11
|
+
#
|
|
12
|
+
# post_install do |installer|
|
|
13
|
+
# ReactNativeCoverage.apply_post_install!(
|
|
14
|
+
# installer,
|
|
15
|
+
# framework_name_prefixes: ['CoverageFixture'],
|
|
16
|
+
# force_dynamic_frameworks: true
|
|
17
|
+
# )
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
require 'fileutils'
|
|
21
|
+
|
|
22
|
+
module ReactNativeCoverage
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
DEFAULT_FRAMEWORK_PREFIXES = ['CoverageFixture'].freeze
|
|
26
|
+
COVERAGE_POD_NAMES = %w[Coverage CoverageFixture].freeze
|
|
27
|
+
|
|
28
|
+
# Wrap Installer AFTER Expo's `use_expo_modules!` patch. Call from the Podfile
|
|
29
|
+
# below `use_expo_modules!` (Expo plugin injects the call).
|
|
30
|
+
# Using prepend-at-require races Expo's define_method and recurses forever.
|
|
31
|
+
def install_installer_hooks!(options = {})
|
|
32
|
+
if options.key?(:force_dynamic_frameworks)
|
|
33
|
+
self.force_dynamic_frameworks = options[:force_dynamic_frameworks]
|
|
34
|
+
end
|
|
35
|
+
return if @installer_hooks_installed
|
|
36
|
+
|
|
37
|
+
installer_class = Pod::Installer
|
|
38
|
+
original = installer_class.instance_method(:run_podfile_pre_install_hooks)
|
|
39
|
+
|
|
40
|
+
installer_class.define_method(:run_podfile_pre_install_hooks) do
|
|
41
|
+
original.bind_call(self)
|
|
42
|
+
ReactNativeCoverage.restore_dynamic_frameworks!(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@installer_hooks_installed = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def restore_dynamic_frameworks!(installer, pod_names: nil)
|
|
49
|
+
return unless force_dynamic_frameworks_enabled?
|
|
50
|
+
|
|
51
|
+
names = Array(pod_names || dynamic_framework_pod_names)
|
|
52
|
+
restored = []
|
|
53
|
+
|
|
54
|
+
installer.pod_targets.each do |target|
|
|
55
|
+
next unless names.any? { |name| target.name == name || target.name.include?(name) }
|
|
56
|
+
|
|
57
|
+
target.define_singleton_method(:build_type) do
|
|
58
|
+
Pod::BuildType.dynamic_framework
|
|
59
|
+
end
|
|
60
|
+
restored << target.name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return if restored.empty?
|
|
64
|
+
|
|
65
|
+
Pod::UI.puts '[react-native-coverage] Restored dynamic frameworks for: ' \
|
|
66
|
+
"#{restored.uniq.join(', ')}".green
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def force_dynamic_frameworks_enabled?
|
|
70
|
+
# Default false: Expo force-statics React-Core, so dynamic Coverage* pods
|
|
71
|
+
# fail CocoaPods transitive static→dynamic validation. Opt in on bare/RNFB.
|
|
72
|
+
@force_dynamic_frameworks == true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def force_dynamic_frameworks=(value)
|
|
76
|
+
@force_dynamic_frameworks = value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def dynamic_framework_pod_names
|
|
80
|
+
@dynamic_framework_pod_names || COVERAGE_POD_NAMES
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dynamic_framework_pod_names=(names)
|
|
84
|
+
@dynamic_framework_pod_names = Array(names)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apply_ios_native_coverage!(build_settings, link_profile:)
|
|
88
|
+
build_settings['CLANG_ENABLE_CODE_COVERAGE'] = 'YES'
|
|
89
|
+
build_settings['OTHER_CFLAGS'] =
|
|
90
|
+
'$(inherited) -fprofile-instr-generate -fcoverage-mapping'
|
|
91
|
+
build_settings['OTHER_SWIFT_FLAGS'] =
|
|
92
|
+
'$(inherited) -profile-generate -profile-coverage-mapping'
|
|
93
|
+
return unless link_profile
|
|
94
|
+
|
|
95
|
+
# -Wl,-u keeps __llvm_profile_set_filename from being dead-stripped so
|
|
96
|
+
# per-image flush can retarget coverage-%m.profraw under dynamic frameworks.
|
|
97
|
+
build_settings['OTHER_LDFLAGS'] = [
|
|
98
|
+
'$(inherited)',
|
|
99
|
+
'-fprofile-instr-generate',
|
|
100
|
+
'-Wl,-u,___llvm_profile_set_filename',
|
|
101
|
+
'-L$(DT_TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)',
|
|
102
|
+
'-L$(SDKROOT)/usr/lib/swift',
|
|
103
|
+
].join(' ')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_coverage_config_header!(prefixes:)
|
|
107
|
+
header_path = File.expand_path('../ios/CoverageConfig.h', __dir__)
|
|
108
|
+
list = Array(prefixes)
|
|
109
|
+
list = DEFAULT_FRAMEWORK_PREFIXES if list.empty?
|
|
110
|
+
|
|
111
|
+
entries = list.map { |prefix| " \"#{prefix}\"" }.join(",\n")
|
|
112
|
+
contents = <<~HEADER
|
|
113
|
+
/**
|
|
114
|
+
* Generated by react-native-coverage CocoaPods helper.
|
|
115
|
+
* Do not edit by hand — regenerate via apply_post_install! /
|
|
116
|
+
* Expo config plugin `frameworkNamePrefixes`.
|
|
117
|
+
*/
|
|
118
|
+
#pragma once
|
|
119
|
+
|
|
120
|
+
#ifndef COVERAGE_ENABLED
|
|
121
|
+
#define COVERAGE_ENABLED 1
|
|
122
|
+
#endif
|
|
123
|
+
|
|
124
|
+
#ifndef COVERAGE_PROFILE_FILE_PATTERN
|
|
125
|
+
#define COVERAGE_PROFILE_FILE_PATTERN "coverage-%m.profraw"
|
|
126
|
+
#endif
|
|
127
|
+
|
|
128
|
+
#ifndef COVERAGE_FRAMEWORK_PREFIX_COUNT
|
|
129
|
+
#define COVERAGE_FRAMEWORK_PREFIX_COUNT #{list.length}
|
|
130
|
+
static const char *const COVERAGE_FRAMEWORK_PREFIXES[] = {
|
|
131
|
+
#{entries}
|
|
132
|
+
};
|
|
133
|
+
#endif
|
|
134
|
+
HEADER
|
|
135
|
+
|
|
136
|
+
FileUtils.mkdir_p(File.dirname(header_path))
|
|
137
|
+
File.write(header_path, contents)
|
|
138
|
+
Pod::UI.puts "[react-native-coverage] Wrote #{header_path} " \
|
|
139
|
+
"(prefixes=#{list.inspect})".green
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_post_install!(installer, options = {})
|
|
143
|
+
prefixes = Array(options.fetch(:framework_name_prefixes, DEFAULT_FRAMEWORK_PREFIXES))
|
|
144
|
+
link_profile = options.fetch(:link_profile, true)
|
|
145
|
+
instrument_app = options.fetch(:instrument_app, true)
|
|
146
|
+
quiet = options.fetch(:quiet, false)
|
|
147
|
+
write_header = options.fetch(:write_config_header, true)
|
|
148
|
+
force_dynamic = options.fetch(:force_dynamic_frameworks, false)
|
|
149
|
+
|
|
150
|
+
self.force_dynamic_frameworks = force_dynamic
|
|
151
|
+
self.dynamic_framework_pod_names =
|
|
152
|
+
options.fetch(:dynamic_framework_pod_names, COVERAGE_POD_NAMES + prefixes)
|
|
153
|
+
|
|
154
|
+
# Belt-and-suspenders: re-apply if Expo/other hooks ran after our prepend.
|
|
155
|
+
restore_dynamic_frameworks!(installer) if force_dynamic
|
|
156
|
+
|
|
157
|
+
write_coverage_config_header!(prefixes: prefixes) if write_header
|
|
158
|
+
|
|
159
|
+
if instrument_app
|
|
160
|
+
installer.aggregate_targets.each do |aggregate_target|
|
|
161
|
+
user_project = aggregate_target.user_project
|
|
162
|
+
next if user_project.nil?
|
|
163
|
+
|
|
164
|
+
user_project.native_targets.each do |target|
|
|
165
|
+
target.build_configurations.each do |config|
|
|
166
|
+
apply_ios_native_coverage!(config.build_settings, link_profile: true)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
user_project.save
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
installer.pods_project.targets.each do |target|
|
|
174
|
+
matched = prefixes.any? { |prefix| target.name.include?(prefix) }
|
|
175
|
+
# Always link profile into the Coverage flusher Pod under dynamic linkage
|
|
176
|
+
# so configure/set_filename and the final pod write resolve.
|
|
177
|
+
is_coverage_pod = target.name == 'Coverage'
|
|
178
|
+
next unless matched || is_coverage_pod
|
|
179
|
+
|
|
180
|
+
target.build_configurations.each do |config|
|
|
181
|
+
apply_ios_native_coverage!(
|
|
182
|
+
config.build_settings,
|
|
183
|
+
link_profile: link_profile || is_coverage_pod
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
unless quiet
|
|
189
|
+
Pod::UI.puts '[react-native-coverage] applied LLVM coverage flags ' \
|
|
190
|
+
"(prefixes=#{prefixes.inspect}, force_dynamic=#{force_dynamic})"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Do not auto-install hooks on require — Expo patches Installer during
|
|
196
|
+
# `use_expo_modules!`. Call `ReactNativeCoverage.install_installer_hooks!`
|
|
197
|
+
# from the Podfile after that (Expo config plugin injects the call).
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
Binary: `rn-coverage`
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
rn-coverage --help
|
|
7
|
+
rn-coverage [--strict|--no-strict] [-c <config>]
|
|
8
|
+
|
|
9
|
+
rn-coverage android pull [--device <serial>] [--output <dir>] [--retries <n>]
|
|
10
|
+
rn-coverage android report [--android-dir <path>] [--jacoco-xml <path>]
|
|
11
|
+
|
|
12
|
+
rn-coverage ios pull --device <udid> [--output <dir>]
|
|
13
|
+
rn-coverage ios export --derived-data <path> [--configuration Debug] [--app-name <name>] [--output <path>]
|
|
14
|
+
rn-coverage ios report --derived-data <path> [--profdata <path>] [--output-dir <path>]
|
|
15
|
+
rn-coverage ios summary --derived-data <path> [--profdata <path>]
|
|
16
|
+
|
|
17
|
+
rn-coverage assert [--platform ios|android|all] [--lcov <path>] [--jacoco-xml <path>]
|
|
18
|
+
|
|
19
|
+
rn-coverage js pull --platform android|ios [--device <id>] [--output <dir>]
|
|
20
|
+
rn-coverage js report --input <coverage-final.json> [--output <dir>] [--cwd <path>] [--nyc-config <path>]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Exit codes
|
|
24
|
+
|
|
25
|
+
| Code | Meaning |
|
|
26
|
+
|------|---------|
|
|
27
|
+
| **0** | Success (or soft-mode empty artifact with warning) |
|
|
28
|
+
| **1** | Unexpected error / bad invocation / tooling failure |
|
|
29
|
+
| **2** | Strict empty-hit / missing artifact (CI presence guard) |
|
|
30
|
+
|
|
31
|
+
### Strict / assert contract
|
|
32
|
+
|
|
33
|
+
- Config default: `strict: true` (recommended for CI).
|
|
34
|
+
- Global flags: `--strict` / `--no-strict` override config for the process.
|
|
35
|
+
- Soft local (`--no-strict` or `strict: false`): missing/empty expected hits **warn and exit 0**.
|
|
36
|
+
- Strict CI: the same conditions **exit 2** so a sabotaged or silent pipeline fails the job.
|
|
37
|
+
|
|
38
|
+
Commands that enforce the guard:
|
|
39
|
+
|
|
40
|
+
| Command | Exit 2 when (strict) |
|
|
41
|
+
|---------|----------------------|
|
|
42
|
+
| `android pull` | No `.ec` after retries |
|
|
43
|
+
| `android report` | Gradle ok but Jacoco XML missing/empty/no matched LINE hits |
|
|
44
|
+
| `ios pull` | No `.profraw` in the simulator container |
|
|
45
|
+
| `ios export` | No `.profraw`, or LCOV has no expected path hits with `LH` > 0 (default: `packages/`) |
|
|
46
|
+
| `ios report` / `ios summary` | Missing `profdata` (run `ios export` first) |
|
|
47
|
+
| `assert` | Dedicated post-pipeline check for LCOV and/or Jacoco XML |
|
|
48
|
+
|
|
49
|
+
`rn-coverage assert` is the package-owned replacement for one-off shell presence scripts. Prefer wiring this CLI (exit 2) into consumer CI rather than maintaining a permanent bespoke assert.
|
|
50
|
+
|
|
51
|
+
Matchers and default artifact paths live under `assert.*` in config (see [config.md](./config.md)).
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Config
|
|
2
|
+
|
|
3
|
+
Copy [`react-native-coverage.config.js.example`](../react-native-coverage.config.js.example) to `react-native-coverage.config.js` in your dedicated test app.
|
|
4
|
+
|
|
5
|
+
Key fields:
|
|
6
|
+
|
|
7
|
+
| Key | Purpose |
|
|
8
|
+
|-----|---------|
|
|
9
|
+
| `nativeModuleName` | TurboModule name (default `Coverage`) |
|
|
10
|
+
| `app.androidApplicationId` | `run-as` package for adb pull |
|
|
11
|
+
| `app.iosBundleId` | simctl container lookup |
|
|
12
|
+
| `app.iosProductName` | App binary / `.app` name |
|
|
13
|
+
| `ios.frameworkNamePrefixes` | Extra llvm-cov `-object` frameworks |
|
|
14
|
+
| `android.coverageRelativePath` | On-device `.ec` path under app files |
|
|
15
|
+
| `android.libraryProjectMatchers` | Fallback Jacoco package substrings for assert |
|
|
16
|
+
| `android.jacocoReportXml` | Default Jacoco XML path after `android report` |
|
|
17
|
+
| `js.androidRelativePath` | On-device Istanbul JSON under `run-as` (default `files/coverage-final.json`) |
|
|
18
|
+
| `js.androidStagingPath` | adb staging path for JS JSON pull |
|
|
19
|
+
| `js.iosRelativePath` | Path under sim data container (default `Documents/coverage-final.json`) |
|
|
20
|
+
| `sourcePathRewrite` | LCOV `SF:` path normalization rules |
|
|
21
|
+
| `strict` | Exit **2** on empty artifacts when true (CI default) |
|
|
22
|
+
| `assert.lcovPathIncludes` | Substrings required in ≥1 LCOV `SF:` (default `packages/`) |
|
|
23
|
+
| `assert.jacocoPackageIncludes` | Jacoco package name substrings that must have LINE covered (`.` or `/`) |
|
|
24
|
+
| `assert.defaultLcovPath` | Default `--lcov` for `rn-coverage assert` |
|
|
25
|
+
| `assert.defaultJacocoXmlPath` | Default `--jacoco-xml` for `rn-coverage assert` |
|
|
26
|
+
|
|
27
|
+
Defaults contain **no** product-specific names.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# react-native-coverage
|
|
2
|
+
|
|
3
|
+
Coverage tooling for **dedicated React Native test apps** (Pattern C): native TurboModule flush (LLVM / Jacoco), Istanbul/NYC TypeScript coverage with source-map remap, pull/export CLI, Expo config plugin, and docs.
|
|
4
|
+
|
|
5
|
+
This package is **New Architecture / TurboModule only**. It does not depend on `expo-modules-core`.
|
|
6
|
+
|
|
7
|
+
## Quick links
|
|
8
|
+
|
|
9
|
+
- [Pattern C](pattern-c.md) — dedicated test apps only
|
|
10
|
+
- [Android integration](integration/android.md) — Gradle helpers + Expo plugin
|
|
11
|
+
- [iOS integration](integration/ios.md) — Ruby helper + dynamic frameworks
|
|
12
|
+
- [JS / TypeScript](integration/js.md) — Istanbul + NYC remap + Codecov
|
|
13
|
+
- [E2E timing](integration/e2e-timing.md)
|
|
14
|
+
- [CI Appium notes](integration/ci-appium.md) — GHA pitfalls (simulator, adb reverse, Jacoco paths)
|
|
15
|
+
- [CLI](cli.md)
|
|
16
|
+
- [Config](config.md)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Android integration
|
|
2
|
+
|
|
3
|
+
Enable Android test coverage in your dedicated test app (Pattern C) and wire Jacoco reporting.
|
|
4
|
+
|
|
5
|
+
## Expo (recommended)
|
|
6
|
+
|
|
7
|
+
Add the config plugin — it applies both Gradle helpers and enables debug `testCoverageEnabled`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"expo": {
|
|
12
|
+
"plugins": [
|
|
13
|
+
[
|
|
14
|
+
"react-native-coverage",
|
|
15
|
+
{
|
|
16
|
+
"libraryProjectMatchers": ["coverage-fixture", "my-native-lib"],
|
|
17
|
+
"enableAndroidCoverage": true
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx expo prebuild
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Inspect `android/build.gradle` (root instrumentation) and `android/app/build.gradle` (Jacoco report + `testCoverageEnabled`).
|
|
32
|
+
|
|
33
|
+
## Bare React Native / manual Gradle
|
|
34
|
+
|
|
35
|
+
In the **root** `android/build.gradle`:
|
|
36
|
+
|
|
37
|
+
```gradle
|
|
38
|
+
ext.coverageLibraryProjectMatchers = ['my-native-lib']
|
|
39
|
+
def rnCoverageRoot = new File(
|
|
40
|
+
["node", "--print", "require.resolve('react-native-coverage/package.json')"]
|
|
41
|
+
.execute(null, rootDir).text.trim()
|
|
42
|
+
).parentFile
|
|
43
|
+
apply from: new File(rnCoverageRoot, "android/rn-coverage.gradle")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
In **app** `android/app/build.gradle`:
|
|
47
|
+
|
|
48
|
+
```gradle
|
|
49
|
+
android {
|
|
50
|
+
buildTypes {
|
|
51
|
+
debug {
|
|
52
|
+
testCoverageEnabled true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def rnCoverageRoot = new File(
|
|
58
|
+
["node", "--print", "require.resolve('react-native-coverage/package.json')"]
|
|
59
|
+
.execute(null, rootDir).text.trim()
|
|
60
|
+
).parentFile
|
|
61
|
+
apply from: new File(rnCoverageRoot, "android/rn-coverage-jacoco.gradle")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Runtime + CLI
|
|
65
|
+
|
|
66
|
+
1. Call `flush()` from the TurboModule at the end of an e2e run (Emma `RT.dumpCoverageData` → `filesDir/coverage.ec`; also dumps Istanbul JSON when Metro is instrumented).
|
|
67
|
+
2. `rn-coverage android pull` — output under **`android/app/build/…`** so Jacoco sees the `.ec` (not only `artifacts/`). Staging parent dirs are created for you.
|
|
68
|
+
3. `rn-coverage android report` (`jacocoTestReport`).
|
|
69
|
+
4. `rn-coverage assert` (strict empty → exit 2).
|
|
70
|
+
5. Optional JS: `rn-coverage js pull` / `js report` — see [JS / TypeScript](js.md).
|
|
71
|
+
|
|
72
|
+
Jacoco package names in XML use slashes (`com/foo`); assert matchers may use dots (`com.foo`) — both work.
|
|
73
|
+
|
|
74
|
+
CI pitfalls (emulator Metro, `byTestId`, etc.): [CI Appium notes](ci-appium.md).
|
|
75
|
+
|
|
76
|
+
Shipped helpers:
|
|
77
|
+
|
|
78
|
+
| File | Role |
|
|
79
|
+
|------|------|
|
|
80
|
+
| `android/rn-coverage.gradle` | Enable Jacoco + `enableAndroidTestCoverage` on matched library projects |
|
|
81
|
+
| `android/rn-coverage-jacoco.gradle` | `jacocoTestReport` / unit / e2e-only report tasks |
|
|
82
|
+
|
|
83
|
+
Pod LLVM flags are **not** applied by regex-editing the Podfile; use the Ruby helper on iOS (see [iOS integration](ios.md)).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# CI Appium notes (learned the hard way)
|
|
2
|
+
|
|
3
|
+
Practical pitfalls from getting GitHub Actions green for this package’s Appium cells. Consumer CI should copy the same patterns.
|
|
4
|
+
|
|
5
|
+
## Matrix
|
|
6
|
+
|
|
7
|
+
| Cell | Harness | What it proves |
|
|
8
|
+
|------|---------|----------------|
|
|
9
|
+
| `e2e:ios:dynamic` | `example-dynamic/` (bare RN, `USE_FRAMEWORKS=dynamic`) | Primary LLVM LCOV with a real dynamic `CoverageFixture.framework` |
|
|
10
|
+
| `e2e:ios:static` | `example/` (Expo prebuild, static merge) | Honest static cell still asserts fixture hits |
|
|
11
|
+
| `e2e:android` | `example/` (Expo + emulator) | Emma `.ec` → Jacoco → assert |
|
|
12
|
+
|
|
13
|
+
Scripts: `scripts/ci/`. Specs: `e2e/`.
|
|
14
|
+
|
|
15
|
+
## iOS / Xcode / simulator
|
|
16
|
+
|
|
17
|
+
- Prefer **`macos-26`** + **`maxim-lobanov/setup-xcode`** with `latest-stable` (Expo SDK 57 wants Xcode 26.4+).
|
|
18
|
+
- Default simulator name: **`iPhone 17`**. Xcode 26.6 images do not ship a plain `iPhone 16`; wrong names fail destination lookup.
|
|
19
|
+
- Boot with an exact-name match, open **Simulator.app**, then poll `bootstatus` (see `scripts/ci/boot-ios-simulator.sh`). Headless `simctl boot` alone + Appium restart hung at WDA timeouts on GHA.
|
|
20
|
+
- Give Appium room: `simulatorStartupTimeout` ≥ 300s, long WDA timeouts (helpers in `e2e/helpers.js`).
|
|
21
|
+
- List sims once before the run (`xcrun simctl list`) — same deflake idea as RNFB.
|
|
22
|
+
|
|
23
|
+
## Android / Metro / Appium
|
|
24
|
+
|
|
25
|
+
- Debug APK loads JS from Metro. Emulator `localhost` is **not** the host — run **`adb reverse tcp:8081 tcp:8081`** (or your Metro port) before launching the app, or the bundle never loads and Appium never sees UI.
|
|
26
|
+
- React Native `testID` on Android maps to **`resource-id`**, not accessibility id. Prefer a shared helper (`byTestId`) that uses `UiSelector().resourceId(...)` on Android and `~id` on iOS.
|
|
27
|
+
- Allow a long `appWaitDuration` for first Metro bundle.
|
|
28
|
+
|
|
29
|
+
## Android coverage pull → Jacoco
|
|
30
|
+
|
|
31
|
+
1. Staging path (`android.detoxStagingPath`, default `/data/local/tmp/coverage/coverage.ec`): **`mkdir -p` the parent** before `run-as … cat … > staging`. Unlike Detox’s `/data/local/tmp/detox/`, this package does not create that directory for you (`rn-coverage android pull` does).
|
|
32
|
+
2. Land `emulator_coverage.ec` under the **app `buildDir`** (e.g. `android/app/build/outputs/code_coverage/`). Jacoco `executionData` is a `fileTree` over `project.buildDir` (+ matched libraries). Pulling only into `artifacts/` leaves `:app:jacocoTestReport` **SKIPPED** with empty data.
|
|
33
|
+
3. Assert matchers: Jacoco XML package names use **slashes** (`com/coverage/fixture`). Config often uses Java **dots** (`coverage.fixture`). `rn-coverage assert` normalizes `/` ↔ `.` — keep matchers readable either way.
|
|
34
|
+
|
|
35
|
+
## Action pins
|
|
36
|
+
|
|
37
|
+
Pin third-party Actions by **full commit SHA** (and comment the release tag). Typo’d SHAs fail the job before any app code runs.
|
|
38
|
+
|
|
39
|
+
## Artifacts
|
|
40
|
+
|
|
41
|
+
Upload `artifacts/e2e/coverage/**` and `artifacts/e2e/logs/**` with `if: always()` so failed pulls/asserts remain diagnosable.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# E2E timing (document only)
|
|
2
|
+
|
|
3
|
+
Glue for when to flush and pull belongs in the consumer test runner (Jet, Detox, Mocha, Appium, etc.). This package does not ship a particular e2e framework.
|
|
4
|
+
|
|
5
|
+
Typical sequence:
|
|
6
|
+
|
|
7
|
+
1. Run instrumented e2e against the dedicated test app (`RN_COVERAGE_JS=1` for Istanbul).
|
|
8
|
+
2. Invoke `Coverage.flush()` (TurboModule) once at suite teardown — native buffers **and** `global.__coverage__` when present.
|
|
9
|
+
3. Host-side: `rn-coverage android pull` / `rn-coverage ios pull`.
|
|
10
|
+
4. `rn-coverage android report` or `rn-coverage ios export` (+ optional `ios report` / `ios summary`).
|
|
11
|
+
5. Optional: `rn-coverage js pull` + `rn-coverage js report` (NYC source-map remap).
|
|
12
|
+
6. Optional: `rn-coverage assert` (exit 2 on empty artifacts when `strict: true`).
|
|
13
|
+
|
|
14
|
+
The example app documents Appium as the intended e2e runner. CI wiring:
|
|
15
|
+
|
|
16
|
+
- `yarn e2e:ios:dynamic` — primary bare-RN dynamic frameworks cell
|
|
17
|
+
- `yarn e2e:ios:static` — Expo static cell
|
|
18
|
+
- `yarn e2e:android` — Expo Android Jacoco + JS cell
|
|
19
|
+
|
|
20
|
+
Scripts live under `scripts/ci/`; WDIO specs under `e2e/`. See also [CI Appium notes](ci-appium.md) and [JS / TypeScript](js.md).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# iOS integration
|
|
2
|
+
|
|
3
|
+
- New Architecture only; TurboModule autolinks via the podspec.
|
|
4
|
+
- Flusher packaging (dynamic frameworks): **mode (c)** — Pod LINKEDIT for
|
|
5
|
+
configured `frameworkNamePrefixes` **and** the main executable (see spike verdict).
|
|
6
|
+
|
|
7
|
+
## Expo (recommended)
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"expo": {
|
|
12
|
+
"plugins": [
|
|
13
|
+
[
|
|
14
|
+
"react-native-coverage",
|
|
15
|
+
{
|
|
16
|
+
"frameworkNamePrefixes": ["MyLib"],
|
|
17
|
+
"forceDynamicFrameworks": false
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Keep `forceDynamicFrameworks` **false** under Expo (React-Core is force-static). Set **true** only on bare RN / RNFB hosts that build React as dynamic frameworks.
|
|
26
|
+
|
|
27
|
+
## CI cells (honest matrix)
|
|
28
|
+
|
|
29
|
+
| Cell | App | Linkage | Gate |
|
|
30
|
+
|------|-----|---------|------|
|
|
31
|
+
| **iOS dynamic (primary)** | `example-dynamic/` bare RN | `use_frameworks! :linkage => :dynamic` | Non-zero fixture LCOV **and** `CoverageFixture.framework` is a dylib |
|
|
32
|
+
| **iOS static** | `example/` Expo | staticlib merge into app / `.debug.dylib` | Non-zero fixture LCOV |
|
|
33
|
+
| **Android** | `example/` Expo | Jacoco-instrumented libraries | Non-zero fixture package LINE hits |
|
|
34
|
+
|
|
35
|
+
Appium (WebDriverIO) drives the harness; see `e2e/` and `scripts/ci/run-ios-e2e-cell.sh`.
|
|
36
|
+
|
|
37
|
+
1. Sets `ios.useFrameworks=dynamic` in Podfile properties (needed for multi-image LINKEDIT on dynamic-React hosts).
|
|
38
|
+
2. Requires the shipped Ruby helper and calls `apply_post_install!` once (safe split — no Podfile regex for LLVM flags).
|
|
39
|
+
3. Optionally restores dynamic frameworks for `Coverage` + matched fixture pods
|
|
40
|
+
(`forceDynamicFrameworks: true`) — **only when React is also dynamic**
|
|
41
|
+
(bare RN / RNFB). Leave `false` under Expo (React-Core is force-static);
|
|
42
|
+
CocoaPods rejects dynamic Coverage* pods that transitively depend on static
|
|
43
|
+
React-Core. Under Expo, Coverage* remain static libraries merged into the app.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx expo prebuild
|
|
47
|
+
cd ios && pod install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Bare React Native / manual Podfile
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install'
|
|
54
|
+
|
|
55
|
+
# After `use_expo_modules!` (wraps Installer so restore runs after Expo's staticlib downgrade):
|
|
56
|
+
ReactNativeCoverage.install_installer_hooks!
|
|
57
|
+
|
|
58
|
+
post_install do |installer|
|
|
59
|
+
ReactNativeCoverage.apply_post_install!(
|
|
60
|
+
installer,
|
|
61
|
+
framework_name_prefixes: ['MyLib'],
|
|
62
|
+
force_dynamic_frameworks: true
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The helper:
|
|
68
|
+
|
|
69
|
+
- Applies LLVM `-fprofile-instr-generate` / `-fcoverage-mapping` (+ link flags) to the app target and matching pods.
|
|
70
|
+
- Regenerates `ios/CoverageConfig.h` from `framework_name_prefixes`.
|
|
71
|
+
- Restores `Pod::BuildType.dynamic_framework` for Coverage / fixture pods when Expo forced `static_library`.
|
|
72
|
+
|
|
73
|
+
## Runtime + CLI
|
|
74
|
+
|
|
75
|
+
Call `flush()` before `rn-coverage ios pull`, then `rn-coverage ios export` for LCOV.
|
|
76
|
+
|
|
77
|
+
The Expo config plugin wires the Podfile helper call; it does **not** replace the Ruby helper for Pod LLVM flags (safe split).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# JavaScript / TypeScript coverage
|
|
2
|
+
|
|
3
|
+
Complete the React Native coverage story alongside native LLVM/Jacoco:
|
|
4
|
+
|
|
5
|
+
| Layer | Tooling | Artifact |
|
|
6
|
+
|-------|---------|----------|
|
|
7
|
+
| Unit (package) | Jest `--coverage` | `coverage/unit/lcov.info` |
|
|
8
|
+
| E2e JS/TS | `babel-plugin-istanbul` + NYC source-map remap | `coverage/js/lcov.info` |
|
|
9
|
+
| Native iOS | llvm-cov | `lcov.info` |
|
|
10
|
+
| Native Android | Jacoco | `jacocoTestReport.xml` |
|
|
11
|
+
|
|
12
|
+
## Instrument Metro (e2e)
|
|
13
|
+
|
|
14
|
+
Set `RN_COVERAGE_JS=1` before starting Metro (CI scripts do this). Example babel configs load `babel-plugin-istanbul` only when that env is set.
|
|
15
|
+
|
|
16
|
+
`flush()` dumps `global.__coverage__` via TurboModule `dumpJsCoverage` **before** the native Emma/LLVM flush:
|
|
17
|
+
|
|
18
|
+
- Android → `filesDir/coverage-final.json` (`files/coverage-final.json` under `run-as`)
|
|
19
|
+
- iOS → `Documents/coverage-final.json`
|
|
20
|
+
|
|
21
|
+
## CLI
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
rn-coverage js pull --platform android --output coverage/js
|
|
25
|
+
rn-coverage js pull --platform ios --device <udid> --output coverage/js
|
|
26
|
+
|
|
27
|
+
rn-coverage js report \
|
|
28
|
+
--input coverage/js/coverage-final.json \
|
|
29
|
+
--output coverage/js \
|
|
30
|
+
--cwd example \
|
|
31
|
+
--nyc-config example/nyc.config.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
NYC is configured with `sourceMap: true` and `exclude-after-remap: true` so LCOV `SF:` paths point at **TypeScript** sources (not only the Metro-transformed JS line map). Ship a `nyc.config.js` next to the harness (see `example/nyc.config.js`).
|
|
35
|
+
|
|
36
|
+
## Codecov
|
|
37
|
+
|
|
38
|
+
CI uploads unit LCOV, e2e native LCOV/Jacoco, and e2e JS LCOV with distinct `flags` (`unit-js`, `e2e-ios-dynamic`, …). Wire `CODECOV_TOKEN` (or Codecov GitHub app OIDC) on the repo for private uploads; public repos may work tokenless depending on Codecov settings.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Pattern C — dedicated test apps only
|
|
2
|
+
|
|
3
|
+
`react-native-coverage` is intended for **dedicated test / e2e harness apps** (for example a monorepo `tests/` app), not production product `package.json` trees.
|
|
4
|
+
|
|
5
|
+
Autolinking still scans dependencies; do not rely on `devDependency` alone to keep the TurboModule out of a product app. Keep the package in the test app workspace only.
|