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 +4 -4
- data/lib/fastlane/plugin/stream_actions/actions/build_test_coverage_map.rb +122 -17
- data/lib/fastlane/plugin/stream_actions/actions/select_impacted_tests.rb +2 -1
- data/lib/fastlane/plugin/stream_actions/actions/show_sdk_size.rb +10 -3
- data/lib/fastlane/plugin/stream_actions/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8d838fe7bdba0809e724b8ceffcdd8a9681c65597a7315085c8eda719afd520
|
|
4
|
+
data.tar.gz: e8e093b56d26b398288396cff51df3b063e776a80a9aa82231d9859d5b76317a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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`.
|
|
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
|
-
|
|
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]
|
|
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
|
''
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
54
|
+
version: 0.11.0
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: fastlane
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|