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.
- checksums.yaml +5 -5
- data/.github/workflows/tests.yml +24 -0
- data/.gitignore +4 -1
- data/.rubocop.yml +168 -0
- data/.ruby-version +1 -1
- data/.vscode/configure.sh +23 -0
- data/.vscode/vscode_ruby.json.template +44 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +63 -0
- data/OWNERS.yml +2 -0
- data/README.md +9 -1
- data/Rakefile +16 -11
- data/TestDumper/README.md +2 -1
- data/TestDumper/TestDumper.xcodeproj/project.pbxproj +27 -5
- data/TestDumper/TestDumper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- data/TestDumper/TestDumper.xcodeproj/xcshareddata/xcschemes/TestDumper.xcscheme +1 -1
- data/TestDumper/TestDumper/Initialize.m +83 -44
- 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 +6 -2
- data/lib/xcknife/json_stream_parser_helper.rb +8 -8
- data/lib/xcknife/runner.rb +18 -19
- data/lib/xcknife/stream_parser.rb +84 -37
- data/lib/xcknife/test_dumper.rb +283 -112
- data/lib/xcknife/xcscheme_analyzer.rb +9 -9
- data/lib/xcknife/xctool_cmd_helper.rb +28 -4
- data/xcknife.gemspec +8 -9
- metadata +15 -12
- data/.gitmodules +0 -3
- data/.travis.yml +0 -18
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'
|
@@ -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 ||=
|
25
|
-
@logger =
|
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,
|
44
|
+
output_fd = File.open(@output_file, 'w')
|
35
45
|
if @temporary_output_folder.nil?
|
36
|
-
Dir.mktmpdir(
|
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
|
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.
|
65
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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(
|
91
|
-
opts.on(
|
92
|
-
opts.on(
|
93
|
-
opts.on(
|
94
|
-
|
95
|
-
opts.
|
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
|
-
|
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.
|
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[
|
137
|
-
|
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
|
-
|
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
|
-
@
|
150
|
-
@
|
151
|
-
@
|
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 =
|
161
|
-
xctestrun_file = Dir[
|
162
|
-
if xctestrun_file.nil?
|
163
|
-
|
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.
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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[
|
254
|
+
is_logic_test = test_bundle['TestHostBundleIdentifier'].nil?
|
189
255
|
env = simctl_child_attrs(
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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.
|
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[
|
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
|
-
|
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[
|
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
|
232
|
-
|
233
|
-
|
234
|
-
|
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 ||
|
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
|
-
|
251
|
-
|
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
|
393
|
+
until test_dumper_terminated?(file)
|
258
394
|
retries_count += 1
|
259
|
-
if retries_count == @max_retry_count
|
260
|
-
|
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
|
268
|
-
return false unless File.
|
401
|
+
def test_dumper_terminated?(file)
|
402
|
+
return false unless File.exist?(file)
|
403
|
+
|
269
404
|
last_line = `tail -n 1 "#{file}"`
|
270
|
-
|
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
|
-
|
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
|
-
|
279
|
-
|
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
|
-
|
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,
|
287
|
-
|
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
|
-
|
291
|
-
|
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
|