xcknife 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'xcknife'
3
5
 
4
6
  XCKnife::TestDumper.invoke
@@ -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
- include XCKnife::XCToolCmdHelper
8
+
7
9
  TARGET_PARTITIONS = {
8
- "AllTests" => ["CommonTestTarget", "iPadTestTarget"],
9
- "OnlyCommon" => ["CommonTestTarget"]
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("xcknife-exemplar-historical-data.json-stream", "xcknife-exemplar.json-stream")
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":"iphonesimulator9.2"}
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":"iphonesimulator9.2"}
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":"iphonesimulator9.2"}
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":"iphonesimulator9.2"}
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":"iphonesimulator9.2"}
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":"iphonesimulator9.2"}
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"}
@@ -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.11.1'
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 has_test_target?(target)
21
- target_class_map.has_key?(target)
23
+ def test_target?(target)
24
+ target_class_map.key?(target)
22
25
  end
23
26
 
24
- def has_test_class?(target, clazz)
25
- has_test_target?(target) and target_class_map[target].include?(clazz)
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 has_test_target?(target)
48
+ def test_target?(_target)
44
49
  true
45
50
  end
46
51
 
47
- def has_test_class?(target, clazz)
52
+ def test_class?(_target, _clazz)
48
53
  true
49
54
  end
50
55
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module XCKnife
2
4
  # Base error class for xcknife
3
5
  XCKnifeError = Class.new(RuntimeError)
@@ -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 == "begin-ocunit"
10
- if result.test and result.event == "end-test"
11
- raise XCKnife::StreamParsingError, "No test target defined" if current_target.nil?
12
- block.call(current_target, normalize_result(result))
13
- end
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
@@ -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
- File.open(@output_file_name, "w") { |f| f.puts(json) }
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
- warn_and_exit("You must specify *all* required arguments: #{required_arguments.join(", ")}")
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 => error
105
- warn_and_exit(error)
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("-p", "--partition TARGETS",
113
- "Comma separated list of targets. Can be used multiple times.") do |v|
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("-o", "--output FILENAME", "Output file. Defaults to STDOUT") { |v| @output_file_name = v }
118
- opts.on("-a", "--abbrev", "Results are abbreviated") { |v| @abbreviated_output = v }
119
- opts.on("-x", "--xcodebuild-output", "Output is formatted for xcodebuild") { |v| @xcodebuild_output = v }
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("-h", "--help", "Show this message") do
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 = EventsAnalyzer.for(current_events, relevant_partitions)
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.has_test_class?(target_name, result.className)
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, "There are not enough workers provided" if number_of_shards <= 0
144
- raise XCKnife::XCKnifeError, "Cannot shard an empty partition_time" if test_time_map.empty?
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
- list_of_test_target_classes_times = list_of_test_target_class_times.group_by do |test_target, class_name, duration_in_milliseconds|
157
- if options.split_bundles_across_machines
158
- [test_target, class_name]
159
- else
160
- test_target
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 { |test_target, class_name, duration_in_milliseconds| class_name },
166
- classes.reduce(0) { |total_duration, (test_target, class_name, duration_in_milliseconds)| total_duration + duration_in_milliseconds},
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! { |test_target, class_names, duration_in_milliseconds| -duration_in_milliseconds }
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.exists?(filename)
195
+ return [] unless File.exist?(filename)
196
+
190
197
  lines = IO.readlines(filename)
191
- lines.lazy.map { |line| OpenStruct.new(JSON.load(line)) }
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, &block)
200
- test_time_map.each do |test_target, class_times|
201
- class_times.each do |class_name, duration_in_milliseconds|
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.has_key?(test_target)
225
+ if times_for_target_class.key?(test_target)
217
226
  class_set.each do |clazz|
218
- unless times_for_target_class[test_target].has_key?(clazz)
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