fastlane-plugin-stream_actions 0.4.13 → 0.4.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 637f0d12b17b61c738e37e6530202c347d45776ec6281591c1b6ea94e162d501
4
- data.tar.gz: fc3494f5e33aa0e6cde00c3bb741880e709338f670742a4a1697774f8fcd36af
3
+ metadata.gz: d8d838fe7bdba0809e724b8ceffcdd8a9681c65597a7315085c8eda719afd520
4
+ data.tar.gz: e8e093b56d26b398288396cff51df3b063e776a80a9aa82231d9859d5b76317a
5
5
  SHA512:
6
- metadata.gz: 9fe31f2ee1e876d9bab6e7e434d06b74f9de6f9bf70124afda534638d8c6eb96e88c49f06c05669f432bdcfdbcf6024e4317cf4646202d53c78e82c2d58093a1
7
- data.tar.gz: df42ca37cabdb7c533379f407aa3dfd21898214eb1206ca61cfad635f082562e91cc9bd35153ba06c7f736181151369c06e8b7aafd80df7a3a1bd2e118742ece
6
+ metadata.gz: abcc77e118f5c4c023c099544a850f0e8f2989d83510d15e9bd086b0f1a9fe00f6ea07ef753ca3cf382508032f5a26b6877ca06d3dbfb040776ab0c1a6df16f1
7
+ data.tar.gz: 3d7c1c83fab37d3096946d1064d30e82a733995dd7cca21813f568de18d64f61dca101241a75ca1cd8f93a0f01f308257406fead0f15ff7e62f1f47a12c09839
@@ -2,15 +2,36 @@ require 'json'
2
2
  require 'open3'
3
3
  require 'set'
4
4
  require 'time'
5
+ require 'tmpdir'
5
6
 
6
7
  module Fastlane
7
8
  module Actions
8
9
  class BuildTestCoverageMapAction < Action
9
10
  def self.run(params)
10
11
  source_root = File.expand_path(params[:source_root] || Dir.pwd)
12
+ sources, tests = params[:class_dirs] ? jacoco_coverage(params, source_root) : llvm_coverage(params, source_root)
13
+
14
+ carry_over_failed_tests(tests, sources, params) unless params[:failed_tests].to_a.empty?
15
+ drop_untracked(tests, sources, source_root)
16
+
17
+ map = {
18
+ 'version' => Helper::TestCoverageMapHelper::MAP_VERSION,
19
+ 'generated_at' => Time.now.utc.iso8601,
20
+ 'commit' => params[:commit],
21
+ 'sources' => sources.sort,
22
+ 'tests' => tests.sort.to_h
23
+ }
24
+ write(map, params[:output_path])
25
+ UI.success("Coverage map of #{tests.size} tests and #{map['sources'].size} sources saved to #{params[:output_path]}")
26
+ map
27
+ end
28
+
29
+ # iOS: one folder of .profraw files per test, `<coverage_dir>/<TestClass>/<testMethod>/`.
30
+ def self.llvm_coverage(params, source_root)
11
31
  test_dirs = Dir.glob(File.join(params[:coverage_dir], '*', '*')).select { |dir| File.directory?(dir) }
12
32
  UI.user_error!("No per-test coverage found in '#{params[:coverage_dir]}'") if test_dirs.empty?
13
33
 
34
+ UI.user_error!('Pass the instrumented binaries or the class_dirs') if params[:binaries].to_a.empty?
14
35
  binaries = params[:binaries].select { |binary| File.exist?(binary) }
15
36
  UI.user_error!('None of the instrumented binaries exist') if binaries.empty?
16
37
 
@@ -41,20 +62,81 @@ module Fastlane
41
62
  end
42
63
  end
43
64
  workers.each(&:join)
65
+ [sources, tests]
66
+ end
44
67
 
45
- carry_over_failed_tests(tests, sources, params) unless params[:failed_tests].to_a.empty?
46
- drop_untracked(tests, sources, source_root)
68
+ # Android: one JaCoCo `.ec` file per test, named `<package>.<TestClass>#<testMethod>.ec` by the test orchestrator.
69
+ def self.jacoco_coverage(params, source_root)
70
+ exec_files = Dir.glob(File.join(params[:coverage_dir], '**', '*.ec'))
71
+ UI.user_error!("No per-test coverage found in '#{params[:coverage_dir]}'") if exec_files.empty?
47
72
 
