run_loop 2.1.0.pre1 → 2.1.0

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: 7ea9c17ad6325b7381a689e16ea30c98b34e2765
4
- data.tar.gz: a1f044d25c196213e310f2f70f096f35bb558c65
3
+ metadata.gz: ee9ecaf4f1c6ec33be1a4e7640991c46e14f1938
4
+ data.tar.gz: 7348a649fd00ef921ccf16d049ca1f7c9c9b5421
5
5
  SHA512:
6
- metadata.gz: 49f63b7057fb4448850252365b840843b05a4ac39f81db367dd32a52d0b23dc0cdcb67c8de22e4838f02bfb81ef7f2bf3c189da4d4953a008f1812b6bd108326
7
- data.tar.gz: 30aed60ffb8069e15260ea81f02fa8a2261a59e473fcd4beaeacfcec940030671c6eb730423d088f73864cfc66daf18972df60a0cfc671013ed61e7f3b8f293f
6
+ metadata.gz: f2de3db8241ef24056e0d140d569c30082d0436acd251134b0b2908ef063a9dc97e9be2be8431701340c21e80bc3cd2978a41893ffb3b787231aaff191b8637f
7
+ data.tar.gz: 46c3ca51600d39434fee5a25d47b41c1122846f5e7eb3852e079174c1393acfa18df5e0cb9605899d6341b7cad1e3846212ad8348de1be1aad13af2622555010
data/lib/run_loop.rb CHANGED
@@ -31,7 +31,7 @@ require 'run_loop/cache/cache'
31
31
  require 'run_loop/host_cache'
32
32
  require 'run_loop/patches/awesome_print'
33
33
  require 'run_loop/core_simulator'
34
- require 'run_loop/simctl/plists'
34
+ require "run_loop/simctl"
35
35
  require 'run_loop/template'
36
36
  require "run_loop/locale"
37
37
  require "run_loop/language"
@@ -66,64 +66,69 @@ module RunLoop
66
66
 
67
67
  def self.run(options={})
68
68
 
69
- if RunLoop::Instruments.new.instruments_app_running?
70
- raise %q(The Instruments.app is open.
69
+ if options[:xcuitest]
70
+ RunLoop::XCUITest.run(options)
71
+ else
72
+
73
+ if RunLoop::Instruments.new.instruments_app_running?
74
+ raise %q(The Instruments.app is open.
71
75
 
72
76
  If the Instruments.app is open, the instruments command line tool cannot take
73
77
  control of your application.
74
78
 
75
79
  Please quit the Instruments.app and try again.)
76
80
 
77
- end
81
+ end
78
82
 
79
- uia_strategy = options[:uia_strategy]
80
- if options[:script]
81
- script = validate_script(options[:script])
82
- else
83
- if uia_strategy
84
- script = default_script_for_uia_strategy(uia_strategy)
83
+ uia_strategy = options[:uia_strategy]
84
+ if options[:script]
85
+ script = validate_script(options[:script])
85
86
  else
86
- if options[:calabash_lite]
87
- uia_strategy = :host
88
- script = Core.script_for_key(:run_loop_host)
89
- else
90
- uia_strategy = :preferences
87
+ if uia_strategy
91
88
  script = default_script_for_uia_strategy(uia_strategy)
89
+ else
90
+ if options[:calabash_lite]
91
+ uia_strategy = :host
92
+ script = Core.script_for_key(:run_loop_host)
93
+ else
94
+ uia_strategy = :preferences
95
+ script = default_script_for_uia_strategy(uia_strategy)
96
+ end
92
97
  end
93
98
  end
94
- end
95
- # At this point, 'script' has been chosen, but uia_strategy might not
96
- unless uia_strategy
97
- desired_script = options[:script]
98
- if desired_script.is_a?(String) #custom path to script
99
- uia_strategy = :host
100
- elsif desired_script == :run_loop_host
101
- uia_strategy = :host
102
- elsif desired_script == :run_loop_fast_uia
103
- uia_strategy = :preferences
104
- elsif desired_script == :run_loop_shared_element
105
- uia_strategy = :shared_element
106
- else
107
- raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
99
+ # At this point, 'script' has been chosen, but uia_strategy might not
100
+ unless uia_strategy
101
+ desired_script = options[:script]
102
+ if desired_script.is_a?(String) #custom path to script
103
+ uia_strategy = :host
104
+ elsif desired_script == :run_loop_host
105
+ uia_strategy = :host
106
+ elsif desired_script == :run_loop_fast_uia
107
+ uia_strategy = :preferences
108
+ elsif desired_script == :run_loop_shared_element
109
+ uia_strategy = :shared_element
110
+ else
111
+ raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
112
+ end
108
113
  end
109
- end
110
114
 
111
- # At this point script and uia_strategy selected
112
- cloned_options = options.clone
113
- cloned_options[:script] = script
114
- cloned_options[:uia_strategy] = uia_strategy
115
+ # At this point script and uia_strategy selected
116
+ cloned_options = options.clone
117
+ cloned_options[:script] = script
118
+ cloned_options[:uia_strategy] = uia_strategy
115
119
 
116
- # Xcode and SimControl will not be properly cloned and we don't want
117
- # them to be; we want to use the exact objects that were passed.
118
- if options[:xcode]
119
- cloned_options[:xcode] = options[:xcode]
120
- end
120
+ # Xcode and SimControl will not be properly cloned and we don't want
121
+ # them to be; we want to use the exact objects that were passed.
122
+ if options[:xcode]
123
+ cloned_options[:xcode] = options[:xcode]
124
+ end
121
125
 
122
- if options[:sim_control]
123
- cloned_options[:sim_control] = options[:sim_control]
124
- end
126
+ if options[:sim_control]
127
+ cloned_options[:sim_control] = options[:sim_control]
128
+ end
125
129
 
126
- Core.run_with_options(cloned_options)
130
+ Core.run_with_options(cloned_options)
131
+ end
127
132
  end
128
133
 
129
134
  def self.send_command(run_loop, cmd, options={timeout: 60}, num_retries=0, last_error=nil)
@@ -235,5 +240,4 @@ Please quit the Instruments.app and try again.)
235
240
  def self.log_info(*args)
