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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'pp'
3
5
  require 'fileutils'
@@ -8,6 +10,7 @@ require 'logger'
8
10
  require 'shellwords'
9
11
  require 'open3'
10
12
  require 'xcknife/exceptions'
13
+ require 'etc'
11
14
 
12
15
  module XCKnife
13
16
  class TestDumper
@@ -27,7 +30,7 @@ module XCKnife
27
30
  @skip_dump_bundle_names = []
28
31
  @simctl_timeout = 0
29
32
  parse_arguments(args)
30
- @device_id ||= "booted"
33
+ @device_id ||= 'booted'
31
34
  @logger = logger
32
35
  @logger.level = @debug ? Logger::DEBUG : Logger::FATAL
33
36
  @parser = nil
@@ -38,70 +41,61 @@ module XCKnife
38
41
  naive_dump_bundle_names: @naive_dump_bundle_names, skip_dump_bundle_names: @skip_dump_bundle_names, simctl_timeout: @simctl_timeout)
39
42
  extra_environment_variables = parse_scheme_file
40
43
  logger.info { "Environment variables from xcscheme: #{extra_environment_variables.pretty_inspect}" }
41
- output_fd = File.open(@output_file, "w")
44
+ output_fd = File.open(@output_file, 'w')
42
45
  if @temporary_output_folder.nil?
43
- Dir.mktmpdir("xctestdumper_") do |outfolder|
46
+ Dir.mktmpdir('xctestdumper_') do |outfolder|
44
47
  list_tests(extra_environment_variables, helper, outfolder, output_fd)
45
48
  end
46
49
  else
47
- unless File.directory?(@temporary_output_folder)
48
- raise TestDumpError, "Error no such directory: #{@temporary_output_folder}"
49
- end
50
+ raise TestDumpError, "Error no such directory: #{@temporary_output_folder}" unless File.directory?(@temporary_output_folder)
50
51
 
51
- if Dir.entries(@temporary_output_folder).any? { |f| File.file?(File.join(@temporary_output_folder,f)) }
52
- puts "Warning: #{@temporary_output_folder} is not empty! Files can be overwritten."
53
- end
52
+ puts "Warning: #{@temporary_output_folder} is not empty! Files can be overwritten." if Dir.entries(@temporary_output_folder).any? { |f| File.file?(File.join(@temporary_output_folder, f)) }
54
53
  list_tests(extra_environment_variables, helper, File.absolute_path(@temporary_output_folder), output_fd)
55
54
  end
56
55
  output_fd.close
57
- puts "Done listing test methods"
56
+ puts 'Done listing test methods'
58
57
  end
59
58
 
60
59
  private
60
+
61
61
  def list_tests(extra_environment_variables, helper, outfolder, output_fd)
62
62
  helper.call(@derived_data_folder, outfolder, extra_environment_variables).each do |test_specification|
63
63
  concat_to_file(test_specification, output_fd)
64
64
  end
65
65
  end
66
66
 
67
-
68
67
  def parse_scheme_file
69
68
  return {} unless @xcscheme_file
70
- unless File.exists?(@xcscheme_file)
71
- raise ArgumentError, "Error: no such xcscheme file: #{@xcscheme_file}"
72
- end
69
+ raise ArgumentError, "Error: no such xcscheme file: #{@xcscheme_file}" unless File.exist?(@xcscheme_file)
70
+
73
71
  XCKnife::XcschemeAnalyzer.extract_environment_variables(IO.read(@xcscheme_file))
74
72
  end
75
73
 
76
74
  def parse_arguments(args)
77
75
  positional_arguments = parse_options(args)
78
- if positional_arguments.size < required_arguments.size
79
- warn_and_exit("You must specify *all* required arguments: #{required_arguments.join(", ")}")
80
- end
76
+ warn_and_exit("You must specify *all* required arguments: #{required_arguments.join(', ')}") if positional_arguments.size < required_arguments.size
81
77
  @derived_data_folder, @output_file, @device_id = positional_arguments
82
78
  end
83
79
 
84
80
  def parse_options(args)
