xcknife 0.6.6 → 0.13.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 +5 -5
- data/.github/workflows/tests.yml +24 -0
- data/.gitignore +4 -1
- data/.rubocop.yml +168 -0
- data/.ruby-version +1 -1
- data/.vscode/configure.sh +23 -0
- data/.vscode/vscode_ruby.json.template +44 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +63 -0
- data/OWNERS.yml +2 -0
- data/README.md +9 -1
- data/Rakefile +16 -11
- data/TestDumper/README.md +2 -1
- data/TestDumper/TestDumper.xcodeproj/project.pbxproj +27 -5
- data/TestDumper/TestDumper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- data/TestDumper/TestDumper.xcodeproj/xcshareddata/xcschemes/TestDumper.xcscheme +1 -1
- data/TestDumper/TestDumper/Initialize.m +83 -44
- data/bin/xcknife +2 -0
- data/bin/xcknife-min +12 -15
- data/bin/xcknife-test-dumper +2 -0
- data/example/run_example.rb +10 -8
- data/example/xcknife-exemplar-historical-data.json-stream +3 -3
- data/example/xcknife-exemplar.json-stream +3 -3
- data/lib/xcknife.rb +3 -1
- data/lib/xcknife/events_analyzer.rb +11 -6
- data/lib/xcknife/exceptions.rb +6 -2
- data/lib/xcknife/json_stream_parser_helper.rb +8 -8
- data/lib/xcknife/runner.rb +18 -19
- data/lib/xcknife/stream_parser.rb +84 -37
- data/lib/xcknife/test_dumper.rb +283 -112
- data/lib/xcknife/xcscheme_analyzer.rb +9 -9
- data/lib/xcknife/xctool_cmd_helper.rb +28 -4
- data/xcknife.gemspec +8 -9
- metadata +15 -12
- data/.gitmodules +0 -3
- data/.travis.yml +0 -18
@@ -1,15 +1,15 @@
|
|
1
|
-
{"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"
|
1
|
+
{"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"iphonesimulator13.6"}
|
2
2
|
{"test":"1","className":"iPadTestClassFour","event":"end-test","totalDuration":"0"}
|
3
3
|
{"test":"1","className":"iPadTestClassOne","event":"end-test","totalDuration":"0"}
|
4
4
|
{"test":"1","className":"iPadTestClassThree","event":"end-test","totalDuration":"0"}
|
5
5
|
{"test":"1","className":"iPadTestClassTwo","event":"end-test","totalDuration":"0"}
|
6
|
-
{"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"
|
6
|
+
{"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"iphonesimulator13.6"}
|
7
7
|
{"test":"1","className":"iPhoneTestClassAlpha","event":"end-test","totalDuration":"0"}
|
8
8
|
{"test":"1","className":"iPhoneTestClassBeta","event":"end-test","totalDuration":"0"}
|
9
9
|
{"test":"1","className":"iPhoneTestClassDelta","event":"end-test","totalDuration":"0"}
|
10
10
|
{"test":"1","className":"iPhoneTestClassGama","event":"end-test","totalDuration":"0"}
|
11
11
|
{"test":"1","className":"iPhoneTestClassOmega","event":"end-test","totalDuration":"0"}
|
12
|
-
{"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"
|
12
|
+
{"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"iphonesimulator13.6"}
|
13
13
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
|
14
14
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
|
15
15
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
|
data/lib/xcknife.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'xcknife/events_analyzer'
|
2
4
|
require 'xcknife/stream_parser'
|
3
5
|
require 'xcknife/xctool_cmd_helper'
|
@@ -7,5 +9,5 @@ require 'xcknife/exceptions'
|
|
7
9
|
require 'xcknife/xcscheme_analyzer'
|
8
10
|
|
9
11
|
module XCKnife
|
10
|
-
VERSION = '0.
|
12
|
+
VERSION = '0.13.0'
|
11
13
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'xcknife/json_stream_parser_helper'
|
2
4
|
require 'set'
|
3
5
|
|
@@ -8,6 +10,7 @@ module XCKnife
|
|
8
10
|
|
9
11
|
def self.for(events, relevant_partitions)
|
10
12
|
return NullEventsAnalyzer.new if events.nil?
|
13
|
+
|
11
14
|
new(events, relevant_partitions)
|
12
15
|
end
|
13
16
|
|
@@ -17,19 +20,21 @@ module XCKnife
|
|
17
20
|
@target_class_map = analyze_events(events)
|
18
21
|
end
|
19
22
|
|
20
|
-
def
|
21
|
-
target_class_map.
|
23
|
+
def test_target?(target)
|
24
|
+
target_class_map.key?(target)
|
22
25
|
end
|
23
26
|
|
24
|
-
def
|
25
|
-
|
27
|
+
def test_class?(target, clazz)
|
28
|
+
test_target?(target) and target_class_map[target].include?(clazz)
|
26
29
|
end
|
27
30
|
|
28
31
|
private
|
32
|
+
|
29
33
|
def analyze_events(events)
|
30
34
|
ret = Hash.new { |h, key| h[key] = Set.new }
|
31
35
|
each_test_event(events) do |target_name, result|
|
32
36
|
next unless @relevant_partitions.include?(target_name)
|
37
|
+
|
33
38
|
@total_tests += 1
|
34
39
|
ret[target_name] << result.className
|
35
40
|
end
|
@@ -40,11 +45,11 @@ module XCKnife
|
|
40
45
|
# Null object for EventsAnalyzer
|
41
46
|
# @ref https://en.wikipedia.org/wiki/Null_Object_pattern
|
42
47
|
class NullEventsAnalyzer
|
43
|
-
def
|
48
|
+
def test_target?(_target)
|
44
49
|
true
|
45
50
|
end
|
46
51
|
|
47
|
-
def
|
52
|
+
def test_class?(_target, _clazz)
|
48
53
|
true
|
49
54
|
end
|
50
55
|
|
data/lib/xcknife/exceptions.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module XCKnife
|
2
4
|
# Base error class for xcknife
|
3
|
-
XCKnifeError = Class.new(
|
5
|
+
XCKnifeError = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
TestDumpError = Class.new(XCKnifeError)
|
4
8
|
|
5
9
|
StreamParsingError = Class.new(XCKnifeError)
|
6
|
-
end
|
10
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module XCKnife
|
2
4
|
module JsonStreamParserHelper
|
3
5
|
extend self
|
@@ -6,18 +8,16 @@ module XCKnife
|
|
6
8
|
def each_test_event(events, &block)
|
7
9
|
current_target = nil
|
8
10
|
events.each do |result|
|
9
|
-
current_target = result.targetName if result.event ==
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
current_target = result.targetName if result.event == 'begin-ocunit'
|
12
|
+
next unless result.test && (result.event == 'end-test')
|
13
|
+
raise XCKnife::StreamParsingError, 'No test target defined' if current_target.nil?
|
14
|
+
|
15
|
+
block.call(current_target, normalize_result(result))
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
19
|
def normalize_result(result)
|
18
|
-
if result.totalDuration.is_a?(String)
|
19
|
-
result.totalDuration = result.totalDuration.to_f
|
20
|
-
end
|
20
|
+
result.totalDuration = result.totalDuration.to_f if result.totalDuration.is_a?(String)
|
21
21
|
result
|
22
22
|
end
|
23
23
|
end
|
data/lib/xcknife/runner.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
|
3
5
|
module XCKnife
|
@@ -33,11 +35,11 @@ module XCKnife
|
|
33
35
|
end
|
34
36
|
|
35
37
|
private
|
38
|
+
|
36
39
|
def gen_abbreviated_output(result)
|
37
40
|
result.test_maps.map { |partition_set| only_arguments_for_a_partition_set(partition_set, output_type) }
|
38
41
|
end
|
39
42
|
|
40
|
-
|
41
43
|
def output_type
|
42
44
|
@xcodebuild_output ? :xcodebuild : :xctool
|
43
45
|
end
|
@@ -47,7 +49,7 @@ module XCKnife
|
|
47
49
|
metadata: {
|
48
50
|
worker_count: @worker_count,
|
49
51
|
partition_set_count: result.test_maps.size,
|
50
|
-
total_time_in_ms: result.total_test_time
|
52
|
+
total_time_in_ms: result.total_test_time
|
51
53
|
}.merge(result.stats.to_h),
|
52
54
|
partition_set_data: partition_sets_data(result)
|
53
55
|
}
|
@@ -81,18 +83,15 @@ module XCKnife
|
|
81
83
|
def write_output(data)
|
82
84
|
json = JSON.pretty_generate(data)
|
83
85
|
return puts json if @output_file_name.nil?
|
84
|
-
|
86
|
+
|
87
|
+
File.open(@output_file_name, 'w') { |f| f.puts(json) }
|
85
88
|
puts "Wrote file to: #{@output_file_name}"
|
86
89
|
end
|
87
90
|
|
88
91
|
def parse_arguments(args)
|
89
92
|
positional_arguments = parse_options(args)
|
90
|
-
if positional_arguments.size < required_arguments.size
|
91
|
-
|
92
|
-
end
|
93
|
-
if @partitions.empty?
|
94
|
-
warn_and_exit("At least one target partition set must be provided with -p flag")
|
95
|
-
end
|
93
|
+
warn_and_exit("You must specify *all* required arguments: #{required_arguments.join(', ')}") if positional_arguments.size < required_arguments.size
|
94
|
+
warn_and_exit('At least one target partition set must be provided with -p flag') if @partitions.empty?
|
96
95
|
worker_count, @historical_timings_file, @current_tests_file = positional_arguments
|
97
96
|
@worker_count = Integer(worker_count)
|
98
97
|
end
|
@@ -101,24 +100,24 @@ module XCKnife
|
|
101
100
|
build_parser
|
102
101
|
begin
|
103
102
|
parser.parse(args)
|
104
|
-
rescue OptionParser::ParseError =>
|
105
|
-
warn_and_exit(
|
103
|
+
rescue OptionParser::ParseError => e
|
104
|
+
warn_and_exit(e)
|
106
105
|
end
|
107
106
|
end
|
108
107
|
|
109
108
|
def build_parser
|
110
109
|
@parser = OptionParser.new do |opts|
|
111
110
|
opts.banner += " #{arguments_banner}"
|
112
|
-
opts.on(
|
113
|
-
|
111
|
+
opts.on('-p', '--partition TARGETS',
|
112
|
+
'Comma separated list of targets. Can be used multiple times.') do |v|
|
114
113
|
@partition_names << v
|
115
|
-
@partitions << v.split(
|
114
|
+
@partitions << v.split(',')
|
116
115
|
end
|
117
|
-
opts.on(
|
118
|
-
opts.on(
|
119
|
-
opts.on(
|
116
|
+
opts.on('-o', '--output FILENAME', 'Output file. Defaults to STDOUT') { |v| @output_file_name = v }
|
117
|
+
opts.on('-a', '--abbrev', 'Results are abbreviated') { |v| @abbreviated_output = v }
|
118
|
+
opts.on('-x', '--xcodebuild-output', 'Output is formatted for xcodebuild') { |v| @xcodebuild_output = v }
|
120
119
|
|
121
|
-
opts.on_tail(
|
120
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
122
121
|
puts opts
|
123
122
|
exit
|
124
123
|
end
|
@@ -135,7 +134,7 @@ module XCKnife
|
|
135
134
|
|
136
135
|
def arguments_banner
|
137
136
|
optional_args = optional_arguments.map { |a| "[#{a}]" }
|
138
|
-
(required_arguments + optional_args).join(
|
137
|
+
(required_arguments + optional_args).join(' ')
|
139
138
|
end
|
140
139
|
|
141
140
|
def warn_and_exit(msg)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'xcknife/json_stream_parser_helper'
|
2
4
|
require 'json'
|
3
5
|
require 'set'
|
@@ -10,21 +12,31 @@ module XCKnife
|
|
10
12
|
|
11
13
|
attr_reader :number_of_shards, :test_partitions, :stats, :relevant_partitions
|
12
14
|
|
13
|
-
def initialize(number_of_shards, test_partitions)
|
15
|
+
def initialize(number_of_shards, test_partitions, options_for_metapartition: Array.new(test_partitions.size, {}), allow_fewer_shards: false, on_extrapolation: nil)
|
14
16
|
@number_of_shards = number_of_shards
|
15
17
|
@test_partitions = test_partitions.map(&:to_set)
|
16
18
|
@relevant_partitions = test_partitions.flatten.to_set
|
17
19
|
@stats = ResultStats.new
|
18
|
-
|
20
|
+
@options_for_metapartition = options_for_metapartition.map { |o| Options::DEFAULT.merge(o) }
|
21
|
+
@allow_fewer_shards = allow_fewer_shards
|
22
|
+
@on_extrapolation = on_extrapolation
|
23
|
+
ResultStats.members.each { |k| @stats[k] = 0 }
|
19
24
|
end
|
20
25
|
|
21
|
-
PartitionWithMachines = Struct.new :test_time_map, :number_of_shards, :partition_time, :max_shard_count
|
26
|
+
PartitionWithMachines = Struct.new :test_time_map, :number_of_shards, :partition_time, :max_shard_count, :options
|
22
27
|
MachineAssignment = Struct.new :test_time_map, :total_time
|
23
28
|
ResultStats = Struct.new :historical_total_tests, :current_total_tests, :class_extrapolations, :target_extrapolations
|
29
|
+
Options = Struct.new :max_shard_count, :split_bundles_across_machines, :allow_fewer_shards do
|
30
|
+
def merge(hash)
|
31
|
+
self.class.new(*to_h.merge(hash).values_at(*members))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Options::DEFAULT = Options.new(nil, true, false).freeze
|
24
35
|
|
25
36
|
class PartitionResult
|
26
37
|
TimeImbalances = Struct.new :partition_set, :partitions
|
27
38
|
attr_reader :stats, :test_maps, :test_times, :total_test_time, :test_time_imbalances, :test_time_for_partitions
|
39
|
+
|
28
40
|
extend Forwardable
|
29
41
|
delegate ResultStats.members => :@stats
|
30
42
|
|
@@ -39,10 +51,11 @@ module XCKnife
|
|
39
51
|
end
|
40
52
|
|
41
53
|
private
|
54
|
+
|
42
55
|
# Yields the imbalances ratios of the partition sets, and the internal imbalance ratio of the respective partitions
|
43
56
|
def compute_test_time_imbalances
|
44
57
|
times = test_times
|
45
|
-
average_partition_size = times.map { |l| l.inject(:+).to_f / l.size}
|
58
|
+
average_partition_size = times.map { |l| l.inject(:+).to_f / l.size }
|
46
59
|
ideal_partition_set_avg = average_partition_size.inject(:+) / @partition_sets.size
|
47
60
|
partition_set_imbalance = average_partition_size.map { |avg| avg / ideal_partition_set_avg }
|
48
61
|
|
@@ -75,18 +88,20 @@ module XCKnife
|
|
75
88
|
|
76
89
|
def compute_shards_for_partitions(test_time_for_partitions)
|
77
90
|
PartitionResult.new(@stats, split_machines_proportionally(test_time_for_partitions).map do |partition|
|
78
|
-
compute_single_shards(partition.number_of_shards, partition.test_time_map)
|
91
|
+
compute_single_shards(partition.number_of_shards, partition.test_time_map, options: partition.options)
|
79
92
|
end, test_time_for_partitions)
|
80
93
|
end
|
81
94
|
|
82
95
|
def test_time_for_partitions(historical_events, current_events = nil)
|
83
|
-
analyzer =
|
96
|
+
analyzer = EventsAnalyzer.for(current_events, relevant_partitions)
|
84
97
|
@stats[:current_total_tests] = analyzer.total_tests
|
85
98
|
times_for_target_class = Hash.new { |h, current_target| h[current_target] = Hash.new(0) }
|
86
99
|
each_test_event(historical_events) do |target_name, result|
|
87
100
|
next unless relevant_partitions.include?(target_name)
|
101
|
+
|
88
102
|
inc_stat :historical_total_tests
|
89
|
-
next unless analyzer.
|
103
|
+
next unless analyzer.test_class?(target_name, result.className)
|
104
|
+
|
90
105
|
times_for_target_class[target_name][result.className] += (result.totalDuration * 1000).ceil
|
91
106
|
end
|
92
107
|
|
@@ -94,43 +109,47 @@ module XCKnife
|
|
94
109
|
hash_partitions(times_for_target_class)
|
95
110
|
end
|
96
111
|
|
112
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
97
113
|
def split_machines_proportionally(partitions)
|
98
114
|
total = 0
|
99
115
|
partitions.each do |test_time_map|
|
100
|
-
each_duration(test_time_map) { |duration_in_milliseconds| total += duration_in_milliseconds}
|
116
|
+
each_duration(test_time_map) { |duration_in_milliseconds| total += duration_in_milliseconds }
|
101
117
|
end
|
102
118
|
|
103
119
|
used_shards = 0
|
104
120
|
assignable_shards = number_of_shards - partitions.size
|
105
|
-
partition_with_machines_list = partitions.map do |test_time_map|
|
121
|
+
partition_with_machines_list = partitions.each_with_index.map do |test_time_map, metapartition|
|
122
|
+
options = @options_for_metapartition[metapartition]
|
106
123
|
partition_time = 0
|
107
|
-
max_shard_count = test_time_map.
|
108
|
-
|
124
|
+
max_shard_count = test_time_map.each_value.map(&:size).reduce(&:+) || 1
|
125
|
+
max_shard_count = [max_shard_count, options.max_shard_count].min if options.max_shard_count
|
126
|
+
each_duration(test_time_map) { |duration_in_milliseconds| partition_time += duration_in_milliseconds }
|
109
127
|
n = [1 + (assignable_shards * partition_time.to_f / total).floor, max_shard_count].min
|
110
128
|
used_shards += n
|
111
|
-
PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count)
|
129
|
+
PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count, options)
|
112
130
|
end
|
113
131
|
|
114
|
-
fifo_with_machines_who_can_use_more_shards = partition_with_machines_list.select { |x| x.number_of_shards < x.max_shard_count}.sort_by(&:partition_time)
|
115
|
-
while
|
132
|
+
fifo_with_machines_who_can_use_more_shards = partition_with_machines_list.select { |x| x.number_of_shards < x.max_shard_count }.sort_by(&:partition_time)
|
133
|
+
while number_of_shards > used_shards
|
116
134
|
if fifo_with_machines_who_can_use_more_shards.empty?
|
135
|
+
break if @allow_fewer_shards
|
136
|
+
|
117
137
|
raise XCKnife::XCKnifeError, "There are #{number_of_shards - used_shards} extra machines"
|
118
138
|
end
|
119
139
|
machine = fifo_with_machines_who_can_use_more_shards.pop
|
120
140
|
machine.number_of_shards += 1
|
121
141
|
used_shards += 1
|
122
|
-
if machine.number_of_shards < machine.max_shard_count
|
123
|
-
fifo_with_machines_who_can_use_more_shards.unshift(machine)
|
124
|
-
end
|
142
|
+
fifo_with_machines_who_can_use_more_shards.unshift(machine) if machine.number_of_shards < machine.max_shard_count
|
125
143
|
end
|
126
144
|
partition_with_machines_list
|
127
145
|
end
|
128
146
|
|
129
147
|
# Computes a 2-aproximation to the optimal partition_time, which is an instance of the Open shop scheduling problem (which is NP-hard)
|
130
148
|
# see: https://en.wikipedia.org/wiki/Open-shop_scheduling
|
131
|
-
def compute_single_shards(number_of_shards, test_time_map)
|
132
|
-
raise XCKnife::XCKnifeError,
|
133
|
-
raise XCKnife::XCKnifeError,
|
149
|
+
def compute_single_shards(number_of_shards, test_time_map, options: Options::DEFAULT)
|
150
|
+
raise XCKnife::XCKnifeError, 'There are not enough workers provided' if number_of_shards <= 0
|
151
|
+
raise XCKnife::XCKnifeError, 'Cannot shard an empty partition_time' if test_time_map.empty?
|
152
|
+
|
134
153
|
assignements = Array.new(number_of_shards) { MachineAssignment.new(Hash.new { |k, v| k[v] = [] }, 0) }
|
135
154
|
|
136
155
|
list_of_test_target_class_times = []
|
@@ -140,36 +159,61 @@ module XCKnife
|
|
140
159
|
end
|
141
160
|
end
|
142
161
|
|
143
|
-
|
144
|
-
|
162
|
+
# This might seem like an uncessary level of indirection, but it allows us to keep
|
163
|
+
# logic consistent regardless of the `split_bundles_across_machines` option
|
164
|
+
group = list_of_test_target_class_times.group_by do |test_target, class_name, _duration_in_milliseconds|
|
165
|
+
options.split_bundles_across_machines ? [test_target, class_name] : test_target
|
166
|
+
end
|
167
|
+
|
168
|
+
list_of_test_target_classes_times = group.map do |(test_target, _), classes|
|
169
|
+
[
|
170
|
+
test_target,
|
171
|
+
classes.map { |_test_target, class_name, _duration_in_milliseconds| class_name },
|
172
|
+
classes.reduce(0) { |total_duration, (_test_target, _class_name, duration_in_milliseconds)| total_duration + duration_in_milliseconds }
|
173
|
+
]
|
174
|
+
end
|
175
|
+
|
176
|
+
list_of_test_target_classes_times.sort_by! { |_test_target, _class_names, duration_in_milliseconds| -duration_in_milliseconds }
|
177
|
+
list_of_test_target_classes_times.each do |test_target, class_names, duration_in_milliseconds|
|
145
178
|
assignemnt = assignements.min_by(&:total_time)
|
146
|
-
assignemnt.test_time_map[test_target]
|
179
|
+
assignemnt.test_time_map[test_target].concat class_names
|
147
180
|
assignemnt.total_time += duration_in_milliseconds
|
148
181
|
end
|
149
|
-
|
182
|
+
|
183
|
+
if (empty_test_map_assignments = assignements.select { |a| a.test_time_map.empty? }) && !empty_test_map_assignments.empty? && !options.allow_fewer_shards
|
184
|
+
test_grouping = options.split_bundles_across_machines ? 'classes' : 'targets'
|
185
|
+
raise XCKnife::XCKnifeError, "Too many shards -- #{empty_test_map_assignments.size} of #{number_of_shards} assignments are empty," \
|
186
|
+
" because there are not enough test #{test_grouping} for that many shards."
|
187
|
+
end
|
188
|
+
assignements.reject! { |a| a.test_time_map.empty? }
|
189
|
+
|
150
190
|
assignements
|
151
191
|
end
|
192
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
152
193
|
|
153
194
|
def parse_json_stream_file(filename)
|
154
195
|
return nil if filename.nil?
|
155
|
-
return [] unless File.
|
196
|
+
return [] unless File.exist?(filename)
|
197
|
+
|
156
198
|
lines = IO.readlines(filename)
|
157
|
-
lines.lazy.map { |line| OpenStruct.new(JSON.
|
199
|
+
lines.lazy.map { |line| OpenStruct.new(JSON.parse(line)) }
|
158
200
|
end
|
159
201
|
|
160
202
|
private
|
203
|
+
|
161
204
|
def inc_stat(name)
|
162
205
|
@stats[name] += 1
|
163
206
|
end
|
164
207
|
|
165
|
-
def each_duration(test_time_map
|
166
|
-
test_time_map.each do |
|
167
|
-
class_times.each do |
|
208
|
+
def each_duration(test_time_map)
|
209
|
+
test_time_map.each do |_test_target, class_times|
|
210
|
+
class_times.each do |_class_name, duration_in_milliseconds|
|
168
211
|
yield(duration_in_milliseconds)
|
169
212
|
end
|
170
213
|
end
|
171
214
|
end
|
172
215
|
|
216
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
173
217
|
def extrapolate_times_for_current_events(analyzer, times_for_target_class)
|
174
218
|
median_map = {}
|
175
219
|
times_for_target_class.each do |test_target, class_times|
|
@@ -179,17 +223,20 @@ module XCKnife
|
|
179
223
|
all_times_for_all_classes = times_for_target_class.values.flat_map(&:values)
|
180
224
|
median_of_targets = median(all_times_for_all_classes)
|
181
225
|
analyzer.target_class_map.each do |test_target, class_set|
|
182
|
-
if times_for_target_class.
|
226
|
+
if times_for_target_class.key?(test_target)
|
183
227
|
class_set.each do |clazz|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
228
|
+
next if times_for_target_class[test_target].key?(clazz)
|
229
|
+
|
230
|
+
inc_stat :class_extrapolations
|
231
|
+
@on_extrapolation&.call(test_target: test_target, test_class: clazz)
|
232
|
+
times_for_target_class[test_target][clazz] = median_map[test_target]
|
188
233
|
end
|
189
234
|
else
|
190
235
|
inc_stat :target_extrapolations
|
236
|
+
@on_extrapolation&.call(test_target: test_target, test_class: nil)
|
191
237
|
class_set.each do |clazz|
|
192
238
|
inc_stat :class_extrapolations
|
239
|
+
@on_extrapolation&.call(test_target: test_target, test_class: clazz)
|
193
240
|
times_for_target_class[test_target][clazz] = extrapolated_duration(median_of_targets, class_set)
|
194
241
|
end
|
195
242
|
end
|
@@ -199,6 +246,7 @@ module XCKnife
|
|
199
246
|
DEFAULT_EXTRAPOLATED_DURATION = 1000
|
200
247
|
def extrapolated_duration(median_of_targets, class_set)
|
201
248
|
return DEFAULT_EXTRAPOLATED_DURATION if median_of_targets.nil?
|
249
|
+
|
202
250
|
median_of_targets / class_set.size
|
203
251
|
end
|
204
252
|
|
@@ -214,10 +262,9 @@ module XCKnife
|
|
214
262
|
end
|
215
263
|
end
|
216
264
|
ret.each_with_index do |partition, index|
|
217
|
-
if partition.empty?
|
218
|
-
raise XCKnife::XCKnifeError, "The following partition has no tests: #{test_partitions[index].to_a.inspect}"
|
219
|
-
end
|
265
|
+
raise XCKnife::XCKnifeError, "The following partition has no tests: #{test_partitions[index].to_a.inspect}" if partition.empty?
|
220
266
|
end
|
221
267
|
end
|
222
268
|
end
|
269
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
223
270
|
end
|