236
241
  RunLoop::Logging.log_info(*args)
237
242
  end
238
-
239
243
  end
data/lib/run_loop/app.rb CHANGED
@@ -33,7 +33,20 @@ Bundle must:
33
33
 
34
34
  # @!visibility private
35
35
  def to_s
36
- "#<APP: #{path}>"
36
+ cf_bundle_version = bundle_version
37
+ cf_bundle_short_version = short_bundle_version
38
+
39
+ if cf_bundle_version && cf_bundle_short_version
40
+ version = "#{cf_bundle_version.to_s} / #{cf_bundle_short_version}"
41
+ elsif cf_bundle_version
42
+ version = cf_bundle_version.to_s
43
+ elsif cf_bundle_short_version
44
+ version = cf_bundle_short_version
45
+ else
46
+ version = ""
47
+ end
48
+
49
+ "#<APP #{bundle_identifier} #{version} #{path}>"
37
50
  end
38
51
 
39
52
  # @!visibility private
@@ -130,6 +143,75 @@ Bundle must:
130
143
  RunLoop::Codesign.distribution?(path)
131
144
  end
132
145
 
146
+ # Returns the CFBundleShortVersionString of the app as Version instance.
147
+ #
148
+ # Apple docs:
149
+ #
150
+ # CFBundleShortVersionString specifies the release version number of the
151
+ # bundle, which identifies a released iteration of the app. The release
152
+ # version number is a string comprised of three period-separated integers.
153
+ #
154
+ # The first integer represents major revisions to the app, such as revisions
155
+ # that implement new features or major changes. The second integer denotes
156
+ # revisions that implement less prominent features. The third integer
157
+ # represents maintenance releases.
158
+ #
159
+ # The value for this key differs from the value for CFBundleVersion, which
160
+ # identifies an iteration (released or unreleased) of the app. This key can
161
+ # be localized by including it in your InfoPlist.strings files.
162
+ #
163
+ # @return [RunLoop::Version, nil] Returns a Version instance if the
164
+ # CFBundleShortVersion string is well formed and nil if not.
165
+ def marketing_version
166
+ string = plist_buddy.plist_read("CFBundleShortVersionString", info_plist_path)
167
+ begin
168
+ version = RunLoop::Version.new(string)
169
+ rescue
170
+ if string && string != ""
171
+ RunLoop.log_debug("CFBundleShortVersionString: '#{string}' is not a well formed version string")
172
+ else
173
+ RunLoop.log_debug("CFBundleShortVersionString is not defined in Info.plist")
174
+ end
175
+ version = nil
176
+ end
177
+ version
178
+ end
179
+
180
+ # See #marketing_version
181
+ alias_method :short_bundle_version, :marketing_version
182
+
183
+ # Returns the CFBundleVersionString of the app as Version instance.
184
+ #
185
+ # Apple docs:
186
+ #
187
+ # CFBundleVersion specifies the build version number of the bundle, which
188
+ # identifies an iteration (released or unreleased) of the bundle. The build
189
+ # version number should be a string comprised of three non-negative,
190
+ # period-separated integers with the first integer being greater than zero.
191
+ # The string should only contain numeric (0-9) and period (.) characters.
192
+ # Leading zeros are truncated from each integer and will be ignored (that
193
+ # is, 1.02.3 is equivalent to 1.2.3).
194
+ #
195
+ # @return [RunLoop::Version, nil] Returns a Version instance if the
196
+ # CFBundleVersion string is well formed and nil if not.
197
+ def build_version
198
+ string = plist_buddy.plist_read("CFBundleVersionString", info_plist_path)
199
+ begin
200
+ version = RunLoop::Version.new(string)
201
+ rescue
202
+ if string && string != ""
203
+ RunLoop.log_debug("CFBundleVersionString: '#{string}' is not a well formed version string")
204
+ else
205
+ RunLoop.log_debug("CFBundleVersionString is not defined in Info.plist")
206
+ end
207
+ version = nil
208
+ end
209
+ version
210
+ end
211
+
212
+ # See #build_version
213
+ alias_method :bundle_version, :build_version
214
+
133
215
  # @!visibility private
134
216
  # Collects the paths to executables in the bundle.
135
217
  def executables
@@ -56,20 +56,25 @@ module RunLoop
56
56
  debug = options[:debug]
57
57
  device = options[:device]
58
58
 
59
+ manage_processes
60
+
59
61
  if device
60
62
  RunLoop::Environment.with_debugging(debug) do
63
+ RunLoop::CoreSimulator.erase(device)
61
64
  launch_simulator(device, xcode)
62
65
  end
63
66
  else
64
- launch_each_simulator
67
+ RunLoop::Environment.with_debugging(debug) do
68
+ erase_and_launch_each_simulator
69
+ end
65
70
  end
66
71
 
