-
Notifications
You must be signed in to change notification settings - Fork 11
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| require "json" | ||
|
|
||
| package = JSON.parse(File.read(File.join(__dir__, "package.json"))) | ||
|
|
||
| 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/margelo/react-native-harness.git", :tag => "#{s.version}" } | ||
|
|
||
| s.source_files = "ios/**/*.{h,m,mm,swift}" | ||
|
|
||
| install_modules_dependencies(s) | ||
| end | ||
|
|
||
| require_relative 'scripts/harness_coverage_hook' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| #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 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 docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! | ||
| let profrawPath = docs.appendingPathComponent("harness-\(ProcessInfo.processInfo.processIdentifier).profraw").path | ||
| __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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #if defined(HARNESS_COVERAGE) | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| @interface HarnessCoverageSetup : NSObject | ||
| @end | ||
|
|
||
| @implementation HarnessCoverageSetup | ||
|
|
||
| + (void)load { | ||
| Class helper = NSClassFromString(@"HarnessCoverageHelper"); | ||
| if (helper) { | ||
| #pragma clang diagnostic push | ||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | ||
| [helper performSelector:@selector(setup)]; | ||
| #pragma clang diagnostic pop | ||
| } | ||
| } | ||
|
|
||
| @end | ||
| #endif |
| 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/margelo/react-native-harness", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/margelo/react-native-harness.git" | ||
| }, | ||
|
Comment on lines
+39
to
+47
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. 🙈 |
||
| "license": "MIT" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| module.exports = { | ||
| dependency: { | ||
| platforms: { | ||
| ios: { | ||
| configurations: ['debug'], | ||
| }, | ||
| android: null, | ||
| }, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| 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 | ||
| project_dir = Dir.pwd | ||
| config_json = `node -e " | ||
| import('#{project_dir}/rn-harness.config.mjs') | ||
| .then(m => console.log(JSON.stringify( | ||
| m.default?.coverage?.native?.ios?.pods || [] | ||
| ))) | ||
| .catch(() => console.log('[]')) | ||
| "`.strip | ||
| JSON.parse(config_json) | ||
| rescue => e | ||
| Pod::UI.warn "[HarnessCoverage] Failed to read config: #{e.message}" | ||
| [] | ||
| end | ||
|
Comment on lines
+17
to
+30
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 | ||
| end | ||
| end | ||
|
|
||
| Pod::Installer.prepend(HarnessCoverageHook) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export {}; |
| 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" | ||
| } | ||
| ] | ||
| } |
| 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"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🫢