fastlane-plugin-test_center 3.14.3 → 3.14.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e081e2a0d9d6bcb59c38f24072eb0059edef4457f03fed6e2318930260e9f5a4
4
- data.tar.gz: 681403d6200f5d943e28f9c3fa6b90a6f450d7a9252c476eb7cf77c7e0c6101d
3
+ metadata.gz: 290ab3e6fb5932555e74d834784e80cb441dc4b7e4feb49eda479382a603a1e5
4
+ data.tar.gz: f46250359e1d156d283dd033e6e8db8c851297ea7a6385d78da4e72a014d0ebd
5
5
  SHA512:
6
- metadata.gz: 98e979a4fbafe31e6dc33bd1a3a94a1e1da316ce8226c3688e9d223461a33c5d75c9291dc837e256c227915959547f8964df38c524e40b45742408235ee94713
7
- data.tar.gz: 3049728d26e108dea44480f7445e41be419f4189e3bb2b63b8cef7871a9d7bf58b5edcee01351dc40496209f6805706bfcb0b6cf08b9fee6c43bba1decbfecda
6
+ metadata.gz: aaf987944f48d6511755077c7e4e1c564f722ad8c7f69f8c4e0f24b7c5db62590edc9fc69372e98d19e0cc8b7466f8b3fd2ef2a1ddba472b37bc45c3ee0b12a5
7
+ data.tar.gz: e1eb52e0b8b396019fe9727af6895948ad675c574b80293c866dfd627752d0e2dc3c8dd11b3e8f7508a40f1f64732a4b2987b1f92938aa6cf21b98d9c0d731d7
data/README.md CHANGED
@@ -99,7 +99,7 @@ _fastlane_ is the easiest way to automate beta deployments and releases for your
99
99
  ## Supporters
100
100
 