67
- manage_processes
68
72
  end
69
73
 
70
74
  no_commands do
71
- def launch_each_simulator
75
+ def erase_and_launch_each_simulator
72
76
  sim_control.simulators.each do |simulator|
77
+ RunLoop::CoreSimulator.erase(simulator)
73
78
  launch_simulator(simulator, xcode)
74
79
  end
75
80
  end
data/lib/run_loop/core.rb CHANGED
@@ -676,6 +676,8 @@ Logfile: #{log_file}
676
676
  #
677
677
  # 1. enabling accessibility and software keyboard
678
678
  # 2. installing / uninstalling apps
679
+ #
680
+ # TODO: move to CoreSimulator?
679
681
  def self.prepare_simulator(app, device, xcode, simctl, reset_options)
680
682
 
681
683
  # Validate the architecture.
@@ -57,7 +57,7 @@ class RunLoop::CoreSimulator
57
57
  # This process is a daemon, and requires 'KILL' to terminate.
58
58
  # Killing the process is fast, but it takes a long time to
59
59
  # restart.
60
- # ['com.apple.CoreSimulator.CoreSimulatorService', false],
60
+ ['com.apple.CoreSimulator.CoreSimulatorService', false],
61
61
 
62
62
  # Probably do not need to quit this, but it is tempting to do so.
63
63
  #['com.apple.CoreSimulator.SimVerificationService', false],
@@ -94,6 +94,12 @@ class RunLoop::CoreSimulator
94
94
  # launchd_sim process.
95
95
  ['launchd_sim', false],
96
96
 
97
+ # Required for XCUITest termination; the simulator hangs otherwise.
98
+ ["xpcproxy", false],
99
+
100
+ # Causes crash reports on Xcode < 7.0
101
+ ["apsd", true],
102
+
97
103
  # assetsd instances clobber each other and are not properly
98
104
  # killed when quiting the simulator.
99
105
  ['assetsd', false],
@@ -167,7 +173,7 @@ class RunLoop::CoreSimulator
167
173
  #
168
174
  # @param [RunLoop::Device] simulator The simulator to erase
169
175
  # @param [Hash] options Control the behavior of the method.
170
- # @option options [Numeric] :timout (180) How long tow wait for simctl to
176
+ # @option options [Numeric] :timeout (180) How long tow wait for simctl to
171
177
  # shutdown and erase the simulator. The timeout is apply separately to
172
178
  # each command.
173
179
  #
@@ -393,25 +399,46 @@ $ bundle exec run-loop simctl manage-processes
393
399
  # relaunch it.
394
400
  launch_simulator
395
401
 
396
- args = ['simctl', 'launch', device.udid, app.bundle_identifier]
397
- timeout = DEFAULT_OPTIONS[:launch_app_timeout]
398
- hash = xcrun.exec(args, log_cmd: true, timeout: timeout)
402
+ tries = RunLoop::Environment.ci? ? 5 : 3
403
+ last_error = nil
399
404
 
400
- exit_status = hash[:exit_status]
405
+ RunLoop.log_debug("Trying #{tries} times to launch #{app.bundle_identifier} on #{device}")
401
406
 
402
- if exit_status != 0
403
- RunLoop.log_error(hash[:out])
404
- raise RuntimeError, "Could not launch #{app.bundle_identifier} on #{device}"
407
+ tries.times do
408
+ hash = launch_app_with_simctl
409
+ exit_status = hash[:exit_status]
410
+ if exit_status != 0
411
+ RunLoop.log_debug("Failed to launch app.")
412
+ out.split($-0).each do |line|
413
+ RunLoop.log_debug(" #{line}")
414
+ end
415
+ # Simulator is probably in a bad state, but this will be super disruptive.
416
+ # Let's try a softer approach first - sleep.
417
+ # self.terminate_core_simulator_processes
418
+ sleep(0.5)
419
+ last_error = out
420
+ else
421
+ last_error = nil
422
+ break
423
+ end
424
+ end
425
+
426
+ if last_error
427
+ raise RuntimeError, %Q[Could not launch #{app.bundle_identifier} on #{device}
428
+
429
+ #{last_error}
430
+
431
+ ]
405
432
  end
406
433
 
407
434
  options = {
408
- :timeout => 10,
409
- :raise_on_timeout => true
435
+ :timeout => 10,
436
+ :raise_on_timeout => true
410
437
  }
411
438
 
412
439
  RunLoop::ProcessWaiter.new(app.executable_name, options).wait_for_any
413
-
414
440
  device.simulator_wait_for_stable_state
441
+
415
442
  true
416
443
  end
417
444
 
@@ -588,6 +615,13 @@ Command had no output
588
615
  installed_app_bundle_dir
589
616
  end
590
617
 
618
+ # @!visibility private
619
+ def launch_app_with_simctl
620
+ args = ['simctl', 'launch', device.udid, app.bundle_identifier]
621
+ timeout = DEFAULT_OPTIONS[:launch_app_timeout]
622
+ xcrun.exec(args, log_cmd: true, timeout: timeout)
623
+ end
624
+
591
625
  # Required for support of iOS 7 CoreSimulators. Can be removed when
592
626
  # Xcode support is dropped.
593
627
  def sdk_gte_8?
@@ -716,15 +750,20 @@ Command had no output
716
750
  def installed_app_bundle_dir
717
751
  sim_app_dir = device_applications_dir
718
752
  return nil if !File.exist?(sim_app_dir)
