run_loop 1.2.6 → 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab5bc95a6743db8faaae000d728b4af4c36401c2
4
- data.tar.gz: b00267da35da54a4bafdff8f18587bf187aa78f5
3
+ metadata.gz: 2654e1672654ee6ae0af23cb582fec43864992df
4
+ data.tar.gz: d481bf34a098f76d83d94826575b021ace8232ca
5
5
  SHA512:
6
- metadata.gz: 73d64d08b501e73dfa80e49c9446e164626d8bb39d7ccdc3fa1939d2db15163b14b7bbda41bd7bc53ea90c42b8cf97f17ab79c7e616d02ef6734015d6691c553
7
- data.tar.gz: d0a3b4661b426fe09a64252dea9214de136b496af4ab46d900c284c1f9ec59f96c620aedde16719f7b63ae4b16532a962aff064eca89b7b64196589284b95fd9
6
+ metadata.gz: db03ba41012e7bf8f5d4cefa4833a36c478d16f9359339e415eae29dfa4faf62e6fb3491ad301d872941bcb30a1ebb71c1aabd55f95ceb01210119f2f57f00ff
7
+ data.tar.gz: 30e5ec4733c0ef6da15aa590c9390c46e3a9a6fa5ddb08f4e609602ace3a05579ea76d538a132f4667708ea1d4cf83307aeb243573b90635cbd6609c8b0e21d3
data/lib/run_loop.rb CHANGED
@@ -1,10 +1,17 @@
1
+ require 'run_loop/environment'
2
+ require 'run_loop/process_terminator'
3
+ require 'run_loop/process_waiter'
4
+ require 'run_loop/lldb'
5
+ require 'run_loop/dylib_injector'
1
6
  require 'run_loop/core'
2
7
  require 'run_loop/version'
3
8
  require 'run_loop/xctools'
4
9
  require 'run_loop/plist_buddy'
10
+ require 'run_loop/app'
5
11
  require 'run_loop/sim_control'
6
12
  require 'run_loop/device'
7
13
  require 'run_loop/instruments'
8
14
  require 'run_loop/lipo'
9
15
  require 'run_loop/host_cache'
10
16
  require 'run_loop/monkey_patch'
17
+ require 'run_loop/simctl/bridge'
@@ -0,0 +1,67 @@
1
+ module RunLoop
2
+ # A class for interacting with .app bundles.
3
+ class App
4
+
5
+ # @!attribute [r] path
6
+ # @return [String] The path to the app bundle .app
7
+ attr_reader :path
8
+
9
+ # Creates a new App instance.
10
+ #
11
+ # @note The `app_bundle_path` is expanded during initialization.
12
+ #
13
+ # @param [String] app_bundle_path A path to a .app
14
+ # @return [RunLoop::App] A instance of App with a path.
15
+ def initialize(app_bundle_path)
16
+ @path = File.expand_path(app_bundle_path)
17
+ end
18
+
19
+ # Is this a valid app?
20
+ def valid?
21
+ [File.exist?(path),
22
+ File.directory?(path),
23
+ File.extname(path) == '.app'].all?
24
+ end
25
+
26
+ # Returns the Info.plist path.
27
+ # @raise [RuntimeError] If there is no Info.plist.
28
+ def info_plist_path
29
+ info_plist = File.join(path, 'Info.plist')
30
+ unless File.exist?(info_plist)
31
+ raise "Expected an Info.plist at '#{path}'"
32
+ end
33
+ info_plist
34
+ end
35
+
36
+ # Inspects the app's Info.plist for the bundle identifier.
37
+ # @return [String] The value of CFBundleIdentifier.
38
+ # @raise [RuntimeError] If the plist cannot be read or the
39
+ # CFBundleIdentifier is empty or does not exist.
40
+ def bundle_identifier
41
+ identifier = plist_buddy.plist_read('CFBundleIdentifier', info_plist_path)
42
+ unless identifier
43
+ raise "Expected key 'CFBundleIdentifier' in '#{info_plist_path}'"
44
+ end
45
+ identifier
46
+ end
47
+
48
+ # Inspects the app's Info.plist for the executable name.
49
+ # @return [String] The value of CFBundleIdentifier.
50
+ # @raise [RuntimeError] If the plist cannot be read or the
51
+ # CFBundleExecutable is empty or does not exist.
52
+ def executable_name
53
+ identifier = plist_buddy.plist_read('CFBundleExecutable', info_plist_path)
54
+ unless identifier
55
+ raise "Expected key 'CFBundleExecutable' in '#{info_plist_path}'"
56
+ end
57
+ identifier
58
+ end
59
+
60
+ private
61
+
62
+ def plist_buddy
63
+ @plist_buddy ||= RunLoop::PlistBuddy.new
64
+ end
65
+
66
+ end
67
+ end
data/lib/run_loop/core.rb CHANGED
@@ -135,7 +135,8 @@ module RunLoop
135
135
  sim_control ||= options[:sim_control] || RunLoop::SimControl.new