85
- begin
86
- return @parser.parse(args)
87
- rescue OptionParser::ParseError => error
88
- warn_and_exit(error)
89
- end
81
+ @parser.parse(args)
82
+ rescue OptionParser::ParseError => e
83
+ warn_and_exit(e)
90
84
  end
91
85
 
92
86
  def build_parser
93
87
  OptionParser.new do |opts|
94
88
  opts.banner += " #{arguments_banner}"
95
- opts.on("-d", "--debug", "Debug mode enabled") { |v| @debug = v }
96
- opts.on("-r", "--retry-count COUNT", "Max retry count for simulator output", Integer) { |v| @max_retry_count = v }
97
- opts.on("-x", '--simctl-timeout SECONDS', "Max allowed time in seconds for simctl commands", Integer) { |v| @simctl_timeout = v }
98
- opts.on("-t", "--temporary-output OUTPUT_FOLDER", "Sets temporary Output folder") { |v| @temporary_output_folder = v }
99
- opts.on("-s", "--scheme XCSCHEME_FILE", "Reads environments variables from the xcscheme file") { |v| @xcscheme_file = v }
100
- opts.on("-l", "--dylib_logfile DYLIB_LOG_FILE", "Path for dylib log file") { |v| @dylib_logfile_path = v }
89
+ opts.on('-d', '--debug', 'Debug mode enabled') { |v| @debug = v }
90
+ opts.on('-r', '--retry-count COUNT', 'Max retry count for simulator output', Integer) { |v| @max_retry_count = v }
91
+ opts.on('-x', '--simctl-timeout SECONDS', 'Max allowed time in seconds for simctl commands', Integer) { |v| @simctl_timeout = v }
92
+ opts.on('-t', '--temporary-output OUTPUT_FOLDER', 'Sets temporary Output folder') { |v| @temporary_output_folder = v }
93
+ opts.on('-s', '--scheme XCSCHEME_FILE', 'Reads environments variables from the xcscheme file') { |v| @xcscheme_file = v }
94
+ opts.on('-l', '--dylib_logfile DYLIB_LOG_FILE', 'Path for dylib log file') { |v| @dylib_logfile_path = v }
101
95
  opts.on('--naive-dump TEST_BUNDLE_NAMES', 'List of test bundles to dump using static analysis', Array) { |v| @naive_dump_bundle_names = v }
102
96
  opts.on('--skip-dump TEST_BUNDLE_NAMES', 'List of test bundles to skip dumping', Array) { |v| @skip_dump_bundle_names = v }
103
97
 
104
- opts.on_tail("-h", "--help", "Show this message") do
98
+ opts.on_tail('-h', '--help', 'Show this message') do
105
99
  puts opts
106
100
  exit
107
101
  end
@@ -118,7 +112,7 @@ module XCKnife
118
112
 
119
113
  def arguments_banner
120
114
  optional_args = optional_arguments.map { |a| "[#{a}]" }
121
- (required_arguments + optional_args).join(" ")
115
+ (required_arguments + optional_args).join(' ')
122
116
  end
123
117
 
124
118
  def warn_and_exit(msg)
@@ -128,7 +122,7 @@ module XCKnife
128
122
  def concat_to_file(test_specification, output_fd)
129
123
  file = test_specification.json_stream_file
130
124
  IO.readlines(file).each do |line|
131
- event = OpenStruct.new(JSON.load(line))
125
+ event = OpenStruct.new(JSON.parse(line))
132
126
  if should_test_event_be_ignored?(test_specification, event)
133
127
  logger.info "Skipped test dumper line #{line}"
134
128
  else
@@ -141,8 +135,9 @@ module XCKnife
141
135
 
142
136
  # Current limitation: this only supports class level skipping
143
137
  def should_test_event_be_ignored?(test_specification, event)
144
- return false unless event["test"] == "1"
145
- test_specification.skip_test_identifiers.include?(event["className"])
138
+ return false unless event['test'] == '1'
139
+
140
+ test_specification.skip_test_identifiers.include?(event['className'])
146
141
  end