719
- Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
720
- RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
753
+
754
+ if xcode.version_gte_7?
755
+ simctl = RunLoop::Simctl.new(device)
756
+ simctl.app_container(app.bundle_identifier)
757
+ else
758
+ Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
759
+ RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
760
+ end
721
761
  end
722
762
  end
723
763
 
724
764
  # 1. Does nothing if the app is not installed.
725
765
  # 2. Does nothing if the app the same as the app that is installed
726
766
  # 3. Installs app if it is different from the installed app
727
- #
728
767
  def ensure_app_same
729
768
  installed_app_bundle = installed_app_bundle_dir
730
769
 
@@ -229,6 +229,27 @@ module RunLoop
229
229
  value = ENV["CI"]
230
230
  !!value && value != ''
231
231
  end
232
+
233
+ # !@visibility private
234
+ # Returns the value of CBXWS. This can be used to override the default
235
+ # CBXDriver.xcworkspace. You should only set this if you are actively
236
+ # developing the CBXDriver.
237
+ def self.cbxws
238
+ value = ENV["CBXWS"]
239
+ if value.nil? || value == ""
240
+ nil
241
+ else
242
+ path = File.expand_path(value)
243
+ if !File.directory?(path)
244
+ raise RuntimeError, %Q[CBXWS is set, but there is no workspace at
245
+ #{path}
246
+
247
+ Only set CBXWS if you are developing new features in the CBXRunner.
248
+
249
+ Check your environment.]
250
+ end
251
+ path
252
+ end
253
+ end
232
254
  end
233
255
  end
234
-
@@ -101,6 +101,10 @@ module RunLoop
101
101
  request(request, :post, options)
102
102
  end
103
103
 
104
+ def delete(request, options={})
105
+ request(request, :delete, options)
106
+ end
107
+
104
108
  private
105
109
 
106
110
  def request(request, request_method, options={})
@@ -134,7 +138,7 @@ module RunLoop
134
138
  return client.send(request_method, @server.endpoint + request.route,
135
139
  request.params, header)
136
140
  rescue *RETRY_ON => e
137
- RunLoop.log_debug("Rescued http error: #{e}")
141
+ #RunLoop.log_debug("Rescued http error: #{e}")
138
142
 
139
143
  if first_try
140
144
  if @on_error[e.class]
@@ -124,15 +124,11 @@ module RunLoop
124
124
  # Send a kill signal to any running `instruments` processes.
125
125
  #
126
126
  # Only one instruments process can be running at any one time.
127
- #
128
- # @param [RunLoop::Xcode] xcode Used to make check the
129
- # active Xcode version.
130
- def kill_instruments(xcode = RunLoop::Xcode.new)
131
- kill_signal = kill_signal(xcode)
127
+ def kill_instruments(_=nil)
132
128
  instruments_pids.each do |pid|
133
- terminator = RunLoop::ProcessTerminator.new(pid, kill_signal, 'instruments')
129
+ terminator = RunLoop::ProcessTerminator.new(pid, "QUIT", "instruments")
134
130
  unless terminator.kill_process
135
- terminator = RunLoop::ProcessTerminator.new(pid, 'KILL', 'instruments')
131
+ terminator = RunLoop::ProcessTerminator.new(pid, "KILL", "instruments")
136
132
  terminator.kill_process
137
133
  end
138
134
  end
@@ -369,29 +365,6 @@ module RunLoop
369
365
  end.compact.sort
370
366
  end
371
367
 
372
- # @!visibility private
373
- # The kill signal should be sent to instruments.
374
- #
375
- # When testing against iOS 8, sending -9 or 'TERM' causes the ScriptAgent
376
- # process on the device to emit the following error until the device is
377
- # rebooted.
378
- #
379
- # ```
380
- # MobileGestaltHelper[909] <Error>: libMobileGestalt MobileGestalt.c:273: server_access_check denied access to question UniqueDeviceID for pid 796

381
- # ScriptAgent[796] <Error>: libMobileGestalt MobileGestaltSupport.m:170: pid 796 (ScriptAgent) does not have sandbox access for re6Zb+zwFKJNlkQTUeT+/w and IS NOT appropriately entitled
382
- # ScriptAgent[703] <Error>: libMobileGestalt MobileGestalt.c:534: no access to UniqueDeviceID (see <rdar://problem/11744455>)
383
- # ```
384
- #
385
- # @see https://github.com/calabash/run_loop/issues/34
386
- #
387
- # @param [RunLoop::Xcode] xcode The Xcode tools to use to determine
388
- # what version of Xcode is active.
389
- # @return [String] Either 'QUIT' or 'TERM', depending on the Xcode
390
- # version.
391
- def kill_signal(xcode = RunLoop::Xcode.new)
392
- xcode.version_gte_6? ? 'QUIT' : 'TERM'
393
- end
394
-
395
368
  # @!visibility private
396
369
  #
397
370
  # Execute an instruments command.
data/lib/run_loop/ipa.rb CHANGED
@@ -25,7 +25,20 @@ module RunLoop
25
25
 
26
26
  # @!visibility private
27
27
  def to_s
28
- "#<IPA: #{bundle_identifier}: '#{path}'>"
28
+ cf_bundle_version = bundle_version
29
+ cf_bundle_short_version = short_bundle_version
30
+
31
+ if cf_bundle_version && cf_bundle_short_version
32
+ version = "#{cf_bundle_version.to_s}/#{cf_bundle_short_version}"
33
+ elsif cf_bundle_version
34
+ version = cf_bundle_version.to_s
35
+ elsif cf_bundle_short_version
36
+ version = cf_bundle_short_version
37
+ else
38
+ version = ""
39
+ end
40
+
41
+ "#<IPA #{bundle_identifier} #{version} #{path}>"
29
42
  end
