run_loop 0.2.1 → 1.0.0.pre3

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: d55b4a44c450ce991b37aa481254f80a9b7f8cc4
4
- data.tar.gz: 222e967b3c709961e0c753b4e8ba91dc04071fde
3
+ metadata.gz: 8fbeb69acc86dff93a612295675c56e5c60b9286
4
+ data.tar.gz: 3d8639044fcbf76a09a8b1dc3ec426ad6dcae537
5
5
  SHA512:
6
- metadata.gz: 764f0823c6d20c9aca52cd552399629e7d4830fd877abea5862ff854fb55c41fc2437aff2ad697d40d040c8b619102d7dc80e02568e7b4c8f8b755c1ead806d2
7
- data.tar.gz: 48fbe055e7df6e20636596c14118e2c873e16ddf702b7ec4dbaf7129683e51b58824501fe37fb732cd3f96d839d8d5e957145f1e45b00f64929253447bb7a47a
6
+ metadata.gz: 521d737aa170ac8ef6be590b47024aced47db7b8698641a0124b1bc5bff4a6fb7e6207ca01039039cea551e2776c8ae4f55e14ce61f3ad93573a0b6293f36f0e
7
+ data.tar.gz: 4c22b25f74512d2590efd9a0ee79c7c8d776319672f938215acbae5861ca0163b706311ee05d5569951ec6fc968168d9517b77dd83ebaf558a7ba517e86d9090
data/lib/run_loop.rb CHANGED
@@ -1,3 +1,6 @@
1
- require 'rubygems'
2
1
  require 'run_loop/core'
3
2
  require 'run_loop/version'
3
+ require 'run_loop/xctools'
4
+ require 'run_loop/plist_buddy'
5
+ require 'run_loop/sim_control'
6
+ require 'run_loop/device'
data/lib/run_loop/core.rb CHANGED
@@ -2,6 +2,7 @@ require 'fileutils'
2
2
  require 'tmpdir'
3
3
  require 'timeout'
4
4
  require 'json'
5
+ require 'open3'
5
6
 
6
7
  module RunLoop
7
8
 
@@ -24,6 +25,23 @@ module RunLoop
24
25
  SCRIPTS_PATH
25
26
  end
26
27
 
28
+ # @deprecated since 1.0.0
29
+ # still used extensively in calabash-ios launcher
30
+ def self.above_or_eql_version?(target_version, xcode_version)
31
+ if target_version.is_a?(RunLoop::Version)
32
+ target = target_version
33
+ else
34
+ target = RunLoop::Version.new(target_version)
35
+ end
36
+
37
+ if xcode_version.is_a?(RunLoop::Version)
38
+ xcode = xcode_version
39
+ else
40
+ xcode = RunLoop::Version.new(xcode_version)
41
+ end
42
+ target >= xcode
43
+ end
44
+
27
45
  def self.script_for_key(key)
28
46
  if SCRIPTS[key].nil?
29
47
  return nil
@@ -36,7 +54,7 @@ module RunLoop
36
54
  Timeout::timeout(1, TimeoutError) do
37
55
  return `#{File.join(scripts_path, 'udidetect')}`.chomp
38
56
  end
39
- rescue TimeoutError => e
57
+ rescue TimeoutError => _
40
58
  `killall udidetect &> /dev/null`
41
59
  end
42
60
  nil
@@ -46,6 +64,13 @@ module RunLoop
46
64
  before = Time.now
47
65
  ensure_instruments_not_running!
48
66
 
67
+ sim_control ||= options[:sim_control] || RunLoop::SimControl.new
68
+ xctools ||= options[:xctools] || sim_control.xctools
69
+
70
+ # @todo only enable accessibility when there is simulator target
71
+ # @todo only enable accessibility on the targeted simulator
72
+ sim_control.enable_accessibility_on_sims({:verbose => true})
73
+
49
74
  device_target = options[:udid] || options[:device_target] || detect_connected_device || 'simulator'
50
75
  if device_target && device_target.to_s.downcase == 'device'
51
76
  device_target = detect_connected_device
@@ -78,11 +103,12 @@ module RunLoop
78
103
 
79
104
 
80
105
  # Compute udid and bundle_dir / bundle_id from options and target depending on Xcode version
81
-
82
- udid, bundle_dir_or_bundle_id = udid_and_bundle_for_launcher(device_target, options)
106
+ udid, bundle_dir_or_bundle_id = udid_and_bundle_for_launcher(device_target, options, xctools)
83
107
 