147
142
  end
148
143
 
@@ -151,15 +146,16 @@ module XCKnife
151
146
 
152
147
  attr_reader :logger
153
148
 
149
+ # rubocop:disable Metrics/ParameterLists
154
150
  def initialize(device_id, max_retry_count, debug, logger, dylib_logfile_path,
155
151
  naive_dump_bundle_names: [], skip_dump_bundle_names: [], simctl_timeout: 0)
156
152
  @xcode_path = `xcode-select -p`.strip
157
153
  @simctl_path = `xcrun -f simctl`.strip
158
154
  @nm_path = `xcrun -f nm`.strip
159
155
  @swift_path = `xcrun -f swift`.strip
160
- @platforms_path = File.join(@xcode_path, "Platforms")
161
- @platform_path = File.join(@platforms_path, "iPhoneSimulator.platform")
162
- @sdk_path = File.join(@platform_path, "Developer/SDKs/iPhoneSimulator.sdk")
156
+ @platforms_path = File.join(@xcode_path, 'Platforms')
157
+ @platform_path = File.join(@platforms_path, 'iPhoneSimulator.platform')
158
+ @sdk_path = File.join(@platform_path, 'Developer/SDKs/iPhoneSimulator.sdk')
163
159
  @testroot = nil
164
160
  @device_id = device_id
165
161
  @max_retry_count = max_retry_count
@@ -170,68 +166,109 @@ module XCKnife
170
166
  @naive_dump_bundle_names = naive_dump_bundle_names
171
167
  @skip_dump_bundle_names = skip_dump_bundle_names
172
168
  end
169
+ # rubocop:enable Metrics/ParameterLists
173
170
 
174
171
  def call(derived_data_folder, list_folder, extra_environment_variables = {})
175
172
  @testroot = File.join(derived_data_folder, 'Build', 'Products')
176
173
  xctestrun_file = Dir[File.join(@testroot, '*.xctestrun')].first
177
- if xctestrun_file.nil?
178
- raise ArgumentError, "No xctestrun on #{@testroot}"
179
- end
174
+ raise ArgumentError, "No xctestrun on #{@testroot}" if xctestrun_file.nil?
175
+
180
176
  xctestrun_as_json = `plutil -convert json -o - "#{xctestrun_file}"`
181
177
  FileUtils.mkdir_p(list_folder)
182
- list_tests(JSON.load(xctestrun_as_json), list_folder, extra_environment_variables)
178
+ list_tests(JSON.parse(xctestrun_as_json), list_folder, extra_environment_variables)
183
179
  end
184
180
 
185
181
  private
186
182
 
187
183
  attr_reader :testroot
188
184
 
189
- def list_tests(xctestrun, list_folder, extra_environment_variables)
190
- xctestrun.reject! { |test_bundle_name, _| test_bundle_name == '__xctestrun_metadata__' }
191
- xctestrun.map do |test_bundle_name, test_bundle|
185
+ def test_groups(xctestrun)
186
+ xctestrun.group_by do |test_bundle_name, _test_bundle|
192
187
  if @skip_dump_bundle_names.include?(test_bundle_name)
193
- logger.info { "Skipping dumping tests in `#{test_bundle_name}` -- writing out fake event"}
194
- test_specification = list_single_test(list_folder, test_bundle, test_bundle_name)
188
+ 'single'
195
189
  elsif @naive_dump_bundle_names.include?(test_bundle_name)
196
- test_specification = list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
190
+ 'nm'
197
191
  else
198
- test_specification = list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
199
- wait_test_dumper_completion(test_specification.json_stream_file)
192
+ 'simctl'
200
193
  end