30
43
 
31
44
  # @!visibility private
@@ -71,6 +84,22 @@ module RunLoop
71
84
  app.distribution_signed?
72
85
  end
73
86
 
87
+ # @!visibility private
88
+ def marketing_version
89
+ app.marketing_version
90
+ end
91
+
92
+ # See #marketing_version
93
+ alias_method :short_bundle_version, :marketing_version
94
+
95
+ # @!visibility private
96
+ def build_version
97
+ app.build_version
98
+ end
99
+
100
+ # See #build_version
101
+ alias_method :bundle_version, :build_version
102
+
74
103
  private
75
104
 
76
105
  # @!visibility private
@@ -0,0 +1,89 @@
1
+ module RunLoop
2
+
3
+ # @!visibility private
4
+ # An interface to the `simctl` command line tool for CoreSimulator.
5
+ #
6
+ # Replacement for SimControl.
7
+ class Simctl
8
+
9
+ # @!visibility private
10
+ DEFAULTS = {
11
+ :timeout => RunLoop::Environment.ci? ? 90 : 30,
12
+ :log_cmd => true
13
+ }
14
+
15
+ # @!visibility private
16
+ SIMCTL_PLIST_DIR = lambda {
17
+ dirname = File.dirname(__FILE__)
18
+ joined = File.join(dirname, '..', '..', 'plists', 'simctl')
19
+ File.expand_path(joined)
20
+ }.call
21
+
22
+ # @!visibility private
23
+ def self.uia_automation_plist
24
+ File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomation.plist')
25
+ end
26
+
27
+ # @!visibility private
28
+ def self.uia_automation_plugin_plist
29
+ File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomationPlugIn.plist')
30
+ end
31
+
32
+ # @!visibility private
33
+ attr_accessor :device
34
+
35
+ # @!visibility private
36
+ #
37
+ # @param [RunLoop::Device] device Cannot be nil.
38
+ def initialize(device)
39
+ @device = device
40
+ end
41
+
42
+ # @!visibility private
43
+ def to_s
44
+ "#<Simctl: #{device.name} #{device.udid}>"
45
+ end
46
+
47
+ # @!visibility private
48
+ def inspect
49
+ to_s
50
+ end
51
+
52
+ # @!visibility private
53
+ #
54
+ # This method is not supported on Xcode < 7 - returns nil.
55
+ #
56
+ # @param [String] bundle_id The CFBundleIdentifier of the app.
57
+ # @return [String] The path to the .app bundle if it exists; nil otherwise.
58
+ def app_container(bundle_id)
59
+ return nil if !xcode.version_gte_7?
60
+ cmd = ["simctl", "get_app_container", device.udid, bundle_id]
61
+ hash = execute(cmd, DEFAULTS)
62
+
63
+ exit_status = hash[:exit_status]
64
+ if exit_status != 0
65
+ nil
66
+ else
67
+ hash[:out].strip
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # @!visibility private
74
+ def execute(array, options)
75
+ merged = DEFAULTS.merge(options)
76
+ xcrun.exec(array, merged)
77
+ end
78
+
79
+ # @!visibility private
80
+ def xcrun
81
+ @xcrun ||= RunLoop::Xcrun.new
82
+ end
83
+
84
+ # @!visibility private
85
+ def xcode
86
+ @xcode ||= RunLoop::Xcode.new
87
+ end
88
+ end
89
+ end
@@ -1,5 +1,5 @@
1
1
  module RunLoop
2
- VERSION = "2.1.0.pre1"
2
+ VERSION = "2.1.0"
3
3
 
4
4
  # A model of a software release version that can be used to compare two versions.
5
5
  #
@@ -3,20 +3,44 @@ module RunLoop
3
3
  # @!visibility private
4
4
  class XCUITest
5
5
 
6
+ class HTTPError < RuntimeError; end
7
+
6
8
  # @!visibility private
7
9
  DEFAULTS = {
8
10
  :port => 27753,
9
- :simulator_ip => "127.0.0.1"
11
+ :simulator_ip => "127.0.0.1",
12
+ :http_timeout => RunLoop::Environment.ci? ? 120 : 60,
13
+ :version => "1.0"
10
14
  }
11
15
 
12
16
  # @!visibility private
13
- def self.workspace
14
- value = ENV["XCUITEST_WORKSPACE"]
15
- if value.nil? || value == ""
16
- return nil
17
- else
18
- value
17
+ def self.run(options={})
18
+ # logger = options[:logger]
19
+ simctl = options[:sim_control] || options[:simctl] || RunLoop::SimControl.new
20
+ xcode = options[:xcode] || RunLoop::Xcode.new
21
+ instruments = options[:instruments] || RunLoop::Instruments.new
22
+
23
+ # Find the Device under test, the App under test, UIA strategy, and reset options
24
+ device = RunLoop::Device.detect_device(options, xcode, simctl, instruments)
25
+ app_details = RunLoop::DetectAUT.detect_app_under_test(options)
26
+ reset_options = RunLoop::Core.send(:detect_reset_options, options)
27
+
28
+ app = app_details[:app]
29
+ bundle_id = app_details[:bundle_id]
30
+
31
+ if device.simulator? && app
32
+ core_sim = RunLoop::CoreSimulator.new(device, app, :xcode => xcode)
33
+ if reset_options
34
+ core_sim.reset_app_sandbox
35
+ end
36
+
37
+ simctl.ensure_software_keyboard(device)
38
+ core_sim.install
19
39
  end