84
108
  args = options.fetch(:args, [])
85
109
 
110
+ inject_dylib = options.fetch(:inject_dylib, (ENV['CALABASH_USE_DYLIB']=='1'))
111
+
86
112
  log_file ||= File.join(results_dir, 'run_loop.out')
87
113
 
88
114
  if ENV['DEBUG']=='1'
@@ -94,6 +120,7 @@ module RunLoop
94
120
  puts "log_file=#{log_file}"
95
121
  puts "timeout=#{timeout}"
96
122
  puts "args=#{args}"
123
+ puts "inject_dylib=#{inject_dylib}"
97
124
  end
98
125
 
99
126
  after = Time.now
@@ -109,7 +136,8 @@ module RunLoop
109
136
  :results_dir => results_dir,
110
137
  :script => script,
111
138
  :log_file => log_file,
112
- :args => args))
139
+ :args => args),
140
+ xctools)
113
141
 
114
142
  log_header("Starting on #{device_target} App: #{bundle_dir_or_bundle_id}")
115
143
  cmd_str = cmd.join(' ')
@@ -134,11 +162,38 @@ module RunLoop
134
162
 
135
163
  uia_timeout = options[:uia_timeout] || (ENV['UIA_TIMEOUT'] && ENV['UIA_TIMEOUT'].to_f) || 10
136
164
 
165
+ raw_lldb_output = nil
137
166
  before = Time.now
138
167
  begin
139
168
  Timeout::timeout(timeout, TimeoutError) do
140
169
  read_response(run_loop, 0, uia_timeout)
141
170
  end
171
+ if inject_dylib
172
+ lldb_template_file = File.join(scripts_path,'calabash.lldb.erb')
173
+ require 'erb'
174
+ lldb_template = ::ERB.new(File.read(lldb_template_file))
175
+ lldb_template.filename = lldb_template_file
176
+
177
+ cf_bundle_executable = find_cf_bundle_executable(bundle_dir_or_bundle_id)
178
+ require 'calabash/dylibs'
179
+ dylib_path_for_target = Calabash::Dylibs.path_to_sim_dylib
180
+
181
+ lldb_cmd = lldb_template.result(binding)
182
+
183
+ tmpdir = Dir.mktmpdir('lldb_cmd')
184
+ lldb_script = File.join(tmpdir,'lldb')
185
+
186
+ File.open(lldb_script,'w') {|f| f.puts(lldb_cmd)}
187
+
188
+ if ENV['DEBUG'] == '1'
189
+ puts "lldb script #{lldb_script}"
190
+ end
191
+
192
+ raw_lldb_output = `lldb -s #{lldb_script}`
193
+ if ENV['DEBUG'] == '1'
194
+ puts raw_lldb_output
195
+ end
196
+ end
142
197
  rescue TimeoutError => e
143
198
  if ENV['DEBUG']
144
199
  puts "Failed to launch\n"
@@ -150,6 +205,7 @@ module RunLoop
150
205
  puts "log_file=#{log_file}"
151
206
  puts "timeout=#{timeout}"
152
207
  puts "args=#{args}"
208
+ puts "lldb_output=#{raw_lldb_output}" if raw_lldb_output
153
209
  end
154
210
  raise TimeoutError, "Time out waiting for UIAutomation run-loop to Start. \n Logfile #{log_file} \n\n #{File.read(log_file)}\n"
155
211
  end
@@ -163,7 +219,16 @@ module RunLoop
163
219
  run_loop
164
220
  end
165
221
 
166
- def self.udid_and_bundle_for_launcher(device_target, options)
222
+ def self.find_cf_bundle_executable(bundle_dir_or_bundle_id)
223
+ unless File.directory?(bundle_dir_or_bundle_id)
224
+ raise "Injecting dylibs currently only works with simulator and app bundles"
225
+ end
226
+ info_plist = Dir[File.join(bundle_dir_or_bundle_id,'Info.plist')].first
227
+ raise "Unable to find Info.plist in #{bundle_dir_or_bundle_id}" if info_plist.nil?
228
+ `/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "#{info_plist}"`.strip
229
+ end
230
+
231
+ def self.udid_and_bundle_for_launcher(device_target, options, xctools=RunLoop::XCTools.new)
167
232
  bundle_dir_or_bundle_id = options[:app] || ENV['BUNDLE_ID']|| ENV['APP_BUNDLE_PATH'] || ENV['APP']
168
233
 
169
234
  unless bundle_dir_or_bundle_id
