xcknife 0.11.1 → 0.12.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 +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
|