136
136
  xctools ||= options[:xctools] || sim_control.xctools
137
137
 
138
- RunLoop::Instruments.new.kill_instruments(xctools)
138
+ instruments = RunLoop::Instruments.new
139
+ instruments.kill_instruments
139
140
 
140
141
  device_target = options[:udid] || options[:device_target] || detect_connected_device || 'simulator'
141
142
  if device_target && device_target.to_s.downcase == 'device'
@@ -157,7 +158,6 @@ module RunLoop
157
158
 
158
159
  script = File.join(results_dir, '_run_loop.js')
159
160
 
160
-
161
161
  code = File.read(options[:script])
162
162
  code = code.gsub(/\$PATH/, results_dir)
163
163
  code = code.gsub(/\$READ_SCRIPT_PATH/, READ_SCRIPT_PATH)
@@ -186,10 +186,6 @@ module RunLoop
186
186
 
187
187
  args = options.fetch(:args, [])
188
188
 
189
- inject_dylib = self.dylib_path_from_options options
190
- # WIP This is brute-force call against all lldb processes.
191
- self.ensure_lldb_not_running if inject_dylib
192
-
193
189
  log_file ||= File.join(results_dir, 'run_loop.out')
194
190
 
195
191
  after = Time.now
@@ -217,22 +213,11 @@ module RunLoop
217
213
 
218
214
  self.log_run_loop_options(merged_options, xctools)
219
215
 
220
- cmd = instruments_command(merged_options, xctools)
216
+ automation_template = automation_template(xctools)
221
217
 
222
218
  log_header("Starting on #{device_target} App: #{bundle_dir_or_bundle_id}")
223
- cmd_str = cmd.join(' ')
224
219
 
225
- log(cmd_str) if ENV['DEBUG'] == '1'
226
-
227
- if !jruby? && RUBY_VERSION && RUBY_VERSION.start_with?('1.8')
228
- pid = fork do
229
- exec(cmd_str)
230
- end
231
- else
232
- pid = spawn(cmd_str)
233
- end
234
-
235
- Process.detach(pid)
220
+ pid = instruments.spawn(automation_template, merged_options, log_file)
236
221
 
237
222
  File.open(File.join(results_dir, 'run_loop.pid'), 'w') do |f|
238
223
  f.write pid
@@ -249,7 +234,6 @@ module RunLoop
249
234
 
250
235
  uia_timeout = options[:uia_timeout] || (ENV['UIA_TIMEOUT'] && ENV['UIA_TIMEOUT'].to_f) || 10
251
236
 
252
- raw_lldb_output = nil
253
237
  before = Time.now
254
238
  begin
255
239
 
@@ -262,63 +246,25 @@ module RunLoop
262
246
  read_response(run_loop, 0, uia_timeout)
263
247
  end
264
248
  end
