run_loop 0.2.1 → 1.0.0.pre3

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 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