fastlane 2.236.1 → 2.237.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.
- checksums.yaml +4 -4
- data/README.md +104 -104
- data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +3 -1
- data/fastlane/lib/fastlane/actions/swiftlint.rb +10 -1
- data/fastlane/lib/fastlane/helper/sh_helper.rb +7 -3
- data/fastlane/lib/fastlane/lane_manager.rb +0 -1
- data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +0 -7
- data/fastlane/lib/fastlane/plugins/template/.gitignore +0 -1
- data/fastlane/lib/fastlane/plugins/template/.rspec +0 -2
- data/fastlane/lib/fastlane/plugins/template/Gemfile.erb +0 -2
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +23 -3
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +8 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +8 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +3 -1
- data/fastlane_core/lib/fastlane_core/keychain_importer.rb +1 -1
- data/fastlane_core/lib/fastlane_core/project.rb +3 -1
- data/fastlane_core/lib/fastlane_core/ui/errors/fastlane_shell_error.rb +6 -0
- data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +1 -1
- data/gym/lib/gym/error_handler.rb +11 -1
- data/gym/lib/gym/generators/build_command_generator.rb +2 -0
- data/match/lib/match/options.rb +8 -0
- data/match/lib/match/runner.rb +4 -2
- data/match/lib/match/storage/git_storage.rb +18 -2
- data/precheck/lib/precheck/rules/copyright_date_rule.rb +15 -9
- data/scan/lib/scan/options.rb +5 -0
- data/scan/lib/scan/runner.rb +1 -0
- data/scan/lib/scan/test_command_generator.rb +2 -0
- data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +7 -2
- data/supply/lib/supply/uploader.rb +7 -1
- data/trainer/lib/trainer/xcresult/test_case.rb +21 -2
- data/trainer/lib/trainer/xcresult/test_plan.rb +3 -3
- data/trainer/lib/trainer/xcresult/test_suite.rb +28 -19
- metadata +24 -24
|
@@ -134,6 +134,9 @@ public protocol MatchfileProtocol: AnyObject {
|
|
|
134
134
|
/// Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true'
|
|
135
135
|
var forceForNewCertificates: Bool { get }
|
|
136
136
|
|
|
137
|
+
/// Automatically renew expired certificates. Note: to renew `developer_id` and `developer_id_installer` certificates you must log in with the Account Holder account by using username and password; App Store Connect API key doesn't work in this case. The expired certificate is removed from the match storage and a new one is created, but the old certificate is not revoked on the Apple Developer Portal — run `fastlane match nuke` if you need to free up certificate slots
|
|
138
|
+
var renewExpiredCerts: Bool { get }
|
|
139
|
+
|
|
137
140
|
/// Disables confirmation prompts during nuke, answering them with yes
|
|
138
141
|
var skipConfirmation: Bool { get }
|
|
139
142
|
|
|
@@ -351,6 +354,10 @@ public extension MatchfileProtocol {
|
|
|
351
354
|
return false
|
|
352
355
|
}
|
|
353
356
|
|
|
357
|
+
var renewExpiredCerts: Bool {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
|
|
354
361
|
var skipConfirmation: Bool {
|
|
355
362
|
return false
|
|
356
363
|
}
|
|
@@ -406,4 +413,4 @@ public extension MatchfileProtocol {
|
|
|
406
413
|
|
|
407
414
|
// Please don't remove the lines below
|
|
408
415
|
// They are used to detect outdated files
|
|
409
|
-
// FastlaneRunnerAPIVersion [0.9.
|
|
416
|
+
// FastlaneRunnerAPIVersion [0.9.148]
|
|
@@ -107,6 +107,9 @@ public protocol ScanfileProtocol: AnyObject {
|
|
|
107
107
|
/// Remove retry attempts from test results table and the JUnit report (if not using xcpretty)
|
|
108
108
|
var outputRemoveRetryAttempts: Bool { get }
|
|
109
109
|
|
|
110
|
+
/// Force the use of the '--legacy' flag for xcresulttool instead of using the new commands
|
|
111
|
+
var forceLegacyXcresulttool: Bool { get }
|
|
112
|
+
|
|
110
113
|
/// **DEPRECATED!** Use `output_style: 'raw'` instead - Disable xcpretty formatting of build, similar to `output_style='raw'` but this will also skip the test results table
|
|
111
114
|
var disableXcpretty: Bool? { get }
|
|
112
115
|
|
|
@@ -387,6 +390,10 @@ public extension ScanfileProtocol {
|
|
|
387
390
|
return false
|
|
388
391
|
}
|
|
389
392
|
|
|
393
|
+
var forceLegacyXcresulttool: Bool {
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
|
|
390
397
|
var disableXcpretty: Bool? {
|
|
391
398
|
return nil
|
|
392
399
|
}
|
|
@@ -574,4 +581,4 @@ public extension ScanfileProtocol {
|
|
|
574
581
|
|
|
575
582
|
// Please don't remove the lines below
|
|
576
583
|
// They are used to detect outdated files
|
|
577
|
-
// FastlaneRunnerAPIVersion [0.9.
|
|
584
|
+
// FastlaneRunnerAPIVersion [0.9.159]
|
|
@@ -404,6 +404,8 @@ module FastlaneCore
|
|
|
404
404
|
private
|
|
405
405
|
|
|
406
406
|
def file_upload_option(source)
|
|
407
|
+
# altool's --upload-app and --validate-app only support the -f flag,
|
|
408
|
+
# unlike iTMSTransporter which requires -assetFile for .ipa/.pkg since 2026.
|
|
407
409
|
"-f #{source.shellescape}"
|
|
408
410
|
end
|
|
409
411
|
|
|
@@ -482,7 +484,7 @@ module FastlaneCore
|
|
|
482
484
|
'"' + Helper.transporter_path + '"',
|
|
483
485
|
'-m verify',
|
|
484
486
|
build_credential_params(username, password, jwt),
|
|
485
|
-
|
|
487
|
+
file_upload_option(source),
|
|
486
488
|
("-WONoPause true" if Helper.windows?), # Windows only: process instantly returns instead of waiting for key press
|
|
487
489
|
("-itc_provider #{provider_short_name}" if jwt.nil? && !provider_short_name.to_s.empty?)
|
|
488
490
|
].compact.join(' ')
|
|
@@ -10,7 +10,7 @@ module FastlaneCore
|
|
|
10
10
|
password_part = " -P #{certificate_password.shellescape}"
|
|
11
11
|
certificate_format_part = certificate_format.to_s.strip.empty? ? "" : " -f #{certificate_format.shellescape}"
|
|
12
12
|
|
|
13
|
-
command = "security import #{path.shellescape} -k
|
|
13
|
+
command = "security import #{path.shellescape} -k #{keychain_path.shellescape}"
|
|
14
14
|
command << password_part
|
|
15
15
|
command << certificate_format_part
|
|
16
16
|
command << " -T /usr/bin/codesign" # to not be asked for permission when running a tool like `gym` (before Sierra)
|
|
@@ -395,7 +395,9 @@ module FastlaneCore
|
|
|
395
395
|
xcode_at_least_13 = FastlaneCore::Helper.xcode_at_least?("13")
|
|
396
396
|
if xcode_at_least_13 && options[:destination]
|
|
397
397
|
begin
|
|
398
|
-
|
|
398
|
+
destination = options[:destination]
|
|
399
|
+
destination = destination.first if destination.kind_of?(Array)
|
|
400
|
+
destination_parameter = " " + "-destination #{destination.shellescape}"
|
|
399
401
|
rescue => ex
|
|
400
402
|
# xcodebuild command can continue without destination parameter, so
|
|
401
403
|
# we really don't care about this exception if something goes wrong with shellescape
|
|
@@ -3,6 +3,12 @@ require_relative 'fastlane_exception'
|
|
|
3
3
|
module FastlaneCore
|
|
4
4
|
class Interface
|
|
5
5
|
class FastlaneShellError < FastlaneException
|
|
6
|
+
attr_reader :show_github_issues
|
|
7
|
+
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
@show_github_issues = options[:show_github_issues].nil? ? false : options[:show_github_issues]
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
def prefix
|
|
7
13
|
'[SHELL_ERROR]'
|
|
8
14
|
end
|
|
@@ -167,7 +167,7 @@ module Commander
|
|
|
167
167
|
end
|
|
168
168
|
rescue FastlaneCore::Interface::FastlaneCommonException => e # these are exceptions that we don't count as crashes
|
|
169
169
|
display_user_error!(e, e.to_s)
|
|
170
|
-
rescue FastlaneCore::Interface::FastlaneError => e # user_error!
|
|
170
|
+
rescue FastlaneCore::Interface::FastlaneError, FastlaneCore::Interface::FastlaneShellError => e # user_error! or shell_error!
|
|
171
171
|
rescue_fastlane_error(e)
|
|
172
172
|
rescue Errno::ENOENT => e
|
|
173
173
|
rescue_file_error(e)
|
|
@@ -151,11 +151,21 @@ module Gym
|
|
|
151
151
|
# `xcodebuild` doesn't properly mark lines as failure reason or important information
|
|
152
152
|
# so we assume that the last few lines show the error message that's relevant
|
|
153
153
|
# (at least that's what was correct during testing)
|
|
154
|
-
|
|
154
|
+
log_lines = File.read(log_path).split("\n")
|
|
155
|
+
log_content = log_lines.last(5)
|
|
155
156
|
log_content.each do |row|
|
|
156
157
|
UI.command_output(row)
|
|
157
158
|
end
|
|
158
159
|
|
|
160
|
+
# If we didn't find the error in the last 5 lines, let's search for it
|
|
161
|
+
# This is specifically for errors that might be buried by subsequent warnings
|
|
162
|
+
last_5_lines = log_content.join("\n")
|
|
163
|
+
unless last_5_lines.include?("error:")
|
|
164
|
+
UI.verbose("Error not found in last 5 lines, searching for it in the log...")
|
|
165
|
+
error_line = log_lines.reverse.find { |l| l.include?("error:") }
|
|
166
|
+
UI.command_output(error_line) if error_line
|
|
167
|
+
end
|
|
168
|
+
|
|
159
169
|
UI.message("")
|
|
160
170
|
UI.error("⬆️ Check out the few lines of raw `xcodebuild` output above for potential hints on how to solve this error")
|
|
161
171
|
UI.important("📋 For the complete and more detailed error log, check the full log at:")
|
|
@@ -119,6 +119,8 @@ module Gym
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def pipe_xcpretty
|
|
122
|
+
UI.important("Using xcpretty can result in missing some build errors and is slower than the preferred xcbeautify. See https://docs.fastlane.tools/best-practices/xcodebuild-formatters/ for more information.")
|
|
123
|
+
|
|
122
124
|
pipe = []
|
|
123
125
|
|
|
124
126
|
formatter = Gym.config[:xcpretty_formatter]
|
data/match/lib/match/options.rb
CHANGED
|
@@ -297,6 +297,14 @@ module Match
|
|
|
297
297
|
description: "Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true'",
|
|
298
298
|
type: Boolean,
|
|
299
299
|
default_value: false),
|
|
300
|
+
FastlaneCore::ConfigItem.new(key: :renew_expired_certs,
|
|
301
|
+
env_name: "MATCH_RENEW_EXPIRED_CERTS",
|
|
302
|
+
description: "Automatically renew expired certificates. Note: to renew `developer_id` and `developer_id_installer` certificates " \
|
|
303
|
+
"you must log in with the Account Holder account by using username and password; App Store Connect API key doesn't work in this case. " \
|
|
304
|
+
"The expired certificate is removed from the match storage and a new one is created, but the old certificate is not revoked on the " \
|
|
305
|
+
"Apple Developer Portal — run `fastlane match nuke` if you need to free up certificate slots",
|
|
306
|
+
type: Boolean,
|
|
307
|
+
default_value: false),
|
|
300
308
|
FastlaneCore::ConfigItem.new(key: :skip_confirmation,
|
|
301
309
|
env_name: "MATCH_SKIP_CONFIRMATION",
|
|
302
310
|
description: "Disables confirmation prompts during nuke, answering them with yes",
|
data/match/lib/match/runner.rb
CHANGED
|
@@ -85,12 +85,12 @@ module Match
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Certificate
|
|
88
|
-
cert_id = fetch_certificate(params: params, renew_expired_certs:
|
|
88
|
+
cert_id = fetch_certificate(params: params, renew_expired_certs: params[:renew_expired_certs])
|
|
89
89
|
|
|
90
90
|
# Mac Installer Distribution Certificate
|
|
91
91
|
additional_cert_types = params[:additional_cert_types] || []
|
|
92
92
|
cert_ids = additional_cert_types.map do |additional_cert_type|
|
|
93
|
-
fetch_certificate(params: params, renew_expired_certs:
|
|
93
|
+
fetch_certificate(params: params, renew_expired_certs: params[:renew_expired_certs], specific_cert_type: additional_cert_type)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
profile_type = Sigh.profile_type_for_distribution_type(
|
|
@@ -174,6 +174,8 @@ module Match
|
|
|
174
174
|
is_cert_renewable = is_authenticated_with_login || is_cert_renewable_via_api
|
|
175
175
|
|
|
176
176
|
# Validate existing certificate first.
|
|
177
|
+
# NOTE: only a single certificate is considered here — the one matching
|
|
178
|
+
# `certificate_id`, or otherwise the last one found for this type.
|
|
177
179
|
if renew_expired_certs && is_cert_renewable && storage_has_certs && !params[:readonly]
|
|
178
180
|
cert_path = select_cert_or_key(paths: certs, certificate_id: certificate_id)
|
|
179
181
|
|
|
@@ -111,7 +111,7 @@ module Match
|
|
|
111
111
|
|
|
112
112
|
begin
|
|
113
113
|
# GIT_TERMINAL_PROMPT will fail the `git clone` command if user credentials are missing
|
|
114
|
-
Helper.with_env_values(
|
|
114
|
+
Helper.with_env_values(git_env_values) do
|
|
115
115
|
FastlaneCore::CommandExecutor.execute(command: command,
|
|
116
116
|
print_all: FastlaneCore::Globals.verbose?,
|
|
117
117
|
print_command: FastlaneCore::Globals.verbose?)
|
|
@@ -252,7 +252,7 @@ module Match
|
|
|
252
252
|
commands << git_push_command
|
|
253
253
|
|
|
254
254
|
UI.message("Pushing changes to remote git repo...")
|
|
255
|
-
Helper.with_env_values(
|
|
255
|
+
Helper.with_env_values(git_env_values) do
|
|
256
256
|
commands.each do |command|
|
|
257
257
|
FastlaneCore::CommandExecutor.execute(command: command,
|
|
258
258
|
print_all: FastlaneCore::Globals.verbose?,
|
|
@@ -263,6 +263,22 @@ module Match
|
|
|
263
263
|
UI.error("Couldn't commit or push changes back to git...")
|
|
264
264
|
UI.error(ex)
|
|
265
265
|
end
|
|
266
|
+
|
|
267
|
+
def git_env_values
|
|
268
|
+
env_values = { 'GIT_TERMINAL_PROMPT' => '0' }
|
|
269
|
+
env_values['GIT_SSH_COMMAND'] = non_interactive_git_ssh_command
|
|
270
|
+
env_values
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def non_interactive_git_ssh_command
|
|
274
|
+
ssh_command = ENV['GIT_SSH_COMMAND']
|
|
275
|
+
|
|
276
|
+
if ssh_command.nil? || ssh_command.empty?
|
|
277
|
+
return "ssh -o BatchMode=yes"
|
|
278
|
+
end
|
|
279
|
+
return ssh_command if ssh_command.include?('BatchMode=yes')
|
|
280
|
+
"#{ssh_command} -o BatchMode=yes"
|
|
281
|
+
end
|
|
266
282
|
end
|
|
267
283
|
end
|
|
268
284
|
end
|
|
@@ -15,23 +15,29 @@ module Precheck
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.description
|
|
18
|
-
"using a copyright
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def pass_if_empty?
|
|
22
|
-
return false
|
|
18
|
+
"using a copyright year in the future, or missing a copyright year"
|
|
23
19
|
end
|
|
24
20
|
|
|
25
21
|
def supported_fields_symbol_set
|
|
26
22
|
[:copyright].to_set
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
def
|
|
30
|
-
|
|
25
|
+
def rule_block
|
|
26
|
+
return lambda { |text|
|
|
27
|
+
year = copyright_year(text)
|
|
28
|
+
if year.nil?
|
|
29
|
+
RuleReturn.new(validation_state: VALIDATION_STATES[:failed], failure_data: "missing copyright year")
|
|
30
|
+
elsif year > DateTime.now.year
|
|
31
|
+
RuleReturn.new(validation_state: VALIDATION_STATES[:failed], failure_data: "copyright year is in the future: #{year}")
|
|
32
|
+
else
|
|
33
|
+
RuleReturn.new(validation_state: VALIDATION_STATES[:passed])
|
|
34
|
+
end
|
|
35
|
+
}
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def
|
|
34
|
-
|
|
38
|
+
def copyright_year(text)
|
|
39
|
+
match = text.to_s.match(/\b(?:19|20)\d{2}\b/)
|
|
40
|
+
match && match[0].to_i
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
end
|
data/scan/lib/scan/options.rb
CHANGED
|
@@ -273,6 +273,11 @@ module Scan
|
|
|
273
273
|
description: "Remove retry attempts from test results table and the JUnit report (if not using xcpretty)",
|
|
274
274
|
type: Boolean,
|
|
275
275
|
default_value: false),
|
|
276
|
+
FastlaneCore::ConfigItem.new(key: :force_legacy_xcresulttool,
|
|
277
|
+
env_names: ["SCAN_FORCE_LEGACY_XCRESULTTOOL", "TRAINER_FORCE_LEGACY_XCRESULTTOOL"],
|
|
278
|
+
description: "Force the use of the '--legacy' flag for xcresulttool instead of using the new commands",
|
|
279
|
+
type: Boolean,
|
|
280
|
+
default_value: false),
|
|
276
281
|
|
|
277
282
|
# xcpretty
|
|
278
283
|
FastlaneCore::ConfigItem.new(key: :disable_xcpretty,
|
data/scan/lib/scan/runner.rb
CHANGED
|
@@ -216,6 +216,7 @@ module Scan
|
|
|
216
216
|
params = {
|
|
217
217
|
path: result_bundle_path,
|
|
218
218
|
output_remove_retry_attempts: Scan.config[:output_remove_retry_attempts],
|
|
219
|
+
force_legacy_xcresulttool: Scan.config[:force_legacy_xcresulttool],
|
|
219
220
|
silent: !FastlaneCore::Globals.verbose?
|
|
220
221
|
}
|
|
221
222
|
|
|
@@ -177,6 +177,8 @@ module Scan
|
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
def pipe_xcpretty
|
|
180
|
+
UI.important("Using xcpretty can result in missing some build errors and is slower than the preferred xcbeautify. See https://docs.fastlane.tools/best-practices/xcodebuild-formatters/ for more information.")
|
|
181
|
+
|
|
180
182
|
formatter = []
|
|
181
183
|
if (custom_formatter = Scan.config[:xcpretty_formatter] || Scan.config[:formatter])
|
|
182
184
|
if custom_formatter.end_with?(".rb")
|
|
@@ -717,9 +717,9 @@ module Spaceship
|
|
|
717
717
|
type: "appInfoLocalizations",
|
|
718
718
|
attributes: attributes,
|
|
719
719
|
relationships: {
|
|
720
|
-
|
|
720
|
+
appInfo: {
|
|
721
721
|
data: {
|
|
722
|
-
type: "
|
|
722
|
+
type: "appInfos",
|
|
723
723
|
id: app_info_id
|
|
724
724
|
}
|
|
725
725
|
}
|
|
@@ -742,6 +742,11 @@ module Spaceship
|
|
|
742
742
|
tunes_request_client.patch("#{Version::V1}/appInfoLocalizations/#{app_info_localization_id}", body)
|
|
743
743
|
end
|
|
744
744
|
|
|
745
|
+
def delete_app_info_localization(app_info_localization_id: nil)
|
|
746
|
+
params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil)
|
|
747
|
+
tunes_request_client.delete("#{Version::V1}/appInfoLocalizations/#{app_info_localization_id}", params)
|
|
748
|
+
end
|
|
749
|
+
|
|
745
750
|
#
|
|
746
751
|
# appStoreReviewDetails
|
|
747
752
|
#
|
|
@@ -19,9 +19,15 @@ module Supply
|
|
|
19
19
|
|
|
20
20
|
track_to_update = Supply.config[:track]
|
|
21
21
|
|
|
22
|
+
# Capture whether any new binaries were actually uploaded before appending
|
|
23
|
+
# version_codes_to_retain. The branch below uses this flag so that retained
|
|
24
|
+
# codes do not cause a promote-only run to take the upload path.
|
|
25
|
+
# See: https://github.com/fastlane/fastlane/issues/29984
|
|
26
|
+
uploading_new_binaries = !apk_version_codes.empty?
|
|
27
|
+
|
|
22
28
|
apk_version_codes.concat(Supply.config[:version_codes_to_retain]) if Supply.config[:version_codes_to_retain]
|
|
23
29
|
|
|
24
|
-
if
|
|
30
|
+
if uploading_new_binaries
|
|
25
31
|
# Only update tracks if we have version codes
|
|
26
32
|
# update_track handle setting rollout if needed
|
|
27
33
|
# Updating a track with empty version codes can completely clear out a track
|
|
@@ -115,11 +115,30 @@ module Trainer
|
|
|
115
115
|
@retries&.count || 0
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
# Result of the final attempt (last repetition), or the overall result when there are no retries.
|
|
119
|
+
# When retry attempts are removed, counts should reflect this final outcome rather than the
|
|
120
|
+
# aggregate `@result`, which xcresulttool reports as `Failed` even if the test was skipped on retry.
|
|
121
|
+
def final_result
|
|
122
|
+
@retries && !@retries.empty? ? @retries.last.result : @result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def final_failed?
|
|
126
|
+
final_result == 'Failed'
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def final_skipped?
|
|
130
|
+
final_result == 'Skipped'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def total_tests_count(output_remove_retry_attempts: false)
|
|
134
|
+
return 1 if output_remove_retry_attempts
|
|
135
|
+
|
|
119
136
|
retries_count > 0 ? retries_count : 1
|
|
120
137
|
end
|
|
121
138
|
|
|
122
|
-
def total_failures_count
|
|
139
|
+
def total_failures_count(output_remove_retry_attempts: false)
|
|
140
|
+
return final_failed? ? 1 : 0 if output_remove_retry_attempts
|
|
141
|
+
|
|
123
142
|
if retries_count > 0
|
|
124
143
|
@retries.count(&:failed?)
|
|
125
144
|
elsif failed?
|
|
@@ -40,7 +40,7 @@ module Trainer
|
|
|
40
40
|
# Allows iteration over test suites. Used by TestParser to collect test results
|
|
41
41
|
include Enumerable
|
|
42
42
|
def each(&block)
|
|
43
|
-
test_suites.map(
|
|
43
|
+
test_suites.map { |suite| suite.to_hash(output_remove_retry_attempts: output_remove_retry_attempts) }.each(&block)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Generates a JUnit-compatible XML representation of the test plan
|
|
@@ -49,8 +49,8 @@ module Trainer
|
|
|
49
49
|
# Create the root testsuites element with calculated summary attributes
|
|
50
50
|
testsuites = Helper.create_xml_element('testsuites',
|
|
51
51
|
tests: test_suites.sum(&:test_cases_count).to_s,
|
|
52
|
-
failures: test_suites.sum(
|
|
53
|
-
skipped: test_suites.sum(
|
|
52
|
+
failures: test_suites.sum { |suite| suite.failures_count(output_remove_retry_attempts: output_remove_retry_attempts) }.to_s,
|
|
53
|
+
skipped: test_suites.sum { |suite| suite.skipped_count(output_remove_retry_attempts: output_remove_retry_attempts) }.to_s,
|
|
54
54
|
time: test_suites.sum(&:duration).to_s)
|
|
55
55
|
|
|
56
56
|
# Create <properties> node for configuration and device, to be applied to each suite node
|
|
@@ -58,38 +58,47 @@ module Trainer
|
|
|
58
58
|
@test_cases_count ||= @test_cases.count + @sub_suites.sum(&:test_cases_count)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
# When output_remove_retry_attempts is true, counts reflect each test case's final attempt
|
|
62
|
+
# only (matching the trimmed JUnit output), so a test that failed then was skipped on retry
|
|
63
|
+
# is no longer counted as a failure.
|
|
64
|
+
def failures_count(output_remove_retry_attempts: false)
|
|
65
|
+
predicate = output_remove_retry_attempts ? :final_failed? : :failed?
|
|
66
|
+
@test_cases.count(&predicate) +
|
|
67
|
+
@sub_suites.sum { |suite| suite.failures_count(output_remove_retry_attempts: output_remove_retry_attempts) }
|
|
63
68
|
end
|
|
64
69
|
|
|
65
|
-
def skipped_count
|
|
66
|
-
|
|
70
|
+
def skipped_count(output_remove_retry_attempts: false)
|
|
71
|
+
predicate = output_remove_retry_attempts ? :final_skipped? : :skipped?
|
|
72
|
+
@test_cases.count(&predicate) +
|
|
73
|
+
@sub_suites.sum { |suite| suite.skipped_count(output_remove_retry_attempts: output_remove_retry_attempts) }
|
|
67
74
|
end
|
|
68
75
|
|
|
69
|
-
def total_tests_count
|
|
70
|
-
@test_cases.sum(
|
|
71
|
-
@sub_suites.sum(
|
|
76
|
+
def total_tests_count(output_remove_retry_attempts: false)
|
|
77
|
+
@test_cases.sum { |test_case| test_case.total_tests_count(output_remove_retry_attempts: output_remove_retry_attempts) } +
|
|
78
|
+
@sub_suites.sum { |suite| suite.total_tests_count(output_remove_retry_attempts: output_remove_retry_attempts) }
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
def total_failures_count
|
|
75
|
-
@test_cases.sum(
|
|
76
|
-
@sub_suites.sum(
|
|
81
|
+
def total_failures_count(output_remove_retry_attempts: false)
|
|
82
|
+
@test_cases.sum { |test_case| test_case.total_failures_count(output_remove_retry_attempts: output_remove_retry_attempts) } +
|
|
83
|
+
@sub_suites.sum { |suite| suite.total_failures_count(output_remove_retry_attempts: output_remove_retry_attempts) }
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
def total_retries_count
|
|
86
|
+
def total_retries_count(output_remove_retry_attempts: false)
|
|
87
|
+
return 0 if output_remove_retry_attempts
|
|
88
|
+
|
|
80
89
|
@test_cases.sum(&:retries_count) +
|
|
81
90
|
@sub_suites.sum(&:total_retries_count)
|
|
82
91
|
end
|
|
83
92
|
|
|
84
93
|
# Hash representation used by TestParser to collect test results
|
|
85
|
-
def to_hash
|
|
94
|
+
def to_hash(output_remove_retry_attempts: false)
|
|
86
95
|
{
|
|
87
|
-
number_of_tests: total_tests_count,
|
|
88
|
-
number_of_failures: total_failures_count,
|
|
96
|
+
number_of_tests: total_tests_count(output_remove_retry_attempts: output_remove_retry_attempts),
|
|
97
|
+
number_of_failures: total_failures_count(output_remove_retry_attempts: output_remove_retry_attempts),
|
|
89
98
|
number_of_tests_excluding_retries: test_cases_count,
|
|
90
|
-
number_of_failures_excluding_retries: failures_count,
|
|
91
|
-
number_of_retries: total_retries_count,
|
|
92
|
-
number_of_skipped: skipped_count
|
|
99
|
+
number_of_failures_excluding_retries: failures_count(output_remove_retry_attempts: output_remove_retry_attempts),
|
|
100
|
+
number_of_retries: total_retries_count(output_remove_retry_attempts: output_remove_retry_attempts),
|
|
101
|
+
number_of_skipped: skipped_count(output_remove_retry_attempts: output_remove_retry_attempts)
|
|
93
102
|
}
|
|
94
103
|
end
|
|
95
104
|
|
|
@@ -100,8 +109,8 @@ module Trainer
|
|
|
100
109
|
name: @name,
|
|
101
110
|
time: duration.to_s,
|
|
102
111
|
tests: test_cases_count.to_s,
|
|
103
|
-
failures: failures_count.to_s,
|
|
104
|
-
skipped: skipped_count.to_s)
|
|
112
|
+
failures: failures_count(output_remove_retry_attempts: output_remove_retry_attempts).to_s,
|
|
113
|
+
skipped: skipped_count(output_remove_retry_attempts: output_remove_retry_attempts).to_s)
|
|
105
114
|
|
|
106
115
|
# Add test cases
|
|
107
116
|
@test_cases.each do |test_case|
|