48
- map = {
49
- 'version' => Helper::TestCoverageMapHelper::MAP_VERSION,
50
- 'generated_at' => Time.now.utc.iso8601,
51
- 'commit' => params[:commit],
52
- 'sources' => sources.sort,
53
- 'tests' => tests.sort.to_h
54
- }
55
- write(map, params[:output_path])
56
- UI.success("Coverage map of #{tests.size} tests and #{map['sources'].size} sources saved to #{params[:output_path]}")
57
- map
73
+ class_dirs = params[:class_dirs].select { |dir| File.directory?(dir) }
74
+ UI.user_error!('None of the class_dirs exist') if class_dirs.empty?
75
+
76
+ UI.message("Building the coverage map of #{exec_files.size} tests ⌛️")
77
+ cli = params[:jacoco_cli] || download_jacoco_cli
78
+ paths = kotlin_and_java_paths(source_root)
79
+ source_of_class = jacoco_class_sources(cli, class_dirs).transform_values { |key| paths[key] }.compact
80
+
81
+ tests = {}
82
+ jacoco_executed_classes(cli, exec_files).each do |file, classes|
83
+ test_class, method = File.basename(file, '.ec').split('#', 2)
84
+ next UI.important("#{File.basename(file)} is not named <TestClass>#<testMethod>.ec, skipping") if method.nil?
85
+
86
+ tests["#{test_class.split('.').last}/#{method}"] = classes.flat_map { |name| source_of_class[name].to_a }.uniq
87
+ end
88
+ [source_of_class.values.flatten.uniq, tests]
89
+ end
90
+
91
+ # Class name => `<package path>/<source file name>`, from an XML report of the class files without execution data.
92
+ # Classes without code (interfaces, annotations, constants) are empty `<class/>` elements and stay out, like
93
+ # declaration-only files in llvm-cov. One report per folder, as JaCoCo fails on a class name that two folders
94
+ # define differently (e.g. a stale build).
95
+ def self.jacoco_class_sources(cli, class_dirs)
96
+ Dir.mktmpdir('jacoco') do |dir|
97
+ xml = File.join(dir, 'classes.xml')
98
+ class_dirs.each_with_object({}) do |class_dir, sources|
99
+ Helper::TestCoverageMapHelper.command('java', '-jar', cli, 'report', '--classfiles', class_dir, '--xml', xml, '--quiet')
100
+ File.read(xml).scan(/<class name="([^"]+)" sourcefilename="([^"]+)">/).each do |name, source|
101
+ sources[name] ||= "#{File.dirname(name)}/#{source}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Exec file => names of the classes with at least one executed probe.
108
+ def self.jacoco_executed_classes(cli, exec_files)
109
+ exec_files.each_slice(100).with_object({}) do |slice, result|
110
+ current = nil
111
+ Helper::TestCoverageMapHelper.command('java', '-jar', cli, 'execinfo', *slice).each_line do |line|
112
+ if (loaded = line[/\[INFO\] Loading exec file (.+)\.$/, 1])
113
+ current = result[loaded] = []
114
+ elsif current && (hit = line.match(/^\h+\s+(\d+) of\s+\d+\s+(\S+)$/)) && hit[1].to_i.positive?
115
+ current << hit[2]
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ # `<package path>/<file name>` => repository paths of the tracked Kotlin and Java files. The package comes from the
122
+ # file, as a Kotlin file does not have to sit in the folder of its package.
123
+ def self.kotlin_and_java_paths(source_root)
124
+ files = Helper::TestCoverageMapHelper.command('git', '-C', source_root, 'ls-files', '*.kt', '*.java').split("\n")
125
+ files.each_with_object({}) do |file, paths|
126
+ package = File.foreach(File.join(source_root, file)).lazy.map { |line| line[/^\s*package\s+([\w.]+)/, 1] }.find(&:itself)
127
+ (paths["#{package.to_s.tr('.', '/')}/#{File.basename(file)}".delete_prefix('/')] ||= []) << file
128
+ end
129
+ end
130
+
131
+ def self.download_jacoco_cli
132
+ version = '0.8.13'
133
+ path = File.join(Dir.tmpdir, "jacococli-#{version}.jar")
134
+ return path if File.exist?(path)
135
+
136
+ url = "https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/#{version}/org.jacoco.cli-#{version}-nodeps.jar"
137
+ Helper::TestCoverageMapHelper.command('curl', '-sSfL', '-o', "#{path}.part", url)
138
+ File.rename("#{path}.part", path)
139
+ path
58
140
  end
59
141
 
60
142
  # Returns the instrumented files (`all`) and the files with at least one executed line (`covered`).
@@ -152,15 +234,21 @@ module Fastlane
152
234
  #####################################################
153
235
 
154
236
  def self.description
155
- 'Builds a map of which source files every test executes, from per-test LLVM coverage profiles'
237
+ 'Builds a map of which source files every test executes, from per-test LLVM or JaCoCo coverage'
156
238
  end
157
239
 
158
240
  def self.details
159
241
  <<~DETAILS
160
- Expects one folder per test, `<coverage_dir>/<TestClass>/<testMethod>/`, holding the `.profraw` files
242
+ iOS: expects one folder per test, `<coverage_dir>/<TestClass>/<testMethod>/`, holding the `.profraw` files
161
243
  the app wrote while that test ran (e.g. via `LLVM_PROFILE_FILE=<dir>/<TestClass>/<testMethod>/%m_%p%c.profraw`
162
244
  in the app's launch environment). Each folder is merged with `llvm-profdata` and exported with `llvm-cov`
163
- against the instrumented `binaries`. The resulting JSON map feeds `select_impacted_tests`.
245
+ against the instrumented `binaries`.
246
+
247
+ Android: pass `class_dirs` instead of `binaries`, and a `coverage_dir` holding one JaCoCo `.ec` file per test,
248
+ `<package>.<TestClass>#<testMethod>.ec`, which the test orchestrator writes with `-e coverage true
249
+ -e coverageFilePath <dir>/`. A source file counts as executed when one of its classes ran a probe.
250
+
251
+ The resulting JSON map feeds `select_impacted_tests`.
164
252
  DETAILS
165
253
  end
166
254
 
@@ -178,7 +266,19 @@ module Fastlane
178
266
  FastlaneCore::ConfigItem.new(
179
267
  key: :binaries,
180
268
  description: 'Instrumented binaries the profiles belong to, e.g. the app executable (or its .debug.dylib) and every SDK framework',
181
- type: Array
269
+ type: Array,
270
+ optional: true
271
+ ),
272
+ FastlaneCore::ConfigItem.new(
273
+ key: :class_dirs,
274
+ description: 'Android: folders with the compiled classes of the app and its modules. Switches to JaCoCo coverage',
275
+ type: Array,
276
+ optional: true
277
+ ),
278
+ FastlaneCore::ConfigItem.new(
279
+ key: :jacoco_cli,
280
+ description: 'Android: path to the JaCoCo CLI jar (org.jacoco.cli nodeps). Downloaded when not set',
281
+ optional: true
182
282
  ),
183
283
  FastlaneCore::ConfigItem.new(
184
284
  key: :source_root,
@@ -240,6 +340,11 @@ module Fastlane
240
340
  coverage_dir: "test_output/coverage",
241
341
  binaries: ["App.app/App.debug.dylib", "App.app/Frameworks/SDK.framework/SDK"],
242
342
  output_path: "test_output/e2e_coverage_map.json"
343
+ )',
344
+ 'build_test_coverage_map(
345
+ coverage_dir: "coverage",
346
+ class_dirs: Dir.glob("*/build/intermediates/classes/debug/**/dirs"),
347
+ output_path: "e2e_coverage_map.json"
243
348
  )'
244
349
  ]
