xcknife 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|