xcknife 0.6.6 → 0.13.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,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