245
350
  end
@@ -249,7 +354,7 @@ module Fastlane
249
354
  end
250
355
 
251
356
  def self.is_supported?(platform)
252
- [:ios, :mac].include?(platform)
357
+ [:ios, :mac, :android].include?(platform)
253
358
  end
254
359
  end
255
360
  end
@@ -6,7 +6,8 @@ module Fastlane
6
6
  module Actions
7
7
  class SelectImpactedTestsAction < Action
8
8
  CODE_EXTENSIONS = %w[.swift .m .mm .h .kt .java .dart .js .jsx .ts .tsx].freeze
9
- MODIFIERS = '(?:(?:public|open|internal|fileprivate|private|final|indirect|nonisolated|data|sealed|abstract|export|default)\\s+)*'
9
+ # Kotlin's `enum class` and `fun interface` name the type after the second keyword.
10
+ MODIFIERS = '(?:(?:public|open|internal|fileprivate|private|protected|final|indirect|nonisolated|data|sealed|abstract|annotation|value|inline|inner|enum(?=\\s+class\\b)|fun(?=\\s+interface\\b)|export|default)\\s+)*'
10
11
  DECLARATION = /^\s*(?:@\w+(?:\([^)\n]*\))?\s+)*#{MODIFIERS}(class|struct|enum|protocol|actor|typealias|extension|interface|object)\s+([A-Za-z_]\w*)/.freeze