40
+
41
+ xcuitest = RunLoop::XCUITest.new(bundle_id, device)
42
+ xcuitest.launch
43
+ xcuitest
20
44
  end
21
45
 
22
46
  # @!visibility private
@@ -30,78 +54,262 @@ module RunLoop
30
54
  end
31
55
 
32
56
  # @!visibility private
33
- def initialize(bundle_id)
57
+ #
58
+ # The app with `bundle_id` needs to be installed.
59
+ #
60
+ # @param [String] bundle_id The identifier of the app under test.
61
+ # @param [RunLoop::Device] device The device device.
62
+ def initialize(bundle_id, device)
34
63
  @bundle_id = bundle_id
64
+ @device = device
65
+ end
66
+
67
+ def to_s
68
+ "#<XCUITest #{url} : #{bundle_id} : #{device}>"
69
+ end
70
+
71
+ def inspect
72
+ to_s
35
73
  end
36
74
 
37
75
  # @!visibility private
38
- # TODO: move to Device ?
39
- # TODO: needs tests for device case
40
- def url
41
- if target.simulator?
42
- "http://#{DEFAULTS[:simulator_ip]}:#{DEFAULTS[:port]}"
43
- else
44
- calabash_endpoint = RunLoop::Environment.device_endpoint
45
- if calabash_endpoint
46
- base = calabash_endpoint.split(":")[0..1].join(":")
47
- "http://#{base}:#{DEFAULTS[:port]}"
76
+ def bundle_id
77
+ @bundle_id
78
+ end
79
+
80
+ # @!visibility private
81
+ def device
82
+ @device
83
+ end
84
+
85
+ # @!visibility private
86
+ def workspace
87
+ @workspace ||= lambda do
88
+ path = RunLoop::Environment.send(:cbxws)
89
+ if path
90
+ path
48
91
  else
