xcknife 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +168 -0
- data/.travis.yml +1 -1
- data/.vscode/configure.sh +23 -0
- data/.vscode/vscode_ruby.json.template +44 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +43 -15
- data/Rakefile +14 -10
- 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 +2 -0
- data/lib/xcknife/json_stream_parser_helper.rb +8 -8
- data/lib/xcknife/runner.rb +18 -19
- data/lib/xcknife/stream_parser.rb +39 -30
- data/lib/xcknife/test_dumper.rb +155 -121
- data/lib/xcknife/xcscheme_analyzer.rb +9 -9
- data/lib/xcknife/xctool_cmd_helper.rb +16 -4
- data/xcknife.gemspec +8 -6
- metadata +9 -6
data/bin/xcknife-test-dumper
CHANGED
data/example/run_example.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../lib/xcknife'
|
2
4
|
require 'pp'
|
3
5
|
|
4
6
|
# Gem usage of xcknife. Functionaly equivalent to
|
5
7
|
# $ xcknife -p CommonTestTarget -p CommonTestTarget,iPadTestTarget 6 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream
|
6
|
-
|
8
|
+
|
7
9
|
TARGET_PARTITIONS = {
|
8
|
-
|
9
|
-
|
10
|
-
}
|
10
|
+
'AllTests' => %w[CommonTestTarget iPadTestTarget],
|
11
|
+
'OnlyCommon' => ['CommonTestTarget']
|
12
|
+
}.freeze
|
11
13
|
|
12
14
|
def run(historical_file, current_file)
|
13
15
|
test_target_names = TARGET_PARTITIONS.keys
|
@@ -20,16 +22,16 @@ def run(historical_file, current_file)
|
|
20
22
|
puts "imbalances = #{result.test_time_imbalances.to_h.inspect}"
|
21
23
|
shard_number = 0
|
22
24
|
puts "size = #{partition_sets.size}"
|
23
|
-
puts "sizes = #{partition_sets.map(&:size).join(
|
25
|
+
puts "sizes = #{partition_sets.map(&:size).join(', ')}"
|
24
26
|
partition_sets.each_with_index do |partition_set, i|
|
25
27
|
target_name = test_target_names[i]
|
26
28
|
partition_set.each do |partition|
|
27
29
|
puts "target name for worker #{shard_number} = #{target_name}"
|
28
|
-
puts "only is: #{xctool_only_arguments(partition).inspect}"
|
29
|
-
puts "skip-only is: #{xcodebuild_skip_arguments(partition, result.test_time_for_partitions).inspect}"
|
30
|
+
puts "only is: #{XCKnife::XCToolCmdHelper.xctool_only_arguments(partition).inspect}"
|
31
|
+
puts "skip-only is: #{XCKnife::XCToolCmdHelper.xcodebuild_skip_arguments(partition, result.test_time_for_partitions).inspect}"
|
30
32
|
shard_number += 1
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
|
-
run(
|
37
|
+
run('xcknife-exemplar-historical-data.json-stream', 'xcknife-exemplar.json-stream')
|
@@ -1,12 +1,12 @@
|
|
1
|
-
{"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"
|
1
|
+
{"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"iphonesimulator13.6"}
|
2
2
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.1075069904327393}
|
3
3
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.303464949131012}
|
4
4
|
{"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.2003870010375977}
|
5
|
-
{"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"
|
5
|
+
{"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"iphonesimulator13.6"}
|
6
6
|
{"test":"1","className":"iPadTestClassOne","event":"end-test","totalDuration":1.001249969005585}
|
7
7
|
{"test":"1","className":"iPadTestClassThree","event":"end-test","totalDuration":0.5002140402793884}
|
8
8
|
{"test":"1","className":"iPadTestClassTwo","event":"end-test","totalDuration":5.001157999038696}
|
9
|
-
{"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"
|
9
|
+
{"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"iphonesimulator13.6"}
|
10
10
|
{"test":"1","className":"iPhoneTestClassAlpha","event":"end-test","totalDuration":0.2037490010261536}
|
11
11
|
{"test":"1","className":"iPhoneTestClassBeta","event":"end-test","totalDuration":0.2012439966201782}
|
12
12
|
{"test":"1","className":"iPhoneTestClassDelta","event":"end-test","totalDuration":0.1004489660263062}
|
@@ -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.12.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,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'
|
@@ -33,6 +35,7 @@ module XCKnife
|
|
33
35
|
class PartitionResult
|
34
36
|
TimeImbalances = Struct.new :partition_set, :partitions
|
35
37
|
attr_reader :stats, :test_maps, :test_times, :total_test_time, :test_time_imbalances, :test_time_for_partitions
|
38
|
+
|
36
39
|
extend Forwardable
|
37
40
|
delegate ResultStats.members => :@stats
|
38
41
|
|
@@ -47,10 +50,11 @@ module XCKnife
|
|
47
50
|
end
|
48
51
|
|
49
52
|
private
|
53
|
+
|
50
54
|
# Yields the imbalances ratios of the partition sets, and the internal imbalance ratio of the respective partitions
|
51
55
|
def compute_test_time_imbalances
|
52
56
|
times = test_times
|
53
|
-
average_partition_size = times.map { |l| l.inject(:+).to_f / l.size}
|
57
|
+
average_partition_size = times.map { |l| l.inject(:+).to_f / l.size }
|
54
58
|
ideal_partition_set_avg = average_partition_size.inject(:+) / @partition_sets.size
|
55
59
|
partition_set_imbalance = average_partition_size.map { |avg| avg / ideal_partition_set_avg }
|
56
60
|
|
@@ -88,13 +92,15 @@ module XCKnife
|
|
88
92
|
end
|
89
93
|
|
90
94
|
def test_time_for_partitions(historical_events, current_events = nil)
|
91
|
-
analyzer =
|
95
|
+
analyzer = EventsAnalyzer.for(current_events, relevant_partitions)
|
92
96
|
@stats[:current_total_tests] = analyzer.total_tests
|
93
97
|
times_for_target_class = Hash.new { |h, current_target| h[current_target] = Hash.new(0) }
|
94
98
|
each_test_event(historical_events) do |target_name, result|
|
95
99
|
next unless relevant_partitions.include?(target_name)
|
100
|
+
|
96
101
|
inc_stat :historical_total_tests
|
97
|
-
next unless analyzer.
|
102
|
+
next unless analyzer.test_class?(target_name, result.className)
|
103
|
+
|
98
104
|
times_for_target_class[target_name][result.className] += (result.totalDuration * 1000).ceil
|
99
105
|
end
|
100
106
|
|
@@ -102,10 +108,11 @@ module XCKnife
|
|
102
108
|
hash_partitions(times_for_target_class)
|
103
109
|
end
|
104
110
|
|
111
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
105
112
|
def split_machines_proportionally(partitions)
|
106
113
|
total = 0
|
107
114
|
partitions.each do |test_time_map|
|
108
|
-
each_duration(test_time_map) { |duration_in_milliseconds| total += duration_in_milliseconds}
|
115
|
+
each_duration(test_time_map) { |duration_in_milliseconds| total += duration_in_milliseconds }
|
109
116
|
end
|
110
117
|
|
111
118
|
used_shards = 0
|
@@ -121,18 +128,17 @@ module XCKnife
|
|
121
128
|
PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count, options)
|
122
129
|
end
|
123
130
|
|
124
|
-
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)
|
131
|
+
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)
|
125
132
|
while number_of_shards > used_shards
|
126
133
|
if fifo_with_machines_who_can_use_more_shards.empty?
|
127
134
|
break if @allow_fewer_shards
|
135
|
+
|
128
136
|
raise XCKnife::XCKnifeError, "There are #{number_of_shards - used_shards} extra machines"
|
129
137
|
end
|
130
138
|
machine = fifo_with_machines_who_can_use_more_shards.pop
|
131
139
|
machine.number_of_shards += 1
|
132
140
|
used_shards += 1
|
133
|
-
if machine.number_of_shards < machine.max_shard_count
|
134
|
-
fifo_with_machines_who_can_use_more_shards.unshift(machine)
|
135
|
-
end
|
141
|
+
fifo_with_machines_who_can_use_more_shards.unshift(machine) if machine.number_of_shards < machine.max_shard_count
|
136
142
|
end
|
137
143
|
partition_with_machines_list
|
138
144
|
end
|
@@ -140,8 +146,9 @@ module XCKnife
|
|
140
146
|
# Computes a 2-aproximation to the optimal partition_time, which is an instance of the Open shop scheduling problem (which is NP-hard)
|
141
147
|
# see: https://en.wikipedia.org/wiki/Open-shop_scheduling
|
142
148
|
def compute_single_shards(number_of_shards, test_time_map, options: Options::DEFAULT)
|
143
|
-
raise XCKnife::XCKnifeError,
|
144
|
-
raise XCKnife::XCKnifeError,
|
149
|
+
raise XCKnife::XCKnifeError, 'There are not enough workers provided' if number_of_shards <= 0
|
150
|
+
raise XCKnife::XCKnifeError, 'Cannot shard an empty partition_time' if test_time_map.empty?
|
151
|
+
|
145
152
|
assignements = Array.new(number_of_shards) { MachineAssignment.new(Hash.new { |k, v| k[v] = [] }, 0) }
|
146
153
|
|
147
154
|
list_of_test_target_class_times = []
|
@@ -153,21 +160,19 @@ module XCKnife
|
|
153
160
|
|
154
161
|
# This might seem like an uncessary level of indirection, but it allows us to keep
|
155
162
|
# logic consistent regardless of the `split_bundles_across_machines` option
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
162
|
-
end.map do |(test_target, _), classes|
|
163
|
+
group = list_of_test_target_class_times.group_by do |test_target, class_name, _duration_in_milliseconds|
|
164
|
+
options.split_bundles_across_machines ? [test_target, class_name] : test_target
|
165
|
+
end
|
166
|
+
|
167
|
+
list_of_test_target_classes_times = group.map do |(test_target, _), classes|
|
163
168
|
[
|
164
169
|
test_target,
|
165
|
-
classes.map { |
|
166
|
-
classes.reduce(0) { |total_duration, (
|
170
|
+
classes.map { |_test_target, class_name, _duration_in_milliseconds| class_name },
|
171
|
+
classes.reduce(0) { |total_duration, (_test_target, _class_name, duration_in_milliseconds)| total_duration + duration_in_milliseconds }
|
167
172
|
]
|
168
173
|
end
|
169
174
|
|
170
|
-
list_of_test_target_classes_times.sort_by! { |
|
175
|
+
list_of_test_target_classes_times.sort_by! { |_test_target, _class_names, duration_in_milliseconds| -duration_in_milliseconds }
|
171
176
|
list_of_test_target_classes_times.each do |test_target, class_names, duration_in_milliseconds|
|
172
177
|
assignemnt = assignements.min_by(&:total_time)
|
173
178
|
assignemnt.test_time_map[test_target].concat class_names
|
@@ -183,27 +188,31 @@ module XCKnife
|
|
183
188
|
|
184
189
|
assignements
|
185
190
|
end
|
191
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
186
192
|
|
187
193
|
def parse_json_stream_file(filename)
|
188
194
|
return nil if filename.nil?
|
189
|
-
return [] unless File.
|
195
|
+
return [] unless File.exist?(filename)
|
196
|
+
|
190
197
|
lines = IO.readlines(filename)
|
191
|
-
lines.lazy.map { |line| OpenStruct.new(JSON.
|
198
|
+
lines.lazy.map { |line| OpenStruct.new(JSON.parse(line)) }
|
192
199
|
end
|
193
200
|
|
194
201
|
private
|
202
|
+
|
195
203
|
def inc_stat(name)
|
196
204
|
@stats[name] += 1
|
197
205
|
end
|
198
206
|
|
199
|
-
def each_duration(test_time_map
|
200
|
-
test_time_map.each do |
|
201
|
-
class_times.each do |
|
207
|
+
def each_duration(test_time_map)
|
208
|
+
test_time_map.each do |_test_target, class_times|
|
209
|
+
class_times.each do |_class_name, duration_in_milliseconds|
|
202
210
|
yield(duration_in_milliseconds)
|
203
211
|
end
|
204
212
|
end
|
205
213
|
end
|
206
214
|
|
215
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
207
216
|
def extrapolate_times_for_current_events(analyzer, times_for_target_class)
|
208
217
|
median_map = {}
|
209
218
|
times_for_target_class.each do |test_target, class_times|
|
@@ -213,9 +222,9 @@ module XCKnife
|
|
213
222
|
all_times_for_all_classes = times_for_target_class.values.flat_map(&:values)
|
214
223
|
median_of_targets = median(all_times_for_all_classes)
|
215
224
|
analyzer.target_class_map.each do |test_target, class_set|
|
216
|
-
if times_for_target_class.
|
225
|
+
if times_for_target_class.key?(test_target)
|
217
226
|
class_set.each do |clazz|
|
218
|
-
unless times_for_target_class[test_target].
|
227
|
+
unless times_for_target_class[test_target].key?(clazz)
|
219
228
|
inc_stat :class_extrapolations
|
220
229
|
times_for_target_class[test_target][clazz] = median_map[test_target]
|
221
230
|
end
|
@@ -233,6 +242,7 @@ module XCKnife
|
|
233
242
|
DEFAULT_EXTRAPOLATED_DURATION = 1000
|
234
243
|
def extrapolated_duration(median_of_targets, class_set)
|
235
244
|
return DEFAULT_EXTRAPOLATED_DURATION if median_of_targets.nil?
|
245
|
+
|
236
246
|
median_of_targets / class_set.size
|
237
247
|
end
|
238
248
|
|
@@ -248,10 +258,9 @@ module XCKnife
|
|
248
258
|
end
|
249
259
|
end
|
250
260
|
ret.each_with_index do |partition, index|
|
251
|
-
if partition.empty?
|
252
|
-
raise XCKnife::XCKnifeError, "The following partition has no tests: #{test_partitions[index].to_a.inspect}"
|
253
|
-
end
|
261
|
+
raise XCKnife::XCKnifeError, "The following partition has no tests: #{test_partitions[index].to_a.inspect}" if partition.empty?
|
254
262
|
end
|
255
263
|
end
|
256
264
|
end
|
265
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
257
266
|
end
|