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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +104 -104
  3. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +3 -1
  4. data/fastlane/lib/fastlane/actions/swiftlint.rb +10 -1
  5. data/fastlane/lib/fastlane/helper/sh_helper.rb +7 -3
  6. data/fastlane/lib/fastlane/lane_manager.rb +0 -1
  7. data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +0 -7
  8. data/fastlane/lib/fastlane/plugins/template/.gitignore +0 -1
  9. data/fastlane/lib/fastlane/plugins/template/.rspec +0 -2
  10. data/fastlane/lib/fastlane/plugins/template/Gemfile.erb +0 -2
  11. data/fastlane/lib/fastlane/version.rb +1 -1
  12. data/fastlane/swift/Deliverfile.swift +1 -1
  13. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  14. data/fastlane/swift/Fastlane.swift +23 -3
  15. data/fastlane/swift/Gymfile.swift +1 -1
  16. data/fastlane/swift/GymfileProtocol.swift +1 -1
  17. data/fastlane/swift/Matchfile.swift +1 -1
  18. data/fastlane/swift/MatchfileProtocol.swift +8 -1
  19. data/fastlane/swift/Precheckfile.swift +1 -1
  20. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  21. data/fastlane/swift/Scanfile.swift +1 -1
  22. data/fastlane/swift/ScanfileProtocol.swift +8 -1
  23. data/fastlane/swift/Screengrabfile.swift +1 -1
  24. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  25. data/fastlane/swift/Snapshotfile.swift +1 -1
  26. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  27. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +3 -1
  28. data/fastlane_core/lib/fastlane_core/keychain_importer.rb +1 -1
  29. data/fastlane_core/lib/fastlane_core/project.rb +3 -1
  30. data/fastlane_core/lib/fastlane_core/ui/errors/fastlane_shell_error.rb +6 -0
  31. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +1 -1
  32. data/gym/lib/gym/error_handler.rb +11 -1
  33. data/gym/lib/gym/generators/build_command_generator.rb +2 -0
  34. data/match/lib/match/options.rb +8 -0
  35. data/match/lib/match/runner.rb +4 -2
  36. data/match/lib/match/storage/git_storage.rb +18 -2
  37. data/precheck/lib/precheck/rules/copyright_date_rule.rb +15 -9
  38. data/scan/lib/scan/options.rb +5 -0
  39. data/scan/lib/scan/runner.rb +1 -0
  40. data/scan/lib/scan/test_command_generator.rb +2 -0
  41. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +7 -2
  42. data/supply/lib/supply/uploader.rb +7 -1
  43. data/trainer/lib/trainer/xcresult/test_case.rb +21 -2
  44. data/trainer/lib/trainer/xcresult/test_plan.rb +3 -3
  45. data/trainer/lib/trainer/xcresult/test_suite.rb +28 -19
  46. metadata +24 -24
@@ -17,4 +17,4 @@ public class Gymfile: GymfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -392,4 +392,4 @@ public extension GymfileProtocol {
392
392
 
393
393
  // Please don't remove the lines below
394
394
  // They are used to detect outdated files
395
- // FastlaneRunnerAPIVersion [0.9.153]
395
+ // FastlaneRunnerAPIVersion [0.9.154]
@@ -17,4 +17,4 @@ public class Matchfile: MatchfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -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.147]
416
+ // FastlaneRunnerAPIVersion [0.9.148]
@@ -17,4 +17,4 @@ public class Precheckfile: PrecheckfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -84,4 +84,4 @@ public extension PrecheckfileProtocol {
84
84
 
85
85
  // Please don't remove the lines below
86
86
  // They are used to detect outdated files
87
- // FastlaneRunnerAPIVersion [0.9.146]
87
+ // FastlaneRunnerAPIVersion [0.9.147]
@@ -17,4 +17,4 @@ public class Scanfile: ScanfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -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.158]
584
+ // FastlaneRunnerAPIVersion [0.9.159]
@@ -17,4 +17,4 @@ public class Screengrabfile: ScreengrabfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -161,4 +161,4 @@ public extension ScreengrabfileProtocol {
161
161
 
162
162
  // Please don't remove the lines below
163
163
  // They are used to detect outdated files
164
- // FastlaneRunnerAPIVersion [0.9.148]
164
+ // FastlaneRunnerAPIVersion [0.9.149]
@@ -17,4 +17,4 @@ public class Snapshotfile: SnapshotfileProtocol {
17
17
  // during the `init` process, and you won't see this message
18
18
  }
19
19
 
20
- // Generated with fastlane 2.236.1
20
+ // Generated with fastlane 2.237.0
@@ -378,4 +378,4 @@ public extension SnapshotfileProtocol {
378
378
 
379
379
  // Please don't remove the lines below
380
380
  // They are used to detect outdated files
381
- // FastlaneRunnerAPIVersion [0.9.142]
381
+ // FastlaneRunnerAPIVersion [0.9.143]
@@ -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
- "-f #{source.shellescape}",
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 '#{keychain_path.shellescape}'"
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
- destination_parameter = " " + "-destination #{options[:destination].shellescape}"
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
- log_content = File.read(log_path).split("\n").last(5)
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]
@@ -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",
@@ -85,12 +85,12 @@ module Match
85
85
  end
86
86
 
87
87
  # Certificate
88
- cert_id = fetch_certificate(params: params, renew_expired_certs: false)
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: false, specific_cert_type: additional_cert_type)
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('GIT_TERMINAL_PROMPT' => '0') do
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('GIT_TERMINAL_PROMPT' => '0') do
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 date that is any different from this current year, or missing a date"
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 word_search_type
30
- WORD_SEARCH_TYPES[:fail_on_exclusion]
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 lowercased_words_to_look_for
34
- [DateTime.now.year.to_s]
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
@@ -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,
@@ -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
- appStoreVersion: {
720
+ appInfo: {
721
721
  data: {
722
- type: "appStoreVersions",
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 !apk_version_codes.empty?
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
- def total_tests_count
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(&:to_hash).each(&block)
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(&:failures_count).to_s,
53
- skipped: test_suites.sum(&:skipped_count).to_s,
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
- def failures_count
62
- @failures_count ||= @test_cases.count(&:failed?) + @sub_suites.sum(&:failures_count)
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
- @skipped_count ||= @test_cases.count(&:skipped?) + @sub_suites.sum(&:skipped_count)
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(&:total_tests_count) +
71
- @sub_suites.sum(&:total_tests_count)
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(&:total_failures_count) +
76
- @sub_suites.sum(&:total_failures_count)
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|