xcknife 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +168 -0
- data/.travis.yml +1 -1
- data/.vscode/configure.sh +23 -0
- data/.vscode/vscode_ruby.json.template +44 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +43 -15
- data/Rakefile +14 -10
- data/bin/xcknife +2 -0
- data/bin/xcknife-min +12 -15
- data/bin/xcknife-test-dumper +2 -0
- data/example/run_example.rb +10 -8
- data/example/xcknife-exemplar-historical-data.json-stream +3 -3
- data/example/xcknife-exemplar.json-stream +3 -3
- data/lib/xcknife.rb +3 -1
- data/lib/xcknife/events_analyzer.rb +11 -6
- data/lib/xcknife/exceptions.rb +2 -0
- data/lib/xcknife/json_stream_parser_helper.rb +8 -8
- data/lib/xcknife/runner.rb +18 -19
- data/lib/xcknife/stream_parser.rb +39 -30
- data/lib/xcknife/test_dumper.rb +155 -121
- data/lib/xcknife/xcscheme_analyzer.rb +9 -9
- data/lib/xcknife/xctool_cmd_helper.rb +16 -4
- data/xcknife.gemspec +8 -6
- metadata +9 -6
data/lib/xcknife/test_dumper.rb
CHANGED
@@ -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 ||=
|
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,
|
44
|
+
output_fd = File.open(@output_file, 'w')
|
42
45
|
if @temporary_output_folder.nil?
|
43
|
-
Dir.mktmpdir(
|
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
|
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.
|
71
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
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(
|
96
|
-
opts.on(
|
97
|
-
opts.on(
|
98
|
-
opts.on(
|
99
|
-
opts.on(
|
100
|
-
opts.on(
|
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(
|
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.
|
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[
|
145
|
-
|
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,
|
161
|
-
@platform_path = File.join(@platforms_path,
|
162
|
-
@sdk_path = File.join(@platform_path,
|
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
|
-
|
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.
|
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
|
190
|
-
xctestrun.
|
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
|
-
|
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
|
-
|
190
|
+
'nm'
|
197
191
|
else
|
198
|
-
|
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
|
-
|
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[
|
208
|
-
testing_env_variables = test_bundle[
|
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[
|
211
|
-
test_bundle_path = replace_vars(test_bundle[
|
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[
|
254
|
+
is_logic_test = test_bundle['TestHostBundleIdentifier'].nil?
|
218
255
|
env = simctl_child_attrs(
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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[
|
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
|
-
|
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[
|
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:
|
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:
|
322
|
+
f << JSON.dump(test: '1', className: class_name, event: 'end-test', totalDuration: '0') << "\n"
|
283
323
|
end
|
284
|
-
f << JSON.dump(message:
|
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[
|
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
|
-
|
302
|
-
args
|
341
|
+
[*gtimeout, simctl] + args
|
303
342
|
end
|
304
343
|
|
305
344
|
def gtimeout
|
306
|
-
return [] unless @simctl_timeout
|
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,
|
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 =
|
322
|
-
str.gsub(
|
323
|
-
|
324
|
-
|
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 ||
|
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)
|
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
|
393
|
+
until test_dumper_terminated?(file)
|
358
394
|
retries_count += 1
|
359
|
-
if retries_count == @max_retry_count
|
360
|
-
|
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
|
367
|
-
return false unless File.
|
401
|
+
def test_dumper_terminated?(file)
|
402
|
+
return false unless File.exist?(file)
|
403
|
+
|
368
404
|
last_line = `tail -n 1 "#{file}"`
|
369
|
-
|
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
|
-
|
374
|
-
|
375
|
-
|
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:
|
380
|
-
|
381
|
-
|
382
|
-
|
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[
|
402
|
-
|
437
|
+
if test_bundle['TestHostBundleIdentifier'].nil?
|
438
|
+
'LOGICTEST'
|
403
439
|
else
|
404
|
-
|
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']
|
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 =~
|
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
|
-
(?:[
|
457
|
+
(?:[^.]+\.)? # module name
|
424
458
|
(.+) # class name
|
425
459
|
\.(test.+)\s->\s\(\) # method signature
|
426
460
|
)
|
427
|
-
|
461
|
+
$/ox
|
428
462
|
|
429
|
-
{ class:
|
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
|