class XCKnife::TestDumperHelper
Constants
- TestSpecification
Attributes
logger[R]
testroot[R]
Public Class Methods
new(device_id, max_retry_count, debug, logger, dylib_logfile_path, naive_dump_bundle_names: [], skip_dump_bundle_names: [], simctl_timeout: 0)
click to toggle source
rubocop:disable Metrics/ParameterLists
# File lib/xcknife/test_dumper.rb, line 150 def initialize(device_id, max_retry_count, debug, logger, dylib_logfile_path, naive_dump_bundle_names: [], skip_dump_bundle_names: [], simctl_timeout: 0) @xcode_path = `xcode-select -p`.strip @simctl_path = `xcrun -f simctl`.strip @nm_path = `xcrun -f nm`.strip @swift_path = `xcrun -f swift`.strip @platforms_path = File.join(@xcode_path, 'Platforms') @platform_path = File.join(@platforms_path, 'iPhoneSimulator.platform') @sdk_path = File.join(@platform_path, 'Developer/SDKs/iPhoneSimulator.sdk') @testroot = nil @device_id = device_id @max_retry_count = max_retry_count @simctl_timeout = simctl_timeout @logger = logger @debug = debug @dylib_logfile_path = dylib_logfile_path if dylib_logfile_path @naive_dump_bundle_names = naive_dump_bundle_names @skip_dump_bundle_names = skip_dump_bundle_names end
Public Instance Methods
call(derived_data_folder, list_folder, extra_environment_variables = {})
click to toggle source
rubocop:enable Metrics/ParameterLists
# File lib/xcknife/test_dumper.rb, line 171 def call(derived_data_folder, list_folder, extra_environment_variables = {}) @testroot = File.join(derived_data_folder, 'Build', 'Products') xctestrun_file = Dir[File.join(@testroot, '*.xctestrun')].first raise ArgumentError, "No xctestrun on #{@testroot}" if xctestrun_file.nil? xctestrun_as_json = `plutil -convert json -o - "#{xctestrun_file}"` FileUtils.mkdir_p(list_folder) list_tests(JSON.parse(xctestrun_as_json), list_folder, extra_environment_variables) end
Private Instance Methods
call_simctl(args, env: {}, **spawn_opts)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 421 def call_simctl(args, env: {}, **spawn_opts) args = wrapped_simctl(args) cmd = Shellwords.shelljoin(args) puts "Running:\n$ #{cmd}" logger.info { "Environment variables:\n #{env.pretty_print_inspect}" } ret = system(env, *args, **spawn_opts) puts "Simctl errored with the following env:\n #{env.pretty_print_inspect}" unless ret ret end
discover_tests_to_skip(test_bundle)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 330 def discover_tests_to_skip(test_bundle) identifier_for_test_method = '/' skip_test_identifiers = test_bundle['SkipTestIdentifiers'] || [] skip_test_identifiers.reject { |i| i.include?(identifier_for_test_method) }.to_set end
dylib_logfile_path()
click to toggle source
# File lib/xcknife/test_dumper.rb, line 432 def dylib_logfile_path @dylib_logfile_path ||= '/tmp/xcknife_testdumper_dylib.log' end
gtimeout()
click to toggle source
# File lib/xcknife/test_dumper.rb, line 344 def gtimeout return [] unless @simctl_timeout.positive? path = gtimeout_path if path.empty? puts "warning: simctl_timeout specified but 'gtimeout' is not installed. The specified timeout will be ignored." return [] end [path, '-k', '5', @simctl_timeout.to_s] end
gtimeout_path()
click to toggle source
# File lib/xcknife/test_dumper.rb, line 356 def gtimeout_path `which gtimeout`.strip end
inject_vars(env, test_host)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 366 def inject_vars(env, test_host) env.each do |k, v| env[k] = replace_vars(v || '', test_host) end end
install_app(test_host_path)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 378 def install_app(test_host_path) retries_count = 0 max_retry_count = 3 until (retries_count > max_retry_count) || call_simctl(['install', @device_id, test_host_path]) retries_count += 1 call_simctl ['shutdown', @device_id] call_simctl ['boot', @device_id] sleep 1.0 end raise TestDumpError, "Installing #{test_host_path} failed" if retries_count > max_retry_count end
list_single_test(list_folder, test_bundle, test_bundle_name)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 303 def list_single_test(list_folder, test_bundle, test_bundle_name) output_methods(list_folder, test_bundle, test_bundle_name) do [{ class: test_bundle_name, method: 'test' }] end end
list_tests(xctestrun, list_folder, extra_environment_variables)
click to toggle source
This executes naive test dumping in parallel by queueing up items onto a work queue to process with 1 new thread per processor. Results are placed onto a threadsafe spec queue to avoid writing to an object between threads, then popped off re-inserting them to our list of test results. rubocop:disable Metrics/CyclomaticComplexity
# File lib/xcknife/test_dumper.rb, line 201 def list_tests(xctestrun, list_folder, extra_environment_variables) xctestrun.reject! { |test_bundle_name, _| test_bundle_name == '__xctestrun_metadata__' } test_runs_by_method = test_groups(xctestrun) spec_queue = Queue.new nm_bundle_queue = Queue.new results = [] single_tests = test_runs_by_method['single'] || [] nm_tests = test_runs_by_method['nm'] || [] simctl_tests = test_runs_by_method['simctl'] || [] single_tests.each do |test_bundle_name, test_bundle| logger.info { "Skipping dumping tests in `#{test_bundle_name}` -- writing out fake event" } spec_queue << list_single_test(list_folder, test_bundle, test_bundle_name) end simctl_tests.each do |test_bundle_name, test_bundle| test_spec = list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables) wait_test_dumper_completion(test_spec.json_stream_file) spec_queue << test_spec end nm_tests.each { |item| nm_bundle_queue << item } [Etc.nprocessors, nm_bundle_queue.size].min.times.map do nm_bundle_queue << :stop Thread.new do Thread.current.abort_on_exception = true until (item = nm_bundle_queue.pop) == :stop test_bundle_name, test_bundle = item spec_queue << list_tests_with_nm(list_folder, test_bundle, test_bundle_name) end end end.each(&:join) results << spec_queue.pop until spec_queue.empty? results end
list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
click to toggle source
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
# File lib/xcknife/test_dumper.rb, line 289 def list_tests_with_nm(list_folder, test_bundle, test_bundle_name) output_methods(list_folder, test_bundle, test_bundle_name) do |test_bundle_path| methods = [] swift_demangled_nm(test_bundle_path) do |output| output.each_line do |line| next unless (method = method_from_nm_line(line)) methods << method end end methods end end
list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
click to toggle source
rubocop:enable Metrics/CyclomaticComplexity
# File lib/xcknife/test_dumper.rb, line 245 def list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables) env_variables = test_bundle['EnvironmentVariables'] testing_env_variables = test_bundle['TestingEnvironmentVariables'] outpath = File.join(list_folder, test_bundle_name) test_host = replace_vars(test_bundle['TestHostPath']) test_bundle_path = replace_vars(test_bundle['TestBundlePath'], test_host) test_dumper_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'TestDumper', 'TestDumper.dylib')) raise TestDumpError, "Could not find TestDumper.dylib on #{test_dumper_path}" unless File.exist?(test_dumper_path) is_logic_test = test_bundle['TestHostBundleIdentifier'].nil? env = simctl_child_attrs( 'XCTEST_TYPE' => xctest_type(test_bundle), 'XCTEST_TARGET' => test_bundle_name, 'TestDumperOutputPath' => outpath, 'IDE_INJECTION_PATH' => testing_env_variables['DYLD_INSERT_LIBRARIES'], 'XCInjectBundleInto' => testing_env_variables['XCInjectBundleInto'], 'XCInjectBundle' => test_bundle_path, 'TestBundleLocation' => test_bundle_path, 'OS_ACTIVITY_MODE' => 'disable', 'DYLD_PRINT_LIBRARIES' => 'YES', 'DYLD_PRINT_ENV' => 'YES', 'DYLD_ROOT_PATH' => @sdk_path, 'DYLD_LIBRARY_PATH' => env_variables['DYLD_LIBRARY_PATH'], 'DYLD_FRAMEWORK_PATH' => env_variables['DYLD_FRAMEWORK_PATH'], 'DYLD_FALLBACK_LIBRARY_PATH' => "#{@sdk_path}/usr/lib", 'DYLD_FALLBACK_FRAMEWORK_PATH' => "#{@platform_path}/Developer/Library/Frameworks", 'DYLD_INSERT_LIBRARIES' => test_dumper_path ) env.merge!(simctl_child_attrs(extra_environment_variables)) inject_vars(env, test_host) FileUtils.rm_f(outpath) logger.info { "Temporary TestDumper file for #{test_bundle_name} is #{outpath}" } if is_logic_test run_logic_test(env, test_host, test_bundle_path) else install_app(test_host) test_host_bundle_identifier = replace_vars(test_bundle['TestHostBundleIdentifier'], test_host) run_apptest(env, test_host_bundle_identifier, test_bundle_path) end TestSpecification.new outpath, discover_tests_to_skip(test_bundle) end
method_from_nm_line(line)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 448 def method_from_nm_line(line) return unless line.strip =~ /^ [\da-f]+\s # address [tT]\s # symbol type (?: # method -\[(.+)\s(test.+)\] # objc instance method | # or swift instance method _? # only present on Xcode 10.0 and below (?:@objc\s)? # optional objc annotation (?:[^. ]+\.)? # module name ([^ ]+) # class name \.(test.+)\s->\s\(\) # method signature ) $/ox { class: Regexp.last_match(1) || Regexp.last_match(3), method: Regexp.last_match(2) || Regexp.last_match(4) } end
output_methods(list_folder, test_bundle, test_bundle_name) { |test_bundle_path| ... }
click to toggle source
# File lib/xcknife/test_dumper.rb, line 309 def output_methods(list_folder, test_bundle, test_bundle_name) outpath = File.join(list_folder, test_bundle_name) logger.info { "Writing out TestDumper file for #{test_bundle_name} to #{outpath}" } test_specification = TestSpecification.new outpath, discover_tests_to_skip(test_bundle) test_bundle_path = replace_vars(test_bundle['TestBundlePath'], replace_vars(test_bundle['TestHostPath'])) methods = yield(test_bundle_path) test_type = xctest_type(test_bundle) File.open test_specification.json_stream_file, 'a' do |f| f << JSON.dump(message: 'Starting Test Dumper', event: 'begin-test-suite', testType: test_type) << "\n" f << JSON.dump(event: 'begin-ocunit', bundleName: File.basename(test_bundle_path), targetName: test_bundle_name) << "\n" methods.map { |method| method[:class] }.uniq.each do |class_name| f << JSON.dump(test: '1', className: class_name, event: 'end-test', totalDuration: '0') << "\n" end f << JSON.dump(message: 'Completed Test Dumper', event: 'end-action', testType: test_type) << "\n" end test_specification end
replace_vars(str, testhost = '<UNKNOWN>')
click to toggle source
# File lib/xcknife/test_dumper.rb, line 360 def replace_vars(str, testhost = '<UNKNOWN>') str.gsub('__PLATFORMS__', @platforms_path) .gsub('__TESTHOST__', testhost) .gsub('__TESTROOT__', testroot) end
run_apptest(env, test_host_bundle_identifier, test_bundle_path)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 408 def run_apptest(env, test_host_bundle_identifier, test_bundle_path) return if call_simctl(['launch', @device_id, test_host_bundle_identifier, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env) raise TestDumpError, "Launching #{test_bundle_path} in #{test_host_bundle_identifier} failed" end
run_logic_test(env, test_host, test_bundle_path)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 414 def run_logic_test(env, test_host, test_bundle_path) opts = @debug ? {} : { err: '/dev/null' } return if call_simctl(['spawn', @device_id, test_host, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env, **opts) raise TestDumpError, "Spawning #{test_bundle_path} in #{test_host} failed" end
simctl()
click to toggle source
# File lib/xcknife/test_dumper.rb, line 336 def simctl @simctl_path end
simctl_child_attrs(attrs = {})
click to toggle source
# File lib/xcknife/test_dumper.rb, line 372 def simctl_child_attrs(attrs = {}) env = {} attrs.each { |k, v| env["SIMCTL_CHILD_#{k}"] = v } env end
swift_demangled_nm(test_bundle_path, &block)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 444 def swift_demangled_nm(test_bundle_path, &block) Open3.pipeline_r([@nm_path, File.join(test_bundle_path, File.basename(test_bundle_path, '.xctest'))], [@swift_path, 'demangle'], &block) end
test_dumper_terminated?(file)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 401 def test_dumper_terminated?(file) return false unless File.exist?(file) last_line = `tail -n 1 "#{file}"` last_line.include?('Completed Test Dumper') end
test_groups(xctestrun)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 185 def test_groups(xctestrun) xctestrun.group_by do |test_bundle_name, _test_bundle| if @skip_dump_bundle_names.include?(test_bundle_name) 'single' elsif @naive_dump_bundle_names.include?(test_bundle_name) 'nm' else 'simctl' end end end
wait_test_dumper_completion(file)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 391 def wait_test_dumper_completion(file) retries_count = 0 until test_dumper_terminated?(file) retries_count += 1 raise TestDumpError, "Timeout error on: #{file}" if retries_count == @max_retry_count sleep 0.1 end end
wrapped_simctl(args)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 340 def wrapped_simctl(args) [*gtimeout, simctl] + args end
xctest_type(test_bundle)
click to toggle source
# File lib/xcknife/test_dumper.rb, line 436 def xctest_type(test_bundle) if test_bundle['TestHostBundleIdentifier'].nil? 'LOGICTEST' else 'APPTEST' end end