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.
@@ -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