265
-
266
- # inject_dylib will be nil or a path to a dylib
267
- if inject_dylib
268
- lldb_template_file = File.join(scripts_path, 'calabash.lldb.erb')
269
- lldb_template = ::ERB.new(File.read(lldb_template_file))
270
- lldb_template.filename = lldb_template_file
271
-
272
- # Special!
273
- # These are required by the ERB in calabash.lldb.erb
274
- # noinspection RubyUnusedLocalVariable
275
- cf_bundle_executable = find_cf_bundle_executable(bundle_dir_or_bundle_id)
276
- # noinspection RubyUnusedLocalVariable
277
- dylib_path_for_target = inject_dylib
278
-
279
- lldb_cmd = lldb_template.result(binding)
280
-
281
- tmpdir = Dir.mktmpdir('lldb_cmd')
282
- lldb_script = File.join(tmpdir, 'lldb')
283
-
284
- File.open(lldb_script, 'w') { |f| f.puts(lldb_cmd) }
285
-
286
- if ENV['DEBUG'] == '1'
287
- puts "lldb script #{lldb_script}"
288
- puts "=== lldb script ==="
289
- counter = 0
290
- File.open(lldb_script, 'r').readlines.each { |line|
291
- puts "#{counter} #{line}"
292
- counter = counter + 1
293
- }
294
- puts "=== lldb script ==="
295
- end
296
-
297
- # Forcing a timeout. Do not retry here. If lldb is hanging,
298
- # RunLoop::Core.run* needs to be called again. Put another way,
299
- # instruments and lldb must be terminated.
300
- Retriable.retriable({:tries => 1, :timeout => 12, :interval => 1}) do
301
- raw_lldb_output = `xcrun lldb -s #{lldb_script}`
302
- if ENV['DEBUG'] == '1'
303
- puts raw_lldb_output
304
- end
305
- end
306
- end
307
249
  rescue TimeoutError => e
308
250
  if ENV['DEBUG'] == '1'
309
251
  puts "Failed to launch."
310
252
  puts "#{e}: #{e && e.message}"
311
- if raw_lldb_output
312
- puts "LLDB OUTPUT: #{raw_lldb_output}"
313
- end
314
253
  end
315
254
  raise TimeoutError, "Time out waiting for UIAutomation run-loop to Start. \n Logfile #{log_file} \n\n #{File.read(log_file)}\n"
316
255
  end
317
256
 
318
- after = Time.now()
319
-
320
257
  if ENV['DEBUG']=='1'
321
- puts "Launching took #{after-before} seconds"
258
+ puts "Launching took #{Time.now-before} seconds"
259
+ end
260
+
261
+ dylib_path = self.dylib_path_from_options(merged_options)
262
+
263
+ if dylib_path
264
+ RunLoop::LLDB.kill_lldb_processes
265
+ app = RunLoop::App.new(options[:app])
266
+ lldb = RunLoop::DylibInjector.new(app.executable_name, dylib_path)
267
+ lldb.retriable_inject_dylib
322
268
  end
323
269
 
324
270
  run_loop
@@ -392,15 +338,6 @@ module RunLoop
392
338
  dylib_path
393
339
  end
394
340
 
395
- def self.find_cf_bundle_executable(bundle_dir_or_bundle_id)
396
- unless File.directory?(bundle_dir_or_bundle_id)
397
- raise "Injecting dylibs currently only works with simulator and app bundles"
398
- end
399
- info_plist = Dir[File.join(bundle_dir_or_bundle_id, 'Info.plist')].first
400
- raise "Unable to find Info.plist in #{bundle_dir_or_bundle_id}" if info_plist.nil?
401
- `/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "#{info_plist}"`.strip
402
- end
403
-
404
341
  # Returns the a default simulator to target. This default needs to be one
405
342
  # that installed by default in the current Xcode version.
406
343
  #
@@ -409,7 +346,9 @@ module RunLoop
409
346
  # @param [RunLoop::XCTools] xcode_tools Used to detect the current xcode
410
347
  # version.
411
348
  def self.default_simulator(xcode_tools=RunLoop::XCTools.new)
412
- if xcode_tools.xcode_version_gte_62?
349
+ if xcode_tools.xcode_version_gte_63?
350
+ 'iPhone 5s (8.3 Simulator)'
351
+ elsif xcode_tools.xcode_version_gte_62?
413
352
  'iPhone 5s (8.2 Simulator)'
414
353
  elsif xcode_tools.xcode_version_gte_61?
415
354
  'iPhone 5s (8.1 Simulator)'
@@ -615,40 +554,6 @@ module RunLoop
615
554
  RunLoop::Instruments.new.instruments_pids(&block)
616
555
  end
617
556
 