@@ -171,16 +236,21 @@ module RunLoop
171
236
  end
172
237
 
173
238
  udid = nil
174
- if above_or_eql_version?('5.1', xcode_version)
239
+
240
+ if xctools.xcode_version_gte_51?
175
241
  if device_target.nil? || device_target.empty? || device_target == 'simulator'
176
- device_target = 'iPhone Retina (4-inch) - Simulator - iOS 7.1'
242
+ if xctools.xcode_version_gte_6?
243
+ # the simulator can be either the textual name or the UDID (directory name)
244
+ device_target = 'iPhone 5 (8.0 Simulator)'
245
+ else
246
+ device_target = 'iPhone Retina (4-inch) - Simulator - iOS 7.1'
247
+ end
177
248
  end
178
249
  udid = device_target
179
250
 
180
251
  unless /simulator/i.match(device_target)
181
252
  bundle_dir_or_bundle_id = options[:bundle_id] if options[:bundle_id]
182
253
  end
183
-
184
254
  else
185
255
  if device_target == 'simulator'
186
256
 
@@ -210,30 +280,9 @@ module RunLoop
210
280
  return udid, bundle_dir_or_bundle_id
211
281
  end
212
282
 
213
- def self.above_or_eql_version?(target_version, xcode_version)
214
- t_major,t_minor,t_patch = target_version.split('.')
215
- x_major,x_minor,x_patch = xcode_version.split('.')
216
- return true if x_major.to_i > t_major.to_i
217
- return false if x_major.to_i < t_major.to_i
218
- #major versions are equal
219
- t_minor_i = (t_minor && t_minor.to_i || 0)
220
- x_minor_i = (x_minor && x_minor.to_i || 0)
221
- return true if x_minor_i > t_minor_i
222
- return false if x_minor_i < t_minor_i
223
- #minor versions are equal
224
-
225
- t_patch_i = (t_patch && t_patch.to_i || 0)
226
- x_patch_i = (x_patch && x_patch.to_i || 0)
227
-
228
- x_patch_i >= t_patch_i
229
- end
230
-
231
- def self.xcode_version
232
- xcode_build_output = `xcodebuild -version`.split("\n")
233
- xcode_build_output.each do |line|
234
- match=/^Xcode\s(.*)$/.match(line.strip)
235
- return match[1] if match && match.length > 1
236
- end
283
+ # @deprecated 1.0.0 replaced with Xctools#version
284
+ def self.xcode_version(xctools=XCTools.new)
285
+ xctools.xcode_version.to_s
237
286
  end
238
287
 
239
288
  def self.jruby?
@@ -360,7 +409,7 @@ module RunLoop
360
409
  end
361
410
 
362
411
  def self.instruments_command_prefix(udid, results_dir_trace)
363
- instruments_path = 'instruments'
412
+ instruments_path = 'xcrun instruments'
364
413
  if udid
365
414
  instruments_path = "#{instruments_path} -w \"#{udid}\""
366
415
  end
@@ -369,7 +418,7 @@ module RunLoop
369
418
  end
370
419
 
371
420
 
372
- def self.instruments_command(options)
421
+ def self.instruments_command(options, xctools=XCTools.new)
373
422
  udid = options[:udid]
374
423
  results_dir_trace = options[:results_dir_trace]
375
424
  bundle_dir_or_bundle_id = options[:bundle_dir_or_bundle_id]
@@ -381,7 +430,7 @@ module RunLoop
381
430
  instruments_prefix = instruments_command_prefix(udid, results_dir_trace)