194
+ end
195
+ end
196
+
197
+ # This executes naive test dumping in parallel by queueing up items onto a work queue to process
198
+ # with 1 new thread per processor. Results are placed onto a threadsafe spec queue to avoid writing
199
+ # to an object between threads, then popped off re-inserting them to our list of test results.
200
+ # rubocop:disable Metrics/CyclomaticComplexity
201
+ def list_tests(xctestrun, list_folder, extra_environment_variables)
202
+ xctestrun.reject! { |test_bundle_name, _| test_bundle_name == '__xctestrun_metadata__' }
203
+
204
+ test_runs_by_method = test_groups(xctestrun)
205
+ spec_queue = Queue.new
206
+ nm_bundle_queue = Queue.new
207
+ results = []
208
+ single_tests = test_runs_by_method['single'] || []
209
+ nm_tests = test_runs_by_method['nm'] || []
210
+ simctl_tests = test_runs_by_method['simctl'] || []
211
+
212
+ single_tests.each do |test_bundle_name, test_bundle|
213
+ logger.info { "Skipping dumping tests in `#{test_bundle_name}` -- writing out fake event" }
214
+ spec_queue << list_single_test(list_folder, test_bundle, test_bundle_name)
215
+ end
216
+
217
+ simctl_tests.each do |test_bundle_name, test_bundle|
218
+ test_spec = list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
219
+ wait_test_dumper_completion(test_spec.json_stream_file)
201
220
 
202
- test_specification
221
+ spec_queue << test_spec
203
222
  end
223
+
224
+ nm_tests.each { |item| nm_bundle_queue << item }
225
+
226
+ [Etc.nprocessors, nm_bundle_queue.size].min.times.map do
227
+ nm_bundle_queue << :stop
228
+
229
+ Thread.new do
230
+ Thread.current.abort_on_exception = true
231
+
232
+ until (item = nm_bundle_queue.pop) == :stop
233
+ test_bundle_name, test_bundle = item
234
+ spec_queue << list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
235
+ end
236
+ end
237
+ end.each(&:join)
238
+
239
+ results << spec_queue.pop until spec_queue.empty?
240
+
241
+ results
204
242
  end
243
+ # rubocop:enable Metrics/CyclomaticComplexity
205
244
 
206
245
  def list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
207
- env_variables = test_bundle["EnvironmentVariables"]
208
- testing_env_variables = test_bundle["TestingEnvironmentVariables"]
246
+ env_variables = test_bundle['EnvironmentVariables']
247
+ testing_env_variables = test_bundle['TestingEnvironmentVariables']
209
248
  outpath = File.join(list_folder, test_bundle_name)
210
- test_host = replace_vars(test_bundle["TestHostPath"])
211
- test_bundle_path = replace_vars(test_bundle["TestBundlePath"], test_host)
249
+ test_host = replace_vars(test_bundle['TestHostPath'])
250
+ test_bundle_path = replace_vars(test_bundle['TestBundlePath'], test_host)
212
251
  test_dumper_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'TestDumper', 'TestDumper.dylib'))
213
- unless File.exist?(test_dumper_path)
214
- raise TestDumpError, "Could not find TestDumper.dylib on #{test_dumper_path}"
215
- end
252
+ raise TestDumpError, "Could not find TestDumper.dylib on #{test_dumper_path}" unless File.exist?(test_dumper_path)
216
253
 