11
12
 
12
13
  def self.run(params)
@@ -6,6 +6,7 @@ module Fastlane
6
6
  fail_status = '🔴' # Failure if a branch is more than #{max_tolerance} less performant than the benchmark
7
7
  success_status = '🟢' # Success if a branch is more performant or equals to the benchmark
8
8
  outstanding_status = '🚀' # Outstanding performance
9
+ new_status = '🆕' # No benchmark yet, e.g. a newly tracked size
9
10
 
10
11
  metrics_dir = 'metrics'
11
12
  FileUtils.remove_dir(metrics_dir, force: true)
@@ -23,7 +24,13 @@ module Fastlane
23
24
  params[:branch_sizes].each do |sdk_name, branch_value_kb|
24
25
  branch_value_mb = (branch_value_kb / 1024.0).round(2)
25
26
  branch_value = is_kb ? branch_value_kb.round(0) : branch_value_mb
26
- benchmark_value_kb = benchmark_sizes[sdk_name.to_s] || 0
27
+ benchmark_value_kb = benchmark_sizes[sdk_name.to_s]
28
+ # A size the benchmark doesn't track yet has nothing to compare against. Only a push to the base
29
+ # branch or a release branch run rewrites the benchmark (see below), so PRs show it as new until then.
30
+ if benchmark_value_kb.nil?
31
+ markdown_table << "|#{sdk_name}|n/a|#{branch_value} #{params[:size_ext]}|n/a|#{new_status}|\n"
32
+ next
33
+ end
27
34
  benchmark_value_mb = (benchmark_value_kb / 1024.0).round(2)
28
35
  benchmark_value = is_kb ? benchmark_value_kb.round(0) : benchmark_value_mb
29
36
 
@@ -32,8 +39,8 @@ module Fastlane
32
39
  diff = is_kb ? diff_b : diff_kb
33
40
  diff_ext = is_kb ? 'B' : 'KB'
34
41
 
35
- max_tolerance = params[:max_tolerance] || diff_ext == 'B' ? 5000 : 500 # By default, Max Tolerance is 5000 Bytes or 500 Kilobytes
36
- fine_tolerance = params[:fine_tolerance] || diff_ext == 'B' ? 2500 : 250 # By default, Fine Tolerance is 2500 Bytes or 250 Kilobytes
42
+ max_tolerance = params[:max_tolerance] || (diff_ext == 'B' ? 5000 : 500) # By default, Max Tolerance is 5000 Bytes or 500 Kilobytes
43
+ fine_tolerance = params[:fine_tolerance] || (diff_ext == 'B' ? 2500 : 250) # By default, Fine Tolerance is 2500 Bytes or 250 Kilobytes
37
44
 
38
45
  diff_sign = if diff.zero?
39
46
  ''
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module StreamActions
3
- VERSION = '0.4.13'
3
+ VERSION = '0.4.15'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-stream_actions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.13
4
+ version: 0.4.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - GetStream
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xctest_list
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 0.9.0
47
+ version: 0.11.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 0.9.0
54
+ version: 0.11.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: fastlane
57
57
  requirement: !ruby/object:Gem::Requirement