382
431
  cmd = [
383
432
  instruments_prefix,
384
- '-t', "\"#{automation_template}\"",
433
+ '-t', "\"#{automation_template(xctools)}\"",
385
434
  "\"#{bundle_dir_or_bundle_id}\"",
386
435
  '-e', 'UIARESULTSPATH', results_dir,
387
436
  '-e', 'UIASCRIPT', script,
@@ -393,21 +442,20 @@ module RunLoop
393
442
  cmd
394
443
  end
395
444
 
396
- def self.automation_template(candidate = ENV['TRACE_TEMPLATE'])
445
+ def self.automation_template(xctools, candidate = ENV['TRACE_TEMPLATE'])
397
446
  unless candidate && File.exist?(candidate)
398
- candidate = default_tracetemplate
447
+ candidate = default_tracetemplate xctools
399
448
  end
400
449
  candidate
401
450
  end
402
451
 
403
- def self.default_tracetemplate
404
- xcode_path = `xcode-select -print-path`.chomp
405
- automation_bundle = File.expand_path(File.join(xcode_path, '..', 'Applications', 'Instruments.app', 'Contents', 'PlugIns', 'AutomationInstrument.bundle'))
406
- if not File.exist? automation_bundle
407
- automation_bundle= File.expand_path(File.join(xcode_path, 'Platforms', 'iPhoneOS.platform', 'Developer', 'Library', 'Instruments', 'PlugIns', 'AutomationInstrument.bundle'))
408
- raise 'Unable to find AutomationInstrument.bundle' if not File.exist? automation_bundle
409
- end
410
- File.join(automation_bundle, 'Contents', 'Resources', 'Automation.tracetemplate')
452
+ def self.default_tracetemplate(xctools=XCTools.new)
453
+ templates = xctools.instruments :templates
454
+ templates.delete_if do |path|
455
+ not path =~ /\/Automation.tracetemplate/
456
+ end.delete_if do |path|
457
+ not path =~ /Xcode/
458
+ end.first.tr("\"", '').strip
411
459
  end
412
460
 
413
461
  def self.log(message)
@@ -467,7 +515,7 @@ module RunLoop
467
515
  result = Core.read_response(run_loop, expected_index)
468
516
  end
469
517
 
470
- rescue TimeoutError => e
518
+ rescue TimeoutError => _
471
519
  raise TimeoutError, "Time out waiting for UIAutomation run-loop for command #{cmd}. Waiting for index:#{expected_index}"
472
520
  end
473
521
 
@@ -0,0 +1,22 @@
1
+ module RunLoop
2
+ class Device
3
+
4
+ attr_reader :name
5
+ attr_reader :version
6
+ attr_reader :udid
7
+
8
+ def initialize(name, version, udid)
9
+ @name = name
10
+
11
+ if version.is_a? String
12
+ @version = RunLoop::Version.new version
13
+ else
14
+ @version = version
15
+ end
16
+
17
+ @udid = udid
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,173 @@
1
+ module RunLoop
2
+ # A class for reading and writing property list values.
3
+ #
4
+ # Why not use CFPropertyList? Because it is super wonky. Among its other
5
+ # faults, it matches Boolean to a string type with 'true/false' values which
6
+ # is problematic for our purposes.
7
+ class PlistBuddy
8
+
9
+ # Reads key from file and returns the result.
10
+ # @param [String] key the key to inspect (may not be nil or empty)
11
+ # @param [String] file the plist to read
12
+ # @param [Hash] opts options for controlling execution
13
+ # @option opts [Boolean] :verbose (false) controls log level
14
+ # @return [String] the value of the key
15
+ # @raise [ArgumentError] if nil or empty key
16
+ def plist_read(key, file, opts={})
17
+ if key.nil? or key.length == 0
18
+ raise(ArgumentError, "key '#{key}' must not be nil or empty")
19
+ end
20
+ cmd = build_plist_cmd(:print, {:key => key}, file)
21
+ res = execute_plist_cmd(cmd, opts)
22
+ if res == "Print: Entry, \":#{key}\", Does Not Exist"
23
+ nil
24
+ else
25
+ res
26
+ end
27
+ end
28
+
29
+ # Checks if the key exists in plist.
30
+ # @param [String] key the key to inspect (may not be nil or empty)
31
+ # @param [String] file the plist to read
32
+ # @param [Hash] opts options for controlling execution
33
+ # @option opts [Boolean] :verbose (false) controls log level
34
+ # @return [Boolean] true if the key exists in plist file
35
+ def plist_key_exists?(key, file, opts={})
36
+ plist_read(key, file, opts) != nil
37
+ end
38
+
39
+ # Replaces or creates the value of key in the file.
40
+ #
41
+ # @param [String] key the key to set (may not be nil or empty)
42
+ # @param [String] type the plist type (used only when adding a value)
43
+ # @param [String] value the new value
44
+ # @param [String] file the plist to read
45
+ # @param [Hash] opts options for controlling execution
46
+ # @option opts [Boolean] :verbose (false) controls log level
47
+ # @return [Boolean] true if the operation was successful
48
+ # @raise [ArgumentError] if nil or empty key
49
+ def plist_set(key, type, value, file, opts={})
50
+ default_opts = {:verbose => false}
51
+ merged = default_opts.merge(opts)
52
+
53
+ if key.nil? or key.length == 0
54
+ raise(ArgumentError, "key '#{key}' must not be nil or empty")
55
+ end
56
+
57
+ cmd_args = {:key => key,
58
+ :type => type,
59
+ :value => value}
60
+
61
+ if plist_key_exists?(key, file, merged)
62
+ cmd = build_plist_cmd(:set, cmd_args, file)
63
+ else
64
+ cmd = build_plist_cmd(:add, cmd_args, file)
65
+ end
66
+
67
+ res = execute_plist_cmd(cmd, merged)
68
+ res == ''
69
+ end
70
+
71
+ private
72
+
73
+ # returns the path to the PlistBuddy executable
74
+ # @return [String] path to PlistBuddy
75
+ def plist_buddy
76
+ '/usr/libexec/PlistBuddy'
77
+ end
78
+
79
+ # Executes cmd as a shell command and returns the result.
80
+ #
81
+ # @param [String] cmd shell command to execute
82
+ # @param [Hash] opts options for controlling execution
83
+ # @option opts [Boolean] :verbose (false) controls log level
84
+ # @return [Boolean,String] `true` if command was successful. If :print'ing
85
+ # the result, the value of the key. If there is an error, the output of
86
+ # stderr.
87
+ def execute_plist_cmd(cmd, opts={})
88
+ default_opts = {:verbose => false}
89
+ merged = default_opts.merge(opts)
90
+
91
+ puts cmd if merged[:verbose]
92
+
93
+ res = nil
94
+ # noinspection RubyUnusedLocalVariable
95
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
96
+ err = stderr.read
97
+ std = stdout.read
98
+ if not err.nil? and err != ''
99
+ res = err.chomp
100
+ else
101
+ res = std.chomp
102
+ end
103
+ end
104
+ res
105
+ end
106
+
107
+ # Composes a PlistBuddy command that can be executed as a shell command.
108
+ #
109
+ # @param [Symbol] type should be one of [:print, :set, :add]
110
+ #
111
+ # @param [Hash] args_hash arguments used to construct plist command
112
+ # @option args_hash [String] :key (required) the plist key
113
+ # @option args_hash [String] :value (required for :set and :add) the new value
114
+ # @option args_hash [String] :type (required for :add) the new type of the value
115
+ #
116
+ # @param [String] file the plist file to interact with (must exist)
117
+ #
118
+ # @raise [RuntimeError] if file does not exist
119
+ # @raise [ArgumentError] when invalid type is passed
120
+ # @raise [ArgumentError] when args_hash does not include required key/value pairs
121
+ #
122
+ # @return [String] a shell-ready PlistBuddy command
123
+ def build_plist_cmd(type, args_hash, file)
124
+
125
+ unless File.exist?(File.expand_path(file))
126
+ raise(RuntimeError, "plist '#{file}' does not exist - could not read")
127
+ end
128
+
129
+ case type
130
+ when :add
131
+ value_type = args_hash[:type]
132
+ unless value_type
133
+ raise(ArgumentError, ':value_type is a required key for :add command')
134
+ end
135
+ allowed_value_types = ['string', 'bool', 'real', 'integer']
136
+ unless allowed_value_types.include?(value_type)
137
+ raise(ArgumentError, "expected '#{value_type}' to be one of '#{allowed_value_types}'")
138
+ end
139
+ value = args_hash[:value]
140
+ unless value
141
+ raise(ArgumentError, ':value is a required key for :add command')
142
+ end
143
+ key = args_hash[:key]
144
+ unless key
145
+ raise(ArgumentError, ':key is a required key for :add command')
146
+ end
147
+ cmd_part = "\"Add :#{key} #{value_type} #{value}\""
148
+ when :print
149
+ key = args_hash[:key]
150
+ unless key
151
+ raise(ArgumentError, ':key is a required key for :print command')
152
+ end
153
+ cmd_part = "\"Print :#{key}\""
154
+ when :set
155
+ value = args_hash[:value]
156
+ unless value
157
+ raise(ArgumentError, ':value is a required key for :set command')
158
+ end
159
+ key = args_hash[:key]
160
+ unless key
161
+ raise(ArgumentError, ':key is a required key for :set command')
162
+ end
163
+ cmd_part = "\"Set :#{key} #{value}\""
164
+ else
165
+ cmds = [:add, :print, :set]
166
+ raise(ArgumentError, "expected '#{type}' to be one of '#{cmds}'")
167
+ end
168
+
169
+ "#{plist_buddy} -c #{cmd_part} \"#{file}\""
170
+ end
171
+
172
+ end
173
+ end