217
- is_logic_test = test_bundle["TestHostBundleIdentifier"].nil?
254
+ is_logic_test = test_bundle['TestHostBundleIdentifier'].nil?
218
255
  env = simctl_child_attrs(
219
- "XCTEST_TYPE" => xctest_type(test_bundle),
220
- "XCTEST_TARGET" => test_bundle_name,
221
- "TestDumperOutputPath" => outpath,
222
- "IDE_INJECTION_PATH" => testing_env_variables["DYLD_INSERT_LIBRARIES"],
223
- "XCInjectBundleInto" => testing_env_variables["XCInjectBundleInto"],
224
- "XCInjectBundle" => test_bundle_path,
225
- "TestBundleLocation" => test_bundle_path,
226
- "OS_ACTIVITY_MODE" => "disable",
227
- "DYLD_PRINT_LIBRARIES" => "YES",
228
- "DYLD_PRINT_ENV" => "YES",
229
- "DYLD_ROOT_PATH" => @sdk_path,
230
- "DYLD_LIBRARY_PATH" => env_variables["DYLD_LIBRARY_PATH"],
231
- "DYLD_FRAMEWORK_PATH" => env_variables["DYLD_FRAMEWORK_PATH"],
232
- "DYLD_FALLBACK_LIBRARY_PATH" => "#{@sdk_path}/usr/lib",
233
- "DYLD_FALLBACK_FRAMEWORK_PATH" => "#{@platform_path}/Developer/Library/Frameworks",
234
- "DYLD_INSERT_LIBRARIES" => test_dumper_path,
256
+ 'XCTEST_TYPE' => xctest_type(test_bundle),
257
+ 'XCTEST_TARGET' => test_bundle_name,
258
+ 'TestDumperOutputPath' => outpath,
259
+ 'IDE_INJECTION_PATH' => testing_env_variables['DYLD_INSERT_LIBRARIES'],
260
+ 'XCInjectBundleInto' => testing_env_variables['XCInjectBundleInto'],
261
+ 'XCInjectBundle' => test_bundle_path,
262
+ 'TestBundleLocation' => test_bundle_path,
263
+ 'OS_ACTIVITY_MODE' => 'disable',
264
+ 'DYLD_PRINT_LIBRARIES' => 'YES',
265
+ 'DYLD_PRINT_ENV' => 'YES',
266
+ 'DYLD_ROOT_PATH' => @sdk_path,
267
+ 'DYLD_LIBRARY_PATH' => env_variables['DYLD_LIBRARY_PATH'],
268
+ 'DYLD_FRAMEWORK_PATH' => env_variables['DYLD_FRAMEWORK_PATH'],
269
+ 'DYLD_FALLBACK_LIBRARY_PATH' => "#{@sdk_path}/usr/lib",
270
+ 'DYLD_FALLBACK_FRAMEWORK_PATH' => "#{@platform_path}/Developer/Library/Frameworks",
271
+ 'DYLD_INSERT_LIBRARIES' => test_dumper_path
235
272
  )
236
273
  env.merge!(simctl_child_attrs(extra_environment_variables))
237
274
  inject_vars(env, test_host)
@@ -241,18 +278,21 @@ module XCKnife
241
278
  run_logic_test(env, test_host, test_bundle_path)
242
279
  else
243
280
  install_app(test_host)
244
- test_host_bundle_identifier = replace_vars(test_bundle["TestHostBundleIdentifier"], test_host)
281
+ test_host_bundle_identifier = replace_vars(test_bundle['TestHostBundleIdentifier'], test_host)
245
282
  run_apptest(env, test_host_bundle_identifier, test_bundle_path)
246
283
  end
247
- return TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
284
+ TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
248
285
  end
249
286
 
287
+ # Improvement?: assume that everything in the historical info is correct, so dont simctl or nm, and just spit out exactly what it said the classes were
288
+
250
289
  def list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
251
290
  output_methods(list_folder, test_bundle, test_bundle_name) do |test_bundle_path|
252
291
  methods = []
253
292
  swift_demangled_nm(test_bundle_path) do |output|
254
293
  output.each_line do |line|
255
- next unless method = method_from_nm_line(line)
294
+ next unless (method = method_from_nm_line(line))
295
+
256
296
  methods << method
257
297
  end
258
298
  end
@@ -271,25 +311,25 @@ module XCKnife
271
311
  logger.info { "Writing out TestDumper file for #{test_bundle_name} to #{outpath}" }
272
312
  test_specification = TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
273
313
 
274
- test_bundle_path = replace_vars(test_bundle["TestBundlePath"], replace_vars(test_bundle["TestHostPath"]))
314
+ test_bundle_path = replace_vars(test_bundle['TestBundlePath'], replace_vars(test_bundle['TestHostPath']))
275
315
  methods = yield(test_bundle_path)
276
316
 
277
317
  test_type = xctest_type(test_bundle)
278
318
  File.open test_specification.json_stream_file, 'a' do |f|
