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