618
- def self.instruments_command_prefix(udid, results_dir_trace)
619
- instruments_path = 'xcrun instruments'
620
- if udid
621
- instruments_path = "#{instruments_path} -w \"#{udid}\""
622
- end
623
- instruments_path << " -D \"#{results_dir_trace}\"" if results_dir_trace
624
- instruments_path
625
- end
626
-
627
-
628
- def self.instruments_command(options, xctools=RunLoop::XCTools.new)
629
- udid = options[:udid]
630
- results_dir_trace = options[:results_dir_trace]
631
- bundle_dir_or_bundle_id = options[:bundle_dir_or_bundle_id]
632
- results_dir = options[:results_dir]
633
- script = options[:script]
634
- log_file = options[:log_file]
635
- args= options[:args] || []
636
-
637
- instruments_prefix = instruments_command_prefix(udid, results_dir_trace)
638
- cmd = [
639
- instruments_prefix,
640
- '-t', "\"#{automation_template(xctools)}\"",
641
- "\"#{bundle_dir_or_bundle_id}\"",
642
- '-e', 'UIARESULTSPATH', results_dir,
643
- '-e', 'UIASCRIPT', script,
644
- args.join(' ')
645
- ]
646
- if log_file
647
- cmd << "&> #{log_file}"
648
- end
649
- cmd
650
- end
651
-
652
557
  def self.automation_template(xctools, candidate = ENV['TRACE_TEMPLATE'])
653
558
  unless candidate && File.exist?(candidate)
654
559
  candidate = default_tracetemplate xctools
@@ -710,22 +615,6 @@ module RunLoop
710
615
  def self.instruments_pids
711
616
  RunLoop::Instruments.new.instruments_pids
712
617
  end
713
-
714
- # @todo This is a WIP
715
- # @todo Needs rspec test
716
- def self.ensure_lldb_not_running
717
- descripts = `xcrun ps x -o pid,command | grep "lldb" | grep -v grep`.strip.split("\n")
718
- descripts.each do |process_desc|
719
- pid = process_desc.split(' ').first
720
- Open3.popen3("xcrun kill -9 #{pid} && xcrun wait #{pid}") do |_, stdout, stderr, _|
721
- out = stdout.read.strip
722
- err = stderr.read.strip
723
- next if out.to_s.empty? and err.to_s.empty?
724
- # there lots of 'ownership' problems trying to kill the lldb process
725
- #puts "kill process '#{pid}' => stdout: '#{out}' | stderr: '#{err}'"
726
- end
727
- end
728
- end
729
618
  end
730
619
 
731
620
  def self.default_script_for_uia_strategy(uia_strategy)
@@ -4,17 +4,29 @@ module RunLoop
4
4
  attr_reader :name
5
5
  attr_reader :version
6
6
  attr_reader :udid
7
+ attr_reader :state
7
8
 
8
- def initialize(name, version, udid)
9
+ # Create a new device.
10
+ #
11
+ # @param [String] name The name of the device. For sims this should be
12
+ # 'iPhone 5s' and for physical devices it will be the name the user gave
13
+ # to the device.
14
+ # @param [String, RunLoop::Version] version The iOS version that is running
15
+ # on the device. Can be a string or a Version instance.
16
+ # @param [String] udid The device identifier.
17
+ # @param [String] state (nil) This a simulator only value. It refers to
18
+ # the Booted/Shutdown/Creating state of the simulator. For pre-Xcode 6
19
+ # simulators, this value should be nil.
20
+ def initialize(name, version, udid, state=nil)
9
21
  @name = name
22
+ @udid = udid
23
+ @state = state
10
24
 
11
25
  if version.is_a? String
12
26
  @version = RunLoop::Version.new version
13
27
  else
14
28
  @version = version
15
29
  end
16
-
17
- @udid = udid
18
30
  end
19
31
 
20
32
  # Returns and instruments-ready device identifier that is a suitable value
