xcknife 0.6.6 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|