49
- device_name = target.name.gsub(/[\'\s]/, "")
50
- encoding_options = {
51
- :invalid => :replace, # Replace invalid byte sequences
52
- :undef => :replace, # Replace anything not defined in ASCII
53
- :replace => '' # Use a blank for those replacements
54
- }
55
- encoded = device_name.encode(Encoding.find("ASCII"), encoding_options)
56
- "http://#{encoded}.local:27753"
92
+ raise "TODO: figure out how to distribute the CBX-Runner"
57
93
  end
94
+ end.call
95
+ end
96
+
97
+ def launch
98
+ start = Time.now
99
+ launch_cbx_runner
100
+ launch_aut
101
+ elapsed = Time.now - start
102
+ RunLoop.log_debug("Took #{elapsed} seconds to launch #{bundle_id} on #{device}")
103
+ true
104
+ end
105
+
106
+ # @!visibility private
107
+ def running?
108
+ begin
109
+ health(ping_options)
110
+ rescue => _
111
+ nil
112
+ end
113
+ end
114
+
115
+ # @!visibility private
116
+ def stop
117
+ begin
118
+ shutdown
119
+ rescue => _
120
+ nil
58
121
  end
59
122
  end
60
123
 
61
124
  # @!visibility private
62
- def launch_calabus_driver
125
+ def launch_other_app(bundle_id)
126
+ launch_aut(bundle_id)
127
+ end
63
128
 
64
- driver_url = url
65
- server = RunLoop::HTTP::Server.new(driver_url)
66
- request = RunLoop::HTTP::Request.new("/shutdown", {})
67
- options = {
68
- :timeout => 0.5,
69
- :retries => 1
129
+ # @!visibility private
130
+ def query(mark)
131
+ options = http_options
132
+ parameters = { :text => mark }
133
+ request = request("query", parameters)
134
+ client = client(options)
135
+ response = client.post(request)
136
+ expect_200_response(response)
137
+ end
138
+
139
+ # @!visibility private
140
+ def tap_mark(mark)
141
+ options = http_options
142
+ parameters = {
143
+ :gesture => "tap",
144
+ :text => mark
70
145
  }
71
- client = RunLoop::HTTP::RetriableClient.new(server, options)
146
+ request = request("gesture", parameters)
147
+ client(options)
148
+ response = client.post(request)
149
+ expect_200_response(response)
150
+ end
72
151
 
73
- begin
74
- response = client.post(request)
75
- RunLoop.log_debug("Calabus driver says, \"#{response.body}\"")
76
- sleep(2.0)
77
- rescue => e
78
- RunLoop.log_debug("Driver shutdown raised #{e}")
79
- end
152
+ # @!visibility private
153
+ def tap_coordinate(x, y)
154
+ options = http_options
155
+ parameters = {
156
+ :gesture => "tap_coordinate",
157
+ :coordinate => {x: x, y: y}
158
+ }
159
+ request = request("gesture", parameters)
160
+ client(options)
161
+ response = client.post(request)
162
+ expect_200_response(response)
163
+ end
80
164
 
81
- workspace = XCUITest.workspace
165
+ # @!visibility private
166
+ def tap_query_result(hash)
167
+ rect = hash["rect"]
168
+ h = rect["height"]
169
+ w = rect["width"]
170
+ x = rect["x"]
171
+ y = rect["y"]
172
+
173
+ touchx = x + (h/2)
174
+ touchy = y + (w/2)
175
+ tap_coordinate(touchx, touchy)
176
+ end
82
177
 
83
- if !workspace || !File.directory?(workspace)
84
- raise RuntimeError, "No workspace found"
85
- end
178
+ private
86
179
 
87
- destination = target.udid
180
+ # @!visibility private
181
+ def xcrun
182
+ RunLoop::Xcrun.new
183
+ end
88
184
 
89
- # might be nil
90
- if target.simulator?
91
- # quits the simulator
92
- sim = CoreSimulator.new(target, "")
93
- sim.launch_simulator
185
+ # @!visibility private
186
+ def url
187
+ @url ||= lambda do
188
+ if device.simulator?
189
+ "http://#{DEFAULTS[:simulator_ip]}:#{DEFAULTS[:port]}/"
190
+ else
191
+ # This block is untested.
192
+ calabash_endpoint = RunLoop::Environment.device_endpoint
193
+ if calabash_endpoint
194
+ base = calabash_endpoint.split(":")[0..1].join(":")
195
+ "http://#{base}:#{DEFAULTS[:port]}/"
196
+ else
197
+ device_name = device.name.gsub(/['\s]/, "")
198
+ encoding_options = {
199
+ :invalid => :replace, # Replace invalid byte sequences
200
+ :undef => :replace, # Replace anything not defined in ASCII
201
+ :replace => "" # Use a blank for those replacements
202
+ }
203
+ encoded = device_name.encode(Encoding.find("ASCII"), encoding_options)
204
+ "http://#{encoded}.local:27753/"
205
+ end
206
+ end
207
+ end.call
208
+ end
209
+
210
+ # @!visibility private
211
+ def server
212
+ @server ||= RunLoop::HTTP::Server.new(url)
213
+ end
214
+
215
+ # @!visibility private
216
+ def client(options={})
217
+ RunLoop::HTTP::RetriableClient.new(server, options)
218
+ end
219
+
220
+ # @!visibility private
221
+ def versioned_route(route)
222
+ if ["health", "ping", "sessionIdentifier"].include?(route)
223
+ route
94
224
  else
225
+ "#{DEFAULTS[:version]}/#{route}"
226
+ end
227
+ end
228
+
229
+ # @!visibility private
230
+ def request(route, parameters={})
231
+ versioned = versioned_route(route)
232
+ RunLoop::HTTP::Request.request(versioned, parameters)
233
+ end
234
+
235
+ # @!visibility private
236
+ def ping_options
237
+ @ping_options ||= { :timeout => 0.5, :retries => 1 }
238
+ end
95
239
 
240
+ # @!visibility private
241
+ def http_options
242
+ {
243
+ :timeout => DEFAULTS[:http_timeout],
244
+ :interval => 0.1,
245
+ :retries => (DEFAULTS[:http_timeout]/0.1).to_i
246
+ }
247
+ end
248
+
249
+ # @!visibility private
250
+ def session_delete
251
+ options = ping_options
252
+ request = request("delete")
253
+ client = client(options)
254
+ begin
255
+ response = client.delete(request)
256
+ body = expect_200_response(response)
257
+ RunLoop.log_debug("CBX-Runner says, #{body}")
258
+ body
259
+ rescue => e
260
+ RunLoop.log_debug("CBX-Runner session delete error: #{e}")
261
+ nil
96
262
  end
263
+ end
97
264
 
265
+ # @!visibility private
266
+ # TODO expect 200 response and parse body (atm the body in not valid JSON)
267
+ def shutdown
268
+ session_delete
269
+ options = ping_options
270
+ request = request("shutdown")
271
+ client = client(options)
272
+ begin
273
+ response = client.post(request)
274
+ body = response.body
275
+ RunLoop.log_debug("CBX-Runner says, \"#{body}\"")
276
+ 5.times do
277
+ begin
278
+ health
279
+ sleep(0.2)
280
+ rescue => _
281
+ break
282
+ end
283
+ end
284
+ body
285
+ rescue => e
286
+ RunLoop.log_debug("CBX-Runner shutdown error: #{e}")
287
+ nil
288
+ end
289
+ end
290
+
291
+ # @!visibility private
292
+ # TODO expect 200 response and parse body (atm the body is not valid JSON)
293
+ def health(options={})
294
+ merged_options = http_options.merge(options)
295
+ request = request("health")
296
+ client = client(merged_options)
297
+ response = client.get(request)
298
+ body = response.body
299
+ RunLoop.log_debug("CBX-Runner driver says, \"#{body}\"")
300
+ body
301
+ end
302
+
303
+ # @!visibility private
304
+ def xcodebuild
98
305
  args = [
99
306
  "xcrun",
100
307
  "xcodebuild",
101
308
  "-scheme", "CBXAppStub",
102
309
  "-workspace", workspace,
103
310
  "-config", "Debug",
104
- "-destination", "id=#{destination}",
311
+ "-destination",
312
+ "id=#{device.udid}",
105
313
  "clean",
106
314
  "test"
107
315
  ]
@@ -118,82 +326,80 @@ module RunLoop
118
326
 
119
327
  pid = Process.spawn(*args, options)
120
328
  Process.detach(pid)
329
+ pid
330
+ end
121
331
 
122
- if target.simulator?
123
- target.simulator_wait_for_stable_state
124
- end
125
-
126
- RunLoop.log_debug("Waiting for CBX-Runner to build...")
332
+ # @!visibility private
333
+ def launch_cbx_runner
334
+ # Fail fast if CBXWS is not defined.
335
+ # WIP - we will distribute the workspace somehow.
336
+ workspace
127
337
 
128
- server = RunLoop::HTTP::Server.new(driver_url)
129
- request = RunLoop::HTTP::Request.new("/health", {})
338
+ shutdown
130
339
 
131
- options = {
132
- :timeout => 60,
133
- :interval => 0.1,
134
- :retries => 600
135
- }
340
+ if device.simulator?
341
+ # quits the simulator
342
+ sim = CoreSimulator.new(device, "")
343
+ sim.launch_simulator
344
+ else
345
+ # anything special about physical devices?
346
+ end
136
347
 
137
- client = RunLoop::HTTP::RetriableClient.new(server, options)
138
- response = client.get(request)
348
+ start = Time.now
349
+ pid = xcodebuild
350
+ RunLoop.log_debug("Waiting for CBX-Runner to build...")
351
+ health
139
352
 
140
- RunLoop.log_debug("Calabus driver says, \"#{response.body}\"")
353
+ RunLoop.log_debug("Took #{Time.now - start} seconds to build and launch")
141
354
  pid.to_i
142
355
  end
143
356
 
144
- def launch_app
145
- server = RunLoop::HTTP::Server.new(url)
146
- request = RunLoop::HTTP::Request.request("/session", {:bundleID => bundle_id})
147
- client = RunLoop::HTTP::RetriableClient.new(server)
148
- response = client.post(request)
149
-
150
- RunLoop.log_debug("Calabus driver says, \"#{response.body}\"")
151
- end
152
-
153
357
  # @!visibility private
154
- def target
155
- @device ||= lambda do
156
- target = RunLoop::Environment.device_target
358
+ def launch_aut(bundle_id = @bundle_id)
359
+ client = client(http_options)
360
+ request = request("session", {:bundleID => bundle_id})
157
361
 
158
- if !target
159
- target = RunLoop::Core.default_simulator
362
+ begin
363
+ response = client.post(request)
364
+ RunLoop.log_debug("Launched #{bundle_id} on #{device}")
365
+ RunLoop.log_debug("#{response.body}")
366
+ if device.simulator?
367
+ device.simulator_wait_for_stable_state
160
368
  end
369
+ expect_200_response(response)
370
+ rescue => e
371
+ raise e.class, %Q[Could not launch #{bundle_id} on #{device}:
161
372
 
162
- options = {
163
- :sim_control => simctl,
164
- :instruments => instruments
165
- }
166
-
167
- device = RunLoop::Device.device_with_identifier(target, options)
168
-
169
- if !device
170
- raise RuntimeError, "Could not find a device"
171
- end
373
+ #{e.message}
172
374
 
173
- device
174
- end.call
375
+ Something went wrong.
376
+ ]
377
+ end
175
378
  end
176
379
 
177
380
  # @!visibility private
178
- def bundle_id
179
- @bundle_id
381
+ def response_body_to_hash(response)
382
+ body = response.body
383
+ begin
384
+ JSON.parse(body)
385
+ rescue TypeError, JSON::ParserError => _
386
+ raise RunLoop::XCUITest::HTTPError,
387
+ "Could not parse response '#{body}'; the app has probably crashed"
388
+ end
180
389
  end
181
390
 
182
- private
183
-
184
391
  # @!visibility private
185
- def simctl
186
- @simctl ||= RunLoop::SimControl.new
187
- end
392
+ def expect_200_response(response)
393
+ body = response_body_to_hash(response)
394
+ return body if response.status_code < 300
188
395
 
189
- # @!visibility private
190
- def instruments
191
- @instruments ||= RunLoop::Instruments.new
192
- end
396
+ raise RunLoop::XCUITest::HTTPError,
397
+ %Q[Expected status code < 200, found #{response.status_code}.
193
398
 
194
- # @!visibility private
195
- def xcrun
196
- RunLoop::Xcrun.new
399
+ Server replied with:
400
+
401
+ #{body}
402
+ ]
197
403
  end
198
404
 
199
405
  # @!visibility private
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: run_loop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0.pre1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karl Krukow
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-22 00:00:00.000000000 Z
11
+ date: 2016-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -326,7 +326,7 @@ files:
326
326
  - lib/run_loop/process_waiter.rb
327
327
  - lib/run_loop/regex.rb
328
328
  - lib/run_loop/sim_control.rb
329
- - lib/run_loop/simctl/plists.rb
329
+ - lib/run_loop/simctl.rb
330
330
  - lib/run_loop/strings.rb
331
331
  - lib/run_loop/template.rb
332
332
  - lib/run_loop/version.rb
@@ -362,9 +362,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
362
362
  version: '2.0'
363
363
  required_rubygems_version: !ruby/object:Gem::Requirement
364
364
  requirements:
365
- - - ">"
365
+ - - ">="
366
366
  - !ruby/object:Gem::Version
367
- version: 1.3.1
367
+ version: '0'
368
368
  requirements: []
369
369
  rubyforge_project:
370
370
  rubygems_version: 2.5.2
@@ -1,20 +0,0 @@
1
- module RunLoop
2
- module Simctl
3
- class Plists
4
-
5
- SIMCTL_PLIST_DIR = lambda {
6
- dirname = File.dirname(__FILE__)
7
- joined = File.join(dirname, '..', '..', '..', 'plists', 'simctl')
8
- File.expand_path(joined)
9
- }.call
10
-
11
- def self.uia_automation_plist
12
- File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomation.plist')
13
- end
14
-
15
- def self.uia_automation_plugin_plist
16
- File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomationPlugIn.plist')
17
- end
18
- end
19
- end
20
- end