@@ -0,0 +1,125 @@
1
+ module RunLoop
2
+
3
+ # @!visibility private
4
+ #
5
+ # This is experimental.
6
+ #
7
+ # Injects dylibs into running executables using lldb.
8
+ class DylibInjector
9
+
10
+ # @!attribute [r] process_name
11
+ # The name of the process to inject the dylib into. This should be obtained
12
+ # by inspecting the Info.plist in the app bundle.
13
+ # @return [String] The process_name
14
+ attr_reader :process_name
15
+
16
+ # @!attribute [r] dylib_path
17
+ # The path to the dylib that is to be injected.
18
+ # @return [String] The dylib_path
19
+ attr_reader :dylib_path
20
+
21
+ # Create a new dylib injector.
22
+ # @param [String] process_name The name of the process to inject the dylib
23
+ # into. This should be obtained by inspecting the Info.plist in the app
24
+ # bundle.
25
+ # @param [String] dylib_path The path the dylib to inject.
26
+ def initialize(process_name, dylib_path)
27
+ @process_name = process_name
28
+ @dylib_path = dylib_path
29
+ end
30
+
31
+ # Injects a dylib into a a currently running process.
32
+ def inject_dylib
33
+ debug_logging = RunLoop::Environment.debug?
34
+ puts "Starting lldb." if debug_logging
35
+
36
+ stderr_output = nil
37
+ lldb_status = nil
38
+ lldb_start_time = Time.now
39
+ Open3.popen3('sh') do |stdin, stdout, stderr, process_status|
40
+ stdin.puts 'xcrun lldb --no-lldbinit<<EOF'
41
+ stdin.puts "process attach -n '#{@process_name}'"
42
+ stdin.puts "expr (void*)dlopen(\"#{@dylib_path}\", 0x2)"
43
+ stdin.puts 'detach'
44
+ stdin.puts 'exit'
45
+ stdin.puts 'EOF'
46
+ stdin.close
47
+
48
+ puts "#{stdout.read}" if debug_logging
49
+
50
+ lldb_status = process_status
51
+ stderr_output = stderr.read.strip
52
+ end
53
+
54
+ pid = lldb_status.pid
55
+ exit_status = lldb_status.value.exitstatus
56
+
57
+ if stderr_output == ''
58
+ if debug_logging
59
+ puts "lldb '#{pid}' exited with value '#{exit_status}'."
60
+ puts "Took #{Time.now-lldb_start_time} for lldb to inject calabash dylib."
61
+ end
62
+ else
63
+ puts "#{stderr_output}"
64
+ if debug_logging
65
+ puts "lldb '#{pid}' exited with value '#{exit_status}'."
66
+ puts "lldb tried for #{Time.now-lldb_start_time} to inject calabash dylib before giving up."
67
+ end
68
+ end
69
+
70
+ stderr_output == ''
71
+ end
72
+
73
+ def inject_dylib_with_timeout(timeout)
74
+ success = false
75
+ Timeout.timeout(timeout) do
76
+ success = inject_dylib
77
+ end
78
+ success
79
+ end
80
+
81
+ def retriable_inject_dylib(options={})
82
+ merged_options = RETRIABLE_OPTIONS.merge(options)
83
+
84
+ debug_logging = RunLoop::Environment.debug?
85
+
86
+ on_retry = Proc.new do |_, try, elapsed_time, next_interval|
87
+ if debug_logging
88
+ # Retriable 2.0
89
+ if elapsed_time && next_interval
90
+ puts "LLDB: attempt #{try} failed in '#{elapsed_time}'; will retry in '#{next_interval}'"
91
+ else
92
+ puts "LLDB: attempt #{try} failed; will retry in '#{merged_options[:interval]}'"
93
+ end
94
+ end
95
+ RunLoop::LLDB.kill_lldb_processes
96
+ RunLoop::ProcessWaiter.new('lldb').wait_for_none
97
+ end
98
+
99
+ # For some reason, :timeout does not work here - the lldb process can
100
+ # hang indefinitely. Seems to work when
101
+ Retriable.retriable({:tries => merged_options[:retries],
102
+ # Retriable 2.0
103
+ :interval => merged_options[:interval],
104
+ :base_interval => merged_options[:interval],
105
+ :on_retry => on_retry}) do
106
+ #unless inject_dylib_with_timeout merged_options[:timeout]
107
+ unless inject_dylib_with_timeout merged_options[:timeout]
108
+ raise RuntimeError, "Could not inject dylib"
109
+ end
110
+ end
111
+ true
112
+ end
113
+
114
+ private
115
+
116
+ RETRIABLE_OPTIONS =
117
+ {
118
+ :retries => 3,
119
+ :timeout => 10,
120
+ # Retriable 2.0 replaces :interval with :base_interval
121
+ :interval => 2,
122
+ :base_interval => 2
123
+ }
124
+ end
125
+ end