-
Notifications
You must be signed in to change notification settings - Fork 12
RFC: Native iOS code coverage support #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
mfazekas
wants to merge
5
commits into
callstackincubator:main
Choose a base branch
from
mfazekas:feat/native-ios-coverage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8b3acde
feat: add native iOS code coverage support
mfazekas 4035c23
fix: address PR review comments for iOS coverage
mfazekas c709d70
fix: address integration issues found during rive-nitro testing
mfazekas f15a893
fix: write profraw to /tmp/harness-coverage/ to survive app reinstalls
mfazekas 15fc68e
refactor: use getConfig() for resolve-coverage-pods
mfazekas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| require "json" | ||
|
|
||
| package = JSON.parse(File.read(File.join(__dir__, "package.json"))) | ||
|
|
||
| if defined?(Pod::Installer) | ||
| require_relative 'scripts/harness_coverage_hook' | ||
| end | ||
|
|
||
| Pod::Spec.new do |s| | ||
| s.name = "HarnessCoverage" | ||
| s.version = package["version"] | ||
| s.summary = package["description"] | ||
| s.homepage = package["homepage"] | ||
| s.license = package["license"] | ||
| s.authors = package["author"] | ||
|
|
||
| s.platforms = { :ios => "13.0" } | ||
| s.source = { :git => "https://github.com/callstackincubator/react-native-harness.git", :tag => "#{s.version}" } | ||
|
|
||
| s.source_files = "ios/**/*.{h,m,mm,swift}" | ||
|
|
||
| if defined?(install_modules_dependencies) | ||
| install_modules_dependencies(s) | ||
| else | ||
| s.dependency "React-Core" | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| #if HARNESS_COVERAGE | ||
| import Foundation | ||
| import UIKit | ||
|
|
||
| @_silgen_name("__llvm_profile_write_file") | ||
| func __llvm_profile_write_file() -> Int32 | ||
|
|
||
| @_silgen_name("__llvm_profile_set_filename") | ||
| func __llvm_profile_set_filename(_ filename: UnsafePointer<CChar>) | ||
|
|
||
| @objc(HarnessCoverageHelper) public class HarnessCoverageHelper: NSObject { | ||
| private static var isSetUp = false | ||
| private static var flushThread: Thread? | ||
|
|
||
| @objc public static func setup() { | ||
| guard !isSetUp else { return } | ||
| isSetUp = true | ||
|
|
||
| let profrawDir = "/tmp/harness-coverage" | ||
| try? FileManager.default.createDirectory(atPath: profrawDir, withIntermediateDirectories: true) | ||
| let profrawPath = "\(profrawDir)/harness-\(ProcessInfo.processInfo.processIdentifier).profraw" | ||
| __llvm_profile_set_filename(profrawPath) | ||
|
|
||
| startFlushTimer() | ||
|
|
||
| NotificationCenter.default.addObserver( | ||
| forName: UIApplication.willTerminateNotification, | ||
| object: nil, queue: nil | ||
| ) { _ in | ||
| _ = __llvm_profile_write_file() | ||
| } | ||
|
|
||
| NotificationCenter.default.addObserver( | ||
| forName: UIApplication.didEnterBackgroundNotification, | ||
| object: nil, queue: nil | ||
| ) { _ in | ||
| _ = __llvm_profile_write_file() | ||
| } | ||
|
|
||
| signal(SIGTERM) { _ in | ||
| _ = __llvm_profile_write_file() | ||
| exit(0) | ||
| } | ||
| } | ||
|
|
||
| private static func startFlushTimer() { | ||
| let thread = Thread { | ||
| let timer = Timer(timeInterval: 1.0, repeats: true) { _ in | ||
| _ = __llvm_profile_write_file() | ||
| } | ||
| RunLoop.current.add(timer, forMode: .default) | ||
| RunLoop.current.run() | ||
| } | ||
| thread.name = "HarnessCoverageFlush" | ||
| thread.qualityOfService = .background | ||
| thread.start() | ||
| flushThread = thread | ||
| } | ||
| } | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| @interface HarnessCoverageSetup : NSObject | ||
| @end | ||
|
|
||
| @implementation HarnessCoverageSetup | ||
|
|
||
| + (void)load { | ||
| #if defined(HARNESS_COVERAGE) | ||
| NSLog(@"[HarnessCoverage] +load called, HARNESS_COVERAGE is defined"); | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| Class helper = NSClassFromString(@"HarnessCoverageHelper"); | ||
| if (helper) { | ||
| NSLog(@"[HarnessCoverage] Found HarnessCoverageHelper, calling setup"); | ||
| #pragma clang diagnostic push | ||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | ||
| [helper performSelector:@selector(setup)]; | ||
| #pragma clang diagnostic pop | ||
| } else { | ||
| NSLog(@"[HarnessCoverage] ERROR: HarnessCoverageHelper class not found"); | ||
| } | ||
| }); | ||
| #else | ||
| NSLog(@"[HarnessCoverage] +load called but HARNESS_COVERAGE is NOT defined"); | ||
| #endif | ||
| } | ||
|
|
||
| @end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| { | ||
| "name": "@react-native-harness/coverage-ios", | ||
| "description": "Native iOS code coverage support for React Native Harness.", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "src", | ||
| "dist", | ||
| "ios", | ||
| "scripts", | ||
| "*.podspec", | ||
| "react-native.config.cjs", | ||
| "!**/__tests__", | ||
| "!**/__fixtures__", | ||
| "!**/__mocks__", | ||
| "!**/.*" | ||
| ], | ||
| "exports": { | ||
| "./package.json": "./package.json", | ||
| ".": { | ||
| "development": "./src/index.ts", | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "default": "./dist/index.js" | ||
| } | ||
| }, | ||
| "peerDependencies": { | ||
| "react-native": "*" | ||
| }, | ||
| "dependencies": { | ||
| "tslib": "^2.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "react-native": "*" | ||
| }, | ||
| "author": { | ||
| "name": "Margelo", | ||
| "email": "hello@margelo.com" | ||
| }, | ||
| "homepage": "https://github.com/callstackincubator/react-native-harness", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/callstackincubator/react-native-harness.git" | ||
| }, | ||
| "license": "MIT" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| module.exports = { | ||
| dependency: { | ||
| platforms: { | ||
| ios: { | ||
| configurations: ['debug'], | ||
| }, | ||
| android: null, | ||
| }, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| module HarnessCoverageHook | ||
| def run_podfile_post_install_hooks | ||
| super | ||
|
|
||
| pods = resolve_coverage_pods | ||
| return if pods.empty? | ||
|
|
||
| Pod::UI.puts "[HarnessCoverage] Instrumenting pods for native coverage: #{pods.join(', ')}" | ||
|
|
||
| apply_coverage_flags_to_pods(pods) | ||
| enable_harness_coverage_pod | ||
| apply_linker_flags | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def resolve_coverage_pods | ||
| script = File.expand_path('resolve-coverage-pods.mjs', __dir__) | ||
| config_json = `node #{script}`.strip | ||
| JSON.parse(config_json) | ||
| rescue => e | ||
| Pod::UI.warn "[HarnessCoverage] Failed to read config: #{e.message}" | ||
| [] | ||
| end | ||
|
Comment on lines
+17
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder whether we can do something about this. It doesn't seem right to assume the structure of the config file. Maybe we should create a separate reac-config.ts file, include the types from the config package there, and run that instead of using this inlined script? |
||
|
|
||
| def apply_coverage_flags_to_pods(pods) | ||
| pods_project.targets.each do |target| | ||
| next unless pods.include?(target.name) | ||
|
|
||
| target.build_configurations.each do |config| | ||
| swift_flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' | ||
| unless swift_flags.include?('-profile-generate') | ||
| config.build_settings['OTHER_SWIFT_FLAGS'] = | ||
| "#{swift_flags} -profile-generate -profile-coverage-mapping" | ||
| end | ||
|
|
||
| c_flags = config.build_settings['OTHER_CFLAGS'] || '$(inherited)' | ||
| unless c_flags.include?('-fprofile-instr-generate') | ||
| config.build_settings['OTHER_CFLAGS'] = | ||
| "#{c_flags} -fprofile-instr-generate -fcoverage-mapping" | ||
| end | ||
| end | ||
|
|
||
| Pod::UI.puts "[HarnessCoverage] -> #{target.name}" | ||
| end | ||
| end | ||
|
|
||
| def enable_harness_coverage_pod | ||
| pods_project.targets.each do |target| | ||
| next unless target.name == 'HarnessCoverage' | ||
|
|
||
| target.build_configurations.each do |config| | ||
| swift_conditions = config.build_settings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] || '$(inherited)' | ||
| unless swift_conditions.include?('HARNESS_COVERAGE') | ||
| config.build_settings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = | ||
| "#{swift_conditions} HARNESS_COVERAGE" | ||
| end | ||
|
|
||
| gcc_defs = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || '$(inherited)' | ||
| unless gcc_defs.include?('HARNESS_COVERAGE') | ||
| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = | ||
| "#{gcc_defs} HARNESS_COVERAGE=1" | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def apply_linker_flags | ||
| pods_project.targets.each do |target| | ||
| target.build_configurations.each do |config| | ||
| ldflags = config.build_settings['OTHER_LDFLAGS'] || '$(inherited)' | ||
| unless ldflags.include?('-fprofile-instr-generate') | ||
| config.build_settings['OTHER_LDFLAGS'] = | ||
| "#{ldflags} -fprofile-instr-generate" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| apply_app_target_linker_flags | ||
| end | ||
|
|
||
| def apply_app_target_linker_flags | ||
| sandbox_root = config.sandbox.root | ||
| target_support_dir = sandbox_root.join('Target Support Files') | ||
|
|
||
| Dir.glob(target_support_dir.join('Pods-*', '*.xcconfig').to_s).each do |xcconfig_path| | ||
| content = File.read(xcconfig_path) | ||
|
|
||
| modified = false | ||
|
|
||
| unless content.include?('-fprofile-instr-generate') | ||
| content = content.gsub( | ||
| /^(OTHER_LDFLAGS\s*=\s*)/, | ||
| "\\1-fprofile-instr-generate " | ||
| ) | ||
| modified = true | ||
| end | ||
|
|
||
| force_load = '-force_load "${PODS_CONFIGURATION_BUILD_DIR}/HarnessCoverage/libHarnessCoverage.a"' | ||
| unless content.include?('libHarnessCoverage.a') | ||
| content = content.gsub( | ||
| /^(OTHER_LDFLAGS\s*=\s*)/, | ||
| "\\1#{force_load} " | ||
| ) | ||
| modified = true | ||
| end | ||
|
|
||
| if modified | ||
| File.write(xcconfig_path, content) | ||
| Pod::UI.puts "[HarnessCoverage] -> patched #{File.basename(xcconfig_path)}" | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| Pod::Installer.prepend(HarnessCoverageHook) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { getConfig } from '@react-native-harness/config'; | ||
|
|
||
| try { | ||
| const { config } = await getConfig(process.cwd()); | ||
| const pods = config.coverage?.native?.ios?.pods ?? []; | ||
| console.log(JSON.stringify(pods)); | ||
| } catch { | ||
| console.log('[]'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "files": [], | ||
| "include": [], | ||
| "references": [ | ||
| { | ||
| "path": "./tsconfig.lib.json" | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "rootDir": "src", | ||
| "outDir": "dist", | ||
| "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", | ||
| "emitDeclarationOnly": false, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "types": ["node"], | ||
| "lib": ["DOM", "ES2022"] | ||
| }, | ||
| "include": ["src/**/*.ts"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.