101
101
  ![Владислав Давыдов](https://avatars1.githubusercontent.com/u/47553334?s=44&u=4691860dba898943b947180b3d28bb85851b0d7c&v=4)
102
- [vdavydovHH](https://github.com/vdavydovHH)
102
+ [vdavydovHH](https://github.com/vdavydovHH) ![amelendezSGY](https://avatars1.githubusercontent.com/u/28657466?s=44&v=4)[amelendezSGY](https://github.com/amelendezSGY)
103
103
 
104
104
  ## License
105
105
 
@@ -8,7 +8,7 @@ module Fastlane
8
8
  if report_filepaths.size == 1
9
9
  FileUtils.cp(report_filepaths[0], params[:collated_report])
10
10
  else
11
- UI.verbose("collate_junit_reports with #{report_filepaths}")
11
+ verbose("collate_junit_reports with #{report_filepaths}")
12
12
  reports = report_filepaths.map { |report_filepath| REXML::Document.new(File.new(report_filepath)) }
13
13
  packages = reports.map { |r| r.root.attribute('name').value }.uniq
14
14
  combine_multiple_targets = packages.size > 1
@@ -23,7 +23,7 @@ module Fastlane
23
23
  increment_testable_tries(target_report.root, report.root)
24
24
  package = report.root.attribute('name').value
25
25
  preprocess_testsuites(report, package, combine_multiple_targets)
26
- UI.verbose("> collating last report file #{report_filepaths.last}")
26
+ verbose("> collating last report file #{report_filepaths.last}")
27
27
  report.elements.each('//testsuite') do |testsuite|
28
28
  testsuite_name = testsuite.attribute('name').value
29
29
  package_attribute = ''
@@ -32,15 +32,15 @@ module Fastlane
32
32
  end
33
33
  target_testsuite = REXML::XPath.first(target_report, "//testsuite[@name='#{testsuite_name}' #{package_attribute}]")
34
34
  if target_testsuite
35
- UI.verbose(" > collating testsuite #{testsuite_name}")
35
+ verbose(" > collating testsuite #{testsuite_name}")
36
36
  collate_testsuite(target_testsuite, testsuite)
37
- UI.verbose(" < collating testsuite #{testsuite_name}")
37
+ verbose(" < collating testsuite #{testsuite_name}")
38
38
  else
39
39
  testable = REXML::XPath.first(target_report, "//testsuites")
40
40
  testable << testsuite
41
41
  end
42
42
  end
43
- UI.verbose("< collating last report file #{report_filepaths.last}")
43
+ verbose("< collating last report file #{report_filepaths.last}")
44
44
  end
45
45
  target_report.elements.each('//testsuite') do |testsuite|
46
46
  update_testsuite_counts(testsuite)
@@ -83,12 +83,12 @@ module Fastlane
83
83
  testsuite_name = testsuite.attribute('name').value
84
84
  duplicate_testsuites = REXML::XPath.match(report, "//testsuite[@name='#{testsuite_name}']")
85
85
  if duplicate_testsuites.size > 1
86
- UI.verbose(" > flattening_duplicate_testsuites")
86
+ verbose(" > flattening_duplicate_testsuites")
87
87
  duplicate_testsuites.drop(1).each do |duplicate_testsuite|
88
88
  collate_testsuite(testsuite, duplicate_testsuite)
89
89
  duplicate_testsuite.parent.delete_element(duplicate_testsuite)
90
90
  end
91
- UI.verbose(" < flattening_duplicate_testsuites")
91
+ verbose(" < flattening_duplicate_testsuites")
92
92
  end
93
93
  update_testsuite_counts(testsuite)
94
94
  end
@@ -108,14 +108,14 @@ module Fastlane
108
108
  target_testcase = REXML::XPath.first(target_testsuite, "testcase[@name='#{name}' and @classname='#{classname}']")
109
109
  # Replace target_testcase with testcase
110
110
  if target_testcase
111
- UI.verbose(" collate_testsuite with testcase #{name}")
112
- UI.verbose(" replacing \"#{target_testcase}\" with \"#{testcase}\"")
111
+ verbose(" collate_testsuite with testcase #{name}")
112
+ verbose(" replacing \"#{target_testcase}\" with \"#{testcase}\"")
113
113
  parent = target_testcase.parent
114
114
  increment_testcase_tries(target_testcase, testcase) unless testcase.root == target_testcase.root
115
115
  parent.insert_after(target_testcase, testcase)
116
116
  parent.delete_element(target_testcase)
117
- UI.verbose("")
118
- UI.verbose(" target_testcase after replacement \"#{parent}\"")
117
+ verbose("")
118
+ verbose(" target_testcase after replacement \"#{parent}\"")
119
119
  else
120
120
  target_testsuite << testcase
121
121
  end
@@ -165,6 +165,12 @@ module Fastlane
165
165
  (value1 + value2).to_s
166
166
  end
167
167
 
168
+ def self.verbose(message)
169
+ return if ENV.fetch('COLLATE_JUNIT_REPORTS_VERBOSITY', 1).to_i.zero?
170
+
171
+ UI.verbose(message)
172
+ end
173
+
168
174
  #####################################################
169
175
  # @!group Documentation
170
176
  #####################################################
@@ -28,16 +28,23 @@ module Fastlane
28
28
  force_quit_simulator_processes if params[:quit_simulators]
29
29
 
30
30
  prepare_for_testing(params.values)
31
+
32
+ tests_passed = true
33
+ summary = {}
34
+ if params[:build_for_testing]
35
+ summary = build_summary
36
+ else
37
+ coerce_destination_to_array(params)
38
+ platform = :mac
39
+ platform = :ios_simulator if Scan.config[:destination].any? { |d| d.include?('platform=iOS Simulator') }
31
40
 
32
- coerce_destination_to_array(params)
33
- platform = :mac
34
- platform = :ios_simulator if Scan.config[:destination].any? { |d| d.include?('platform=iOS Simulator') }
41
+ runner_options = params.values.merge(platform: platform)
42
+ runner = ::TestCenter::Helper::MultiScanManager::Runner.new(runner_options)
43
+ tests_passed = runner.run
35
44
 
36
- runner_options = params.values.merge(platform: platform)
37
- runner = ::TestCenter::Helper::MultiScanManager::Runner.new(runner_options)
38
- tests_passed = runner.run
45
+ summary = run_summary(params, tests_passed)
46
+ end
39
47
 
40
- summary = run_summary(params, tests_passed)
41
48
  print_run_summary(summary)
42
49
 
43
50
  if params[:fail_build] && !tests_passed
@@ -101,6 +108,16 @@ module Fastlane
101
108
  # :nocov:
102
109
  end
103
110
 
111
+ def self.build_summary
112
+ {
113
+ result: true,
114
+ total_tests: 0,
115
+ passing_testcount: 0,
116
+ failed_testcount: 0,
117
+ total_retry_count: 0
118
+ }
119
+ end
120
+
104
121
  def self.run_summary(scan_options, tests_passed)
105
122
  scan_options = scan_options.clone
106
123
 
@@ -361,6 +378,14 @@ module Fastlane
361
378
  UI.user_error!("Error: Batch counts must be greater than zero") unless count > 0
362
379
  end
363
380
  ),
381
+ FastlaneCore::ConfigItem.new(
382
+ key: :batches,
383
+ env_name: "FL_MULTI_SCAN_BATCHES",
384
+ description: "The explicit batches (an Array of Arrays of test identifiers) to run either serially, or each batch on a simulator in parallel if :parallel_testrun_count is given",
385
+ type: Array,
386
+ optional: true,
387
+ conflicting_options: [:batch_count]
388
+ ),
364
389
  FastlaneCore::ConfigItem.new(
365
390
  key: :retry_test_runner_failures,
366
391
  description: "Set to true If you want to treat build failures during testing, like 'Test runner exited before starting test execution', as 'all tests failed'",
@@ -375,7 +400,7 @@ module Fastlane
375
400
  is_string: false,
376
401
  default_value: false,
377
402
  optional: true,
378
- conflicting_options: [:batch_count],
403
+ conflicting_options: %i[batch_count batches],
379
404
  conflict_block: proc do |value|
380
405
  UI.user_error!(
381
406
  "Error: Can't use 'invocation_based_tests' and 'batch_count' options in one run, "\
@@ -433,7 +458,7 @@ module Fastlane
433
458
  ),
434
459
  FastlaneCore::ConfigItem.new(
435
460
  key: :testrun_completed_block,
436
- description: 'A block invoked each time a test run completes. When combined with :parallel_testrun_count, will be called separately in each child process',
461
+ description: 'A block invoked each time a test run completes. When combined with :parallel_testrun_count, will be called separately in each child process. Return a Hash with :continue set to false to stop retrying tests, or :only_testing to change which tests will be run in the next try',
437
462
  optional: true,
438
463
  is_string: false,
439
464
  default_value: nil,
@@ -461,6 +486,10 @@ module Fastlane
461
486
 
462
487
  # UI.abort_with_message!('You could conditionally abort')
463
488
  UI.message(\"\\\u1F60A everything is fine, let's continue try \#{try_attempt + 1} for batch \#{batch}\")
489
+ {
490
+ continue: true,
491
+ only_testing: ['AtomicBoyUITests/AtomicBoyUITests/testExample17']
492
+ }
464
493
  end
465
494
 
466
495
  multi_scan(
@@ -486,6 +515,120 @@ module Fastlane
486
515
  output_files: 'report.json',
487
516
  fail_build: false
488
517
  )
518
+ ",
519
+ "
520
+ UI.header('batches feature')
521
+ multi_scan(
522
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
523
+ scheme: 'AtomicBoy',
524
+ try_count: 3,
525
+ fail_build: false,
526
+ batches: [
527
+ [
528
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample5',
529
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample10',
530
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample15'
531
+ ],
532
+ [
533
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample6',
534
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample12',
535
+ 'AtomicBoyUITests/AtomicBoyUITests/testExample18'
536
+ ]
537
+ ]
538
+ )
539
+ "
540
+ ]
541
+ end
542
+
543
+ def self.integration_tests
544
+ [
545
+ "
546
+ UI.header('Basic test')
547
+ multi_scan(
548
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
549
+ scheme: 'AtomicBoy',
550
+ fail_build: false,
551
+ try_count: 2,
552
+ disable_xcpretty: true
553
+ )
554
+ ",
555
+ "
556
+ UI.header('Basic test with 1 specific test')
557
+ multi_scan(
558
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
559
+ scheme: 'AtomicBoy',
560
+ fail_build: false,
561
+ try_count: 2,
562
+ only_testing: ['AtomicBoyUITests/AtomicBoyUITests/testExample']
563
+ )
564
+ ",
565
+ "
566
+ UI.header('Basic test with test target expansion')
567
+ multi_scan(
568
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
569
+ scheme: 'AtomicBoy',
570
+ fail_build: false,
571
+ try_count: 2,
572
+ only_testing: ['AtomicBoyUITests', 'AtomicBoyTests']
573
+ )
574
+ ",
575
+ "
576
+ UI.header('Parallel test run')
577
+ multi_scan(
578
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
579
+ scheme: 'AtomicBoy',
580
+ fail_build: false,
581
+ try_count: 2,
582
+ parallel_testrun_count: 2
583
+ )
584
+ ",
585
+ "
586
+ UI.header('Parallel test run with fewer tests than parallel test runs')
587
+ multi_scan(
588
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
589
+ scheme: 'AtomicBoy',
590
+ fail_build: false,
591
+ try_count: 2,
592
+ parallel_testrun_count: 4,
593
+ only_testing: ['AtomicBoyUITests/AtomicBoyUITests/testExample']
594
+ )
595
+ ",
596
+ "
597
+ UI.header('Basic test with batch count')
598
+ multi_scan(
599
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
600
+ scheme: 'AtomicBoy',
601
+ fail_build: false,
602
+ try_count: 2,
603
+ batch_count: 2
604
+ )
605
+ ",
606
+ "
607
+ UI.header('Basic test with batches')
608
+ multi_scan(
609
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
610
+ scheme: 'AtomicBoy',
611
+ fail_build: false,
612
+ try_count: 2,
613
+ batches: [
614
+ ['AtomicBoyUITests/AtomicBoyUITests/testExample', 'AtomicBoyUITests/AtomicBoyUITests/testExample2'],
615
+ ['AtomicBoyUITests/AtomicBoyUITests/testExample3', 'AtomicBoyUITests/AtomicBoyUITests/testExample4']
616
+ ],
617
+ parallel_testrun_count: 2
618
+ )
619
+ ",
620
+ "
621
+ UI.header('Basic test with xcresult')
622
+ multi_scan(
623
+ workspace: File.absolute_path('../AtomicBoy/AtomicBoy.xcworkspace'),
624
+ scheme: 'AtomicBoy',
625
+ output_types: 'xcresult',
626
+ output_files: 'result.xcresult',
627
+ collate_reports: false,
628
+ fail_build: false,
629
+ try_count: 2,
630
+ batch_count: 2
631
+ )
489
632
  "
490
633
  ]
491
634
  end
@@ -1,6 +1,7 @@
1
1
 
2
2
  module FixedCopyLogarchiveFastlaneSimulator
3
3
  def self.included(base)
4
+ @@log_collection_start_time = DateTime.now
4
5
  base.instance_eval do
5
6
  def copy_logarchive(device, log_identity, logs_destination_dir)
6
7
  require 'shellwords'
@@ -8,7 +9,11 @@ module FixedCopyLogarchiveFastlaneSimulator
8
9
  logarchive_dst = File.join(logs_destination_dir, "system_logs-#{log_identity}.logarchive")
9
10
  FileUtils.rm_rf(logarchive_dst)
10
11
  FileUtils.mkdir_p(File.expand_path("..", logarchive_dst))
11
- command = "xcrun simctl spawn #{device.udid} log collect --output #{logarchive_dst.shellescape} 2>/dev/null"
12
+
13
+ logs_collection_start = @@log_collection_start_time.strftime('%Y-%m-%d %H:%M:%S')
14
+ command = "xcrun simctl spawn #{device.udid} log collect "
15
+ command << "--start '#{logs_collection_start}' "
16
+ command << "--output #{logarchive_dst.shellescape} 2>/dev/null"
12
17
  FastlaneCore::CommandExecutor.execute(command: command, print_all: false, print_command: true)
13
18
  end
14
19
  end
@@ -1,6 +1,13 @@
1
1
  module TestCenter
2
2
  module Helper
3
3
  module HtmlTestReport
4
+
5
+ def self.verbose(message)
6
+ return if ENV.fetch('COLLATE_HTML_REPORTS_VERBOSITY', 1).to_i.zero?
7
+
8
+ FastlaneCore::UI.verbose(message)
9
+ end
10
+
4
11
  class Report
5
12
  require 'rexml/formatters/transitive'
6
13
 
@@ -24,16 +31,16 @@ module TestCenter
24
31
  def collate_report(report)
25
32
  testsuites.each(&:remove_duplicate_testcases)
26
33
  report.testsuites.each(&:remove_duplicate_testcases)
27
- FastlaneCore::UI.verbose("TestCenter::Helper::HtmlTestReport::Report.collate_report to report:\n\t#{@root}")
34
+ HtmlTestReport.verbose("TestCenter::Helper::HtmlTestReport::Report.collate_report to report:\n\t#{@root}")
28
35
  report.testsuites.each do |given_testsuite|
29
36
  existing_testsuite = testsuite_with_title(given_testsuite.title)
30
37
  if existing_testsuite.nil?
31
- FastlaneCore::UI.verbose("\tadding testsuite\n\t\t#{given_testsuite}")
38
+ HtmlTestReport.verbose("\tadding testsuite\n\t\t#{given_testsuite}")
32
39
  add_testsuite(given_testsuite)
33
40
  else
34
- FastlaneCore::UI.verbose("\tcollating testsuite\n\t\t#{given_testsuite.root}")
41
+ HtmlTestReport.verbose("\tcollating testsuite\n\t\t#{given_testsuite.root}")
35
42
  existing_testsuite.collate_testsuite(given_testsuite)
36
- FastlaneCore::UI.verbose("\tafter collation exiting testsuite\n\t\t#{existing_testsuite.root}")
43
+ HtmlTestReport.verbose("\tafter collation exiting testsuite\n\t\t#{existing_testsuite.root}")
37
44
  end
38
45
  end
39
46
  update_test_count
@@ -210,15 +217,15 @@ module TestCenter
210
217
  given_testcases.each do |given_testcase|
211
218
  existing_testcase = testcase_with_title(given_testcase.title)
212
219
  if existing_testcase.nil?
213
- FastlaneCore::UI.verbose("\t\tadding testcase\n\t\t\t#{given_testcase.root}")
220
+ HtmlTestReport.verbose("\t\tadding testcase\n\t\t\t#{given_testcase.root}")
214
221
  unless given_testcase.passing?
215
- FastlaneCore::UI.verbose("\t\t\twith failure:\n\t\t\t\t#{given_testcase.failure_details}")
222
+ HtmlTestReport.verbose("\t\t\twith failure:\n\t\t\t\t#{given_testcase.failure_details}")
216
223
  end
217
224
  add_testcase(given_testcase)
218
225
  else
219
- FastlaneCore::UI.verbose("\t\tupdating testcase\n\t\t\t#{existing_testcase.root}")
226
+ HtmlTestReport.verbose("\t\tupdating testcase\n\t\t\t#{existing_testcase.root}")
220
227
  unless given_testcase.passing?
221
- FastlaneCore::UI.verbose("\t\t\twith failure:\n\t\t\t\t#{given_testcase.failure_details}")
228
+ HtmlTestReport.verbose("\t\t\twith failure:\n\t\t\t\t#{given_testcase.failure_details}")
222
229
  end
223
230
  existing_testcase.update_testcase(given_testcase)
224
231
  end
@@ -275,7 +282,7 @@ module TestCenter
275
282
  color = row_color
276
283
  failure = failure_details
277
284
  if failure.nil? && !passing?
278
- FastlaneCore::UI.error("\t\t\t\tupdating failing test case that does not have failure_details")
285
+ HtmlTestReport.error("\t\t\t\tupdating failing test case that does not have failure_details")
279
286
  end
280
287
  parent = @root.parent
281
288
 
@@ -283,7 +290,7 @@ module TestCenter
283
290
 
284
291
  new_failure = testcase.failure_details
285
292
  if new_failure && testcase.passing?
286
- FastlaneCore::UI.error("\t\t\t\tswapping passing failing test case that _does_have_ failure_details")
293
+ HtmlTestReport.error("\t\t\t\tswapping passing failing test case that _does_have_ failure_details")
287
294
  end
288
295
 
289
296
  parent.replace_child(@root, testcase.root)
@@ -50,7 +50,11 @@ module TestCenter
50
50
  if @options[:scan_devices_override]
51
51
  scan_device_names = @options[:scan_devices_override].map { |device| device.name }
52
52
  FastlaneCore::UI.verbose("\tSetting Scan.devices to #{scan_device_names}")
53
- Scan.devices.replace(@options[:scan_devices_override])
53
+ if Scan.devices
54
+ Scan.devices.replace(@options[:scan_devices_override])
55
+ else
56
+ Scan.devices = @options[:scan_devices_override]
57
+ end
54
58
  end
55
59
  end
56
60
 
@@ -16,6 +16,7 @@ module TestCenter
16
16
  @options[:output_files],
17
17
  @options[:custom_report_file_name]
18
18
  )
19
+ @callback_overrides_only_testing = false
19
20
  end
20
21
 
21
22
  def before_testrun
@@ -189,7 +190,26 @@ module TestCenter
189
190
  update_json_failure_details(info)
190
191
  update_test_result_bundle_details(info)
191
192
 
192
- @options[:testrun_completed_block].call(info)
193
+ @callback_overrides_only_testing = false
194
+ callback_result = @options[:testrun_completed_block].call(info)
195
+ if callback_result.kind_of?(Hash)
196
+ should_continue = callback_result.fetch(:continue, true)
197
+ if !should_continue
198
+ discontinue_message = 'Following testrun_completed_block\'s request to discontinue testing'
199
+ discontinue_message << " for batch ##{@options[:batch]}" unless @options[:batch].nil?
200
+ FastlaneCore::UI.verbose(discontinue_message)
201
+ @testrun_count = options[:try_count]
202
+ end
203
+ overridden_only_testing = callback_result.fetch(:only_testing, nil)
204
+ if overridden_only_testing && should_continue
205
+ override_only_testing_message = 'Following testrun_completed_block\'s request to change :only_testing to '
206
+ override_only_testing_message << overridden_only_testing.to_s
207
+ override_only_testing_message << " for batch ##{@options[:batch]}" unless @options[:batch].nil?
208
+ FastlaneCore::UI.verbose(override_only_testing_message)
209
+ @callback_overrides_only_testing = true
210
+ @options[:only_testing] = overridden_only_testing
211
+ end
212
+ end
193
213
  end
194
214
 
195
215
  def failure_details(additional_info)
@@ -242,6 +262,8 @@ module TestCenter
242
262
  end
243
263
 
244
264
  def update_only_testing
265
+ return if @callback_overrides_only_testing
266
+
245
267
  report_filepath = File.join(output_directory, @reportnamer.junit_last_reportname)
246
268
  config = FastlaneCore::Configuration.create(
247
269
  Fastlane::Actions::TestsFromJunitAction.available_options,
@@ -41,6 +41,7 @@ module TestCenter
41
41
  def setup_logcollection
42
42
  FastlaneCore::UI.verbose("> setup_logcollection")
43
43
  return unless @options[:include_simulator_logs]
44
+ return unless @options[:platform] == :ios_simulator
44
45
  return if Scan::Runner.method_defined?(:prelaunch_simulators)
45
46
 
46
47
  # We need to prelaunch the simulators so xcodebuild
@@ -65,8 +66,8 @@ module TestCenter
65
66
 
66
67
  @test_collector = TestCollector.new(@options)
67
68
  @options.reject! { |key| %i[testplan].include?(key) }
68
- @batch_count = @test_collector.test_batches.size
69
- tests = @test_collector.test_batches.flatten
69
+ @batch_count = @test_collector.batches.size
70
+ tests = @test_collector.batches.flatten
70
71
  if tests.size < @options[:parallel_testrun_count].to_i
71
72
  FastlaneCore::UI.important(":parallel_testrun_count greater than the number of tests (#{tests.size}). Reducing to that number.")
72
73
  @options[:parallel_testrun_count] = tests.size
@@ -89,16 +90,51 @@ module TestCenter
89
90
  remove_preexisting_test_result_bundles
90
91
  remote_preexisting_xcresult_bundles
91
92
 
92
- tests_passed = false
93
+ test_results = [false]
93
94
  if should_run_tests_through_single_try?
94
- tests_passed = run_tests_through_single_try
95
+ test_results.clear
96
+ setup_run_tests_for_each_device do |device_name|
97
+ FastlaneCore::UI.message("Single try testing for device '#{device_name}'") if device_name
98
+ test_results << run_tests_through_single_try
99
+ end
95
100
  end
96
101
 
97
- unless tests_passed || @options[:try_count] < 1
102
+ unless test_results.all? || @options[:try_count] < 1
103
+ test_results.clear
98
104
  setup_testcollector
99
- tests_passed = run_test_batches
105
+ setup_run_tests_for_each_device do |device_name|
106
+ FastlaneCore::UI.message("Testing batches for device '#{device_name}'") if device_name
107
+ test_results << run_test_batches
108
+ end
100
109
  end
101
- tests_passed
110
+ test_results.all?
111
+ end
112
+
113
+ def setup_run_tests_for_each_device
114
+ original_output_directory = @options.fetch(:output_directory, 'test_results')
115
+ unless @options[:platform] == :ios_simulator
116
+ yield
117
+ return
118
+ end
119
+
120
+ scan_destinations = Scan.config[:destination].clone
121
+ try_count = @options[:try_count]
122
+
123
+ scan_destinations.each_with_index do |destination, device_index|
124
+ @options[:try_count] = try_count
125
+ device_udid_match = destination.match(/id=(?<udid>[^,]+)/)
126
+ device_udid = device_udid_match[:udid] if device_udid_match
127
+ if scan_destinations.size > 1
128
+ @options[:output_directory] = File.join(original_output_directory, device_udid)
129
+ Scan.config[:destination].replace([destination])
130
+ end
131
+ command = "xcrun simctl list devices | grep #{device_udid}"
132
+ device_info = Fastlane::Actions.sh(command, log: false)
133
+
134
+ yield device_info.strip.gsub(/ \(#{device_udid}.*/, '')
135
+ end
136
+ Scan.config[:destination].replace(scan_destinations)
137
+ @options[:output_directory] = original_output_directory
102
138
  end
103
139
 
104
140
  def should_run_tests_through_single_try?
@@ -196,7 +232,7 @@ module TestCenter
196
232
  pool = TestBatchWorkerPool.new(pool_options)
197
233
  pool.setup_workers
198
234
 
199
- remaining_test_batches = @test_collector.test_batches.clone
235
+ remaining_test_batches = @test_collector.batches.clone
200
236
  remaining_test_batches.each_with_index do |test_batch, current_batch_index|
201
237
  worker = pool.wait_for_worker
202
238
  FastlaneCore::UI.message("Starting test run #{current_batch_index + 1}")
@@ -209,7 +245,7 @@ module TestCenter
209
245
  end
210
246
 
211
247
  def scan_options_for_worker(test_batch, batch_index)
212
- if @test_collector.test_batches.size > 1
248
+ if @test_collector.batches.size > 1
213
249
  # If there are more than 1 batch, then we want each batch result
214
250
  # sent to a "batch index" output folder to be collated later
215
251
  # into the requested output_folder.
@@ -63,7 +63,9 @@ module TestCenter
63
63
  def parallel_scan_options(worker_index)
64
64
  options = @options.reject { |key| %i[device devices].include?(key) }
65
65
  options[:destination] = destination_for_worker(worker_index)
66
- options[:scan_devices_override] = simulator_devices_for_worker(worker_index)
66
+ if @options[:platform] == :ios_simulator
67
+ options[:scan_devices_override] = simulator_devices_for_worker(worker_index)
68
+ end
67
69
  options[:buildlog_path] = buildlog_path_for_worker(worker_index) if @options[:buildlog_path]
68
70
  options[:derived_data_path] = derived_data_path_for_worker(worker_index)
69
71
  options[:batch_index] = worker_index
@@ -89,6 +91,12 @@ module TestCenter
89
91
  clones.flatten.each(&:delete)
90
92
  end
91
93
 
94
+ def shutdown_cloned_simulators(clones)
95
+ return if clones.nil?
96
+
97
+ clones.flatten.each(&:shutdown)
98
+ end
99
+
92
100
  def setup_serial_workers
93
101
  serial_scan_options = @options.reject { |key| %i[device devices].include?(key) }
94
102
  serial_scan_options[:destination] ||= Scan&.config&.fetch(:destination)
@@ -125,6 +133,7 @@ module TestCenter
125
133
  busy_workers.map(&:pid).each do |pid|
126
134
  Process.wait(pid)
127
135
  end
136
+ shutdown_cloned_simulators(@clones)
128
137
  busy_workers.each { |w| w.process_results }
129
138
  end
130
139
  end
@@ -3,34 +3,154 @@ module TestCenter
3
3
  require 'fastlane_core/ui/ui.rb'
4
4
  require 'fastlane/actions/scan'
5
5
  require 'plist'
6
+ require 'set'
6
7
 
7
8
  class TestCollector
8
9
  attr_reader :xctestrun_path
9
- attr_reader :only_testing
10
+ attr_reader :batches
11
+ attr_reader :testables
10
12
 
11
13
  def initialize(options)
12
- unless options[:xctestrun] || options[:derived_data_path]
13
- options[:derived_data_path] = default_derived_data_path(options)
14
+ @invocation_based_tests = options[:invocation_based_tests]
15
+ @swift_test_prefix = options[:swift_test_prefix]
16
+
17
+ @xctestrun_path = self.class.xctestrun_filepath(options)
18
+ initialize_batches(options)
19
+ end
20
+
21
+ private
22
+
23
+ def initialize_batches(options)
24
+ if options[:batches]
25
+ expand_given_batches_to_full_test_identifiers(options)
26
+ else
27
+ derive_batches_from_tests(options)
14
28
  end
15
- @xctestrun_path = options[:xctestrun] || derived_testrun_path(options[:derived_data_path], options[:scheme])
16
- unless @xctestrun_path && File.exist?(@xctestrun_path)
17
- FastlaneCore::UI.user_error!("Error: cannot find xctestrun file '#{@xctestrun_path}'")
29
+ end
30
+
31
+ def expand_given_batches_to_full_test_identifiers(options)
32
+ @batches = options[:batches]
33
+ testables = Set.new
34
+ @batches.each do |batch|
35
+ expand_test_identifiers(batch)
36
+ batch.each { |t| testables << t.split('/')[0] }
18
37
  end
19
- @only_testing = options[:only_testing] || only_testing_from_testplan(options)
20
- if @only_testing.kind_of?(String)
21
- @only_testing = @only_testing.split(',')
38
+ @testables = testables.to_a
39
+ end
40
+
41
+ def derive_batch_count(options)
42
+ batch_count = options.fetch(:batch_count, 1)
43
+ if batch_count == 1 && options.fetch(:parallel_testrun_count, 0) > 1
44
+ # if the batch count is 1, and the users wants parallel runs
45
+ # we *must* set the batch count to the same number of parallel
46
+ # runs or else the desired reports will not be written
47
+ batch_count = options[:parallel_testrun_count]
22
48
  end
23
- @skip_testing = options[:skip_testing]
24
- @invocation_based_tests = options[:invocation_based_tests]
25
- @batch_count = options[:batch_count]
26
- if @batch_count == 1 && options[:parallel_testrun_count] > 1
27
- @batch_count = options[:parallel_testrun_count]
49
+ batch_count
50
+ end
51
+
52
+ def derive_only_testing(options)
53
+ only_testing = options[:only_testing] || self.class.only_testing_from_testplan(options)
54
+ if only_testing && only_testing.kind_of?(String)
55
+ only_testing = only_testing.split(',').map(&:strip)
28
56
  end
57
+ only_testing
58
+ end
29
59
 
30
- @swift_test_prefix = options[:swift_test_prefix]
60
+ def testable_tests_hash_from_options(options)
61
+ testable_tests_hash = Hash.new { |h, k| h[k] = [] }
62
+ only_testing = derive_only_testing(options)
63
+ if only_testing
64
+ expand_test_identifiers(only_testing)
65
+ only_testing.each do |test_identifier|
66
+ testable = test_identifier.split('/')[0]
67
+ testable_tests_hash[testable] << test_identifier
68
+ end
69
+ else
70
+ testable_tests_hash = xctestrun_known_tests.clone
71
+ if options[:skip_testing]
72
+ expand_test_identifiers(options[:skip_testing])
73
+ testable_tests_hash.each do |testable, test_identifiers|
74
+ test_identifiers.replace(test_identifiers - options[:skip_testing])
75
+ testable_tests_hash.delete(testable) if test_identifiers.empty?
76
+ end
77
+ end
78
+ end
79
+ testable_tests_hash
80
+ end
81
+
82
+ def derive_batches_from_tests(options)
83
+ @batches = []
84
+ testable_tests_hash = testable_tests_hash_from_options(options)
85
+ @testables = testable_tests_hash.keys
86
+ batch_count = derive_batch_count(options)
87
+ testable_tests_hash.each do |testable, test_identifiers|
88
+ next if test_identifiers.empty?
89
+
90
+ if batch_count > 1
91
+ slice_count = [(test_identifiers.length / batch_count.to_f).ceil, 1].max
92
+ test_identifiers.each_slice(slice_count).to_a.each do |batch|
93
+ @batches << batch
94
+ end
95
+ else
96
+ @batches << test_identifiers
97
+ end
98
+ end
99
+ end
100
+
101
+ def expand_test_identifiers(test_identifiers)
102
+ all_known_tests = nil
103
+ test_identifiers.each_with_index do |test_identifier, index|
104
+ test_components = test_identifier.split('/')
105
+ is_full_test_identifier = (test_components.size == 3)
106
+ next if is_full_test_identifier
107
+
108
+ all_known_tests ||= xctestrun_known_tests.clone
109
+
110
+ testsuite = ''
111
+ testable = test_components[0]
112
+ expanded_test_identifiers = []
113
+ if test_components.size == 1
114
+ # this is a testable, also known as a test target. Let's expand it out
115
+ # to all of its tests. Note: a test target can have many testSuites, each
116
+ # with their own testCases.
117
+ if all_known_tests[testable].to_a.empty?
118
+ FastlaneCore::UI.verbose("Unable to expand #{testable} to constituent tests")
119
+ expanded_test_identifiers = [testable]
120
+ else
121
+ expanded_test_identifiers = all_known_tests[testable]
122
+ end
123
+ else
124
+ # this is a testable and a test suite, let's expand it out to all of
125
+ # its testCases. Note: if the user put the same test identifier in more than
126
+ # one place in this array, this could lead to multiple repititions of the same
127
+ # set of test identifiers
128
+ testsuite = test_components[1]
129
+ expanded_test_identifiers = all_known_tests[testable].select do |known_test|
130
+ known_test.split('/')[1] == testsuite
131
+ end
132
+ end
133
+ test_identifiers.delete_at(index)
134
+ test_identifiers.insert(index, *expanded_test_identifiers)
135
+ end
136
+ end
137
+
138
+ def xctestrun_known_tests
139
+ unless @known_tests
140
+ config = FastlaneCore::Configuration.create(
141
+ ::Fastlane::Actions::TestsFromXctestrunAction.available_options,
142
+ {
143
+ xctestrun: @xctestrun_path,
144
+ invocation_based_tests: @invocation_based_tests,
145
+ swift_test_prefix: @swift_test_prefix
146
+ }
147
+ )
148
+ @known_tests = ::Fastlane::Actions::TestsFromXctestrunAction.run(config)
149
+ end
150
+ @known_tests
31
151
  end
32
152
 
33
- def only_testing_from_testplan(options)
153
+ def self.only_testing_from_testplan(options)
34
154
  return unless options[:testplan] && options[:scheme]
35
155
 
36
156
  config = FastlaneCore::Configuration.create(
@@ -60,153 +180,26 @@ module TestCenter
60
180
  return test_options[:only_testing]
61
181
  end
62
182
 
63
- def default_derived_data_path(options)
183
+ def self.default_derived_data_path
64
184
  project_derived_data_path = Scan.project.build_settings(key: "BUILT_PRODUCTS_DIR")
65
185
  File.expand_path("../../..", project_derived_data_path)
66
186
  end
67
187
 
68
- def derived_testrun_path(derived_data_path, scheme)
188
+ def self.derived_testrun_path(derived_data_path)
69
189
  xctestrun_files = Dir.glob("#{derived_data_path}/Build/Products/*.xctestrun")
70
190
  xctestrun_files.sort { |f1, f2| File.mtime(f1) <=> File.mtime(f2) }.last
71
191
  end
72
192
 
73
- def testables
74
- unless @testables
75
- if @only_testing
76
- @testables ||= only_testing_to_testables_tests.keys
77
- else
78
- @testables = xctestrun_known_tests.keys
79
- end
80
- end
81
- @testables
82
- end
83
-
84
- def only_testing_to_testables_tests
85
- tests = Hash.new { |h, k| h[k] = [] }
86
- @only_testing.sort.each do |test_identifier|
87
- testable = test_identifier.split('/', 2)[0]
88
- tests[testable] << test_identifier
89
- end
90
- tests
91
- end
92
-
93
- def xctestrun_known_tests
94
- unless @known_tests
95
- config = FastlaneCore::Configuration.create(
96
- ::Fastlane::Actions::TestsFromXctestrunAction.available_options,
97
- {
98
- xctestrun: @xctestrun_path,
99
- invocation_based_tests: @invocation_based_tests,
100
- swift_test_prefix: @swift_test_prefix
101
- }
102
- )
103
- @known_tests = ::Fastlane::Actions::TestsFromXctestrunAction.run(config)
104
- end
105
- @known_tests
106
- end
107
-
108
- def expand_short_testidentifiers_to_tests(testables_tests)
109
- # Remember, testable_tests is of the format:
110
- # {
111
- # 'testable1' => [
112
- # 'testsuite1/testcase1',
113
- # 'testsuite1/testcase2',
114
- # 'testsuite2/testcase1',
115
- # 'testsuite2/testcase2',
116
- # ...
117
- # 'testsuiteN/testcase1', ... 'testsuiteN/testcaseM'
118
- # ],
119
- # ...
120
- # 'testableO' => [
121
- # 'testsuite1/testcase1',
122
- # 'testsuite1/testcase2',
123
- # 'testsuite2/testcase1',
124
- # 'testsuite2/testcase2',
125
- # ...
126
- # 'testsuiteN/testcase1', ... 'testsuiteN/testcaseM'
127
- # ]
128
- # }
129
- return if @invocation_based_tests
130
-
131
- # iterate among all the test identifers for each testable
132
- # A test identifier is seperated into components by '/'
133
- # if a test identifier has only 2 separators, it probably is
134
- # 'testable/testsuite' (but it could be 'testsuite/testcase' )
135
- all_known_tests = nil
136
- known_tests = []
137
- testables_tests.each do |testable, tests|
138
- tests.each_with_index do |test, index|
139
- test_components = test.split('/')
140
- is_full_test_identifier = (test_components.size == 3)
141
- next if is_full_test_identifier
142
-
143
- all_known_tests ||= xctestrun_known_tests.clone
144
-
145
- testsuite = ''
146
- if test_components.size == 1
147
- if test_components[0] == testable
148
- # The following || [] is just in case the user provided multiple
149
- # test targets or there are no actual tests found.
150
- testables_tests[testable][index] = all_known_tests[testable] || []
151
- all_known_tests.delete(testable)
152
- next
153
- end
154
-
155
- testsuite = test_components[0]
156
- else
157
- testsuite = test_components[1]
158
- end
159
-
160
- testables_tests[testable][index], all_known_tests[testable] = all_known_tests[testable].partition do |known_test|
161
- known_test.split('/')[1] == testsuite
162
- end
163
- end
164
- testables_tests[testable].flatten!
165
- end
166
- end
167
-
168
- def testables_tests
169
- unless @testables_tests
170
- if @only_testing
171
- @testables_tests = only_testing_to_testables_tests
172
- expand_short_testidentifiers_to_tests(@testables_tests)
173
- else
174
- @testables_tests = xctestrun_known_tests
175
- if @skip_testing
176
- skipped_testable_tests = Hash.new { |h, k| h[k] = [] }
177
- @skip_testing.sort.each do |skipped_test_identifier|
178
- testable = skipped_test_identifier.split('/', 2)[0]
179
- skipped_testable_tests[testable] << skipped_test_identifier
180
- end
181
- @testables_tests.each_key do |testable|
182
- @testables_tests[testable] -= skipped_testable_tests[testable]
183
- end
184
- end
185
- end
193
+ def self.xctestrun_filepath(options)
194
+ unless options[:xctestrun] || options[:derived_data_path]
195
+ options[:derived_data_path] = default_derived_data_path
186
196
  end
197
+ path = (options[:xctestrun] || derived_testrun_path(options[:derived_data_path]))
187
198
 
188
- @testables_tests
189
- end
190
-
191
- def test_batches
192
- if @batches.nil?
193
- @batches = []
194
- testables.each do |testable|
195
- testable_tests = testables_tests[testable]
196
- next if testable_tests.empty?
197
-
198
- if @batch_count > 1
199
- slice_count = [(testable_tests.length / @batch_count.to_f).ceil, 1].max
200
- testable_tests.each_slice(slice_count).to_a.each do |tests_batch|
201
- @batches << tests_batch
202
- end
203
- else
204
- @batches << testable_tests
205
- end
206
- end
199
+ unless path && File.exist?(path)
200
+ FastlaneCore::UI.user_error!("Error: cannot find xctestrun file '#{path}'")
207
201
  end
208
-
209
- @batches
202
+ path
210
203
  end
211
204
  end
212
205
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module TestCenter
3
- VERSION = "3.14.3"
3
+ VERSION = "3.14.8"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-test_center
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.14.3
4
+ version: 3.14.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lyndsey Ferguson
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-15 00:00:00.000000000 Z
11
+ date: 2020-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -310,7 +310,7 @@ homepage: https://github.com/lyndsey-ferguson/fastlane-plugin-test_center
310
310
  licenses:
311
311
  - MIT
312
312
  metadata: {}
313
- post_install_message:
313
+ post_install_message:
314
314
  rdoc_options: []
315
315
  require_paths:
316
316
  - lib
@@ -326,7 +326,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
326
326
  version: '0'
327
327
  requirements: []
328
328
  rubygems_version: 3.1.2
329
- signing_key:
329
+ signing_key:
330
330
  specification_version: 4
331
331
  summary: Makes testing your iOS app easier
332
332
  test_files: []