279
- f << JSON.dump(message: "Starting Test Dumper", event: "begin-test-suite", testType: test_type) << "\n"
319
+ f << JSON.dump(message: 'Starting Test Dumper', event: 'begin-test-suite', testType: test_type) << "\n"
280
320
  f << JSON.dump(event: 'begin-ocunit', bundleName: File.basename(test_bundle_path), targetName: test_bundle_name) << "\n"
281
321
  methods.map { |method| method[:class] }.uniq.each do |class_name|
282
- f << JSON.dump(test: '1', className: class_name, event: "end-test", totalDuration: "0") << "\n"
322
+ f << JSON.dump(test: '1', className: class_name, event: 'end-test', totalDuration: '0') << "\n"
283
323
  end
284
- f << JSON.dump(message: "Completed Test Dumper", event: "end-action", testType: test_type) << "\n"
324
+ f << JSON.dump(message: 'Completed Test Dumper', event: 'end-action', testType: test_type) << "\n"
285
325
  end
286
326
 
287
327
  test_specification
288
328
  end
289
329
 
290
330
  def discover_tests_to_skip(test_bundle)
291
- identifier_for_test_method = "/"
292
- skip_test_identifiers = test_bundle["SkipTestIdentifiers"] || []
331
+ identifier_for_test_method = '/'
332
+ skip_test_identifiers = test_bundle['SkipTestIdentifiers'] || []
293
333
  skip_test_identifiers.reject { |i| i.include?(identifier_for_test_method) }.to_set
294
334
  end
295
335
 
@@ -298,12 +338,11 @@ module XCKnife
298
338
  end
299
339
 
300
340
  def wrapped_simctl(args)
301
- args = [*gtimeout, simctl] + args
302
- args
341
+ [*gtimeout, simctl] + args
303
342
  end
304
343
 
305
344
  def gtimeout
306
- return [] unless @simctl_timeout > 0
345
+ return [] unless @simctl_timeout.positive?
307
346
 
308
347
  path = gtimeout_path
309
348
  if path.empty?
@@ -311,22 +350,22 @@ module XCKnife
311
350
  return []
312
351
  end
313
352
 
314
- [path, "-k", "5", "#{@simctl_timeout}"]
353
+ [path, '-k', '5', @simctl_timeout.to_s]
315
354
  end
316
355
 
317
356
  def gtimeout_path
318
357
  `which gtimeout`.strip
319
358
  end
320
359
 
321
- def replace_vars(str, testhost = "<UNKNOWN>")
322
- str.gsub("__PLATFORMS__", @platforms_path).
323
- gsub("__TESTHOST__", testhost).
324
- gsub("__TESTROOT__", testroot)
360
+ def replace_vars(str, testhost = '<UNKNOWN>')
361
+ str.gsub('__PLATFORMS__', @platforms_path)
362
+ .gsub('__TESTHOST__', testhost)
363
+ .gsub('__TESTROOT__', testroot)
325
364
  end
326
365
 
327
366
  def inject_vars(env, test_host)
328
367
  env.each do |k, v|
329
- env[k] = replace_vars(v || "", test_host)
368
+ env[k] = replace_vars(v || '', test_host)
330
369
  end
331
370
  end
332
371
 
@@ -339,47 +378,44 @@ module XCKnife
339
378
  def install_app(test_host_path)
340
379
  retries_count = 0
341
380
  max_retry_count = 3
342
- until (retries_count > max_retry_count) or call_simctl(["install", @device_id, test_host_path])
381
+ until (retries_count > max_retry_count) || call_simctl(['install', @device_id, test_host_path])
343
382
  retries_count += 1
344
383
  call_simctl ['shutdown', @device_id]
345
384
  call_simctl ['boot', @device_id]
346
385
  sleep 1.0
347
386
  end
348
387
 
349
- if retries_count > max_retry_count
350
- raise TestDumpError, "Installing #{test_host_path} failed"
351
- end
352
-
388
+ raise TestDumpError, "Installing #{test_host_path} failed" if retries_count > max_retry_count
353
389
  end
354
390
 
355
391
  def wait_test_dumper_completion(file)
356
392
  retries_count = 0
357
- until has_test_dumper_terminated?(file) do
393
+ until test_dumper_terminated?(file)
358
394
  retries_count += 1
359
- if retries_count == @max_retry_count
360
- raise TestDumpError, "Timeout error on: #{file}"
361
- end
395
+ raise TestDumpError, "Timeout error on: #{file}" if retries_count == @max_retry_count
396
+
362
397
  sleep 0.1
363
398
  end
364
399
  end
365
400
 
366
- def has_test_dumper_terminated?(file)
367
- return false unless File.exists?(file)
401
+ def test_dumper_terminated?(file)
402
+ return false unless File.exist?(file)
403
+
368
404
  last_line = `tail -n 1 "#{file}"`
369
- return last_line.include?("Completed Test Dumper")
405
+ last_line.include?('Completed Test Dumper')
370
406
  end
371
407
 
372
408
  def run_apptest(env, test_host_bundle_identifier, test_bundle_path)
373
- unless call_simctl(["launch", @device_id, test_host_bundle_identifier, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env)
374
- raise TestDumpError, "Launching #{test_bundle_path} in #{test_host_bundle_identifier} failed"
375
- end
409
+ return if call_simctl(['launch', @device_id, test_host_bundle_identifier, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env)
410
+
411
+ raise TestDumpError, "Launching #{test_bundle_path} in #{test_host_bundle_identifier} failed"
376
412
  end
377
413
 
378
414
  def run_logic_test(env, test_host, test_bundle_path)
379
- opts = @debug ? {} : { err: "/dev/null" }
380
- unless call_simctl(["spawn", @device_id, test_host, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env, **opts)
381
- raise TestDumpError, "Spawning #{test_bundle_path} in #{test_host} failed"
382
- end
415
+ opts = @debug ? {} : { err: '/dev/null' }
416
+ return if call_simctl(['spawn', @device_id, test_host, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env, **opts)
417
+
418
+ raise TestDumpError, "Spawning #{test_bundle_path} in #{test_host} failed"
383
419
  end
384
420
 
385
421
  def call_simctl(args, env: {}, **spawn_opts)
@@ -398,21 +434,19 @@ module XCKnife
398
434
  end
399
435
 
400
436
  def xctest_type(test_bundle)
401
- if test_bundle["TestHostBundleIdentifier"].nil?
402
- "LOGICTEST"
437
+ if test_bundle['TestHostBundleIdentifier'].nil?
438
+ 'LOGICTEST'
403
439
  else
404
- "APPTEST"
440
+ 'APPTEST'
405
441
  end
406
442
  end
407
443
 
408
- def swift_demangled_nm(test_bundle_path)
409
- Open3.pipeline_r([@nm_path, File.join(test_bundle_path, File.basename(test_bundle_path, '.xctest'))], [@swift_path, 'demangle']) do |o, _ts|
410
- yield(o)
411
- end
444
+ def swift_demangled_nm(test_bundle_path, &block)
445
+ Open3.pipeline_r([@nm_path, File.join(test_bundle_path, File.basename(test_bundle_path, '.xctest'))], [@swift_path, 'demangle'], &block)
412
446
  end
413
447
 
414
448
  def method_from_nm_line(line)
415
- return unless line.strip =~ %r{^
449
+ return unless line.strip =~ /^
416
450
  [\da-f]+\s # address
417
451
  [tT]\s # symbol type
418
452
  (?: # method
@@ -420,13 +454,13 @@ module XCKnife
420
454
  | # or swift instance method
421
455
  _? # only present on Xcode 10.0 and below
422
456
  (?:@objc\s)? # optional objc annotation
423
- (?:[^\.]+\.)? # module name
457
+ (?:[^.]+\.)? # module name
424
458
  (.+) # class name
425
459
  \.(test.+)\s->\s\(\) # method signature
426
460
  )
427
- $}ox
461
+ $/ox
428
462
 
429
- { class: $1 || $3, method: $2 || $4 }
463
+ { class: Regexp.last_match(1) || Regexp.last_match(3), method: Regexp.last_match(2) || Regexp.last_match(4) }
430
464
  end
431
465
  end
432
466
  end