run_loop 2.1.2 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+
2
+ module RunLoop
3
+
4
+ # @!visibility private
5
+ module DeviceAgent
6
+
7
+ # @!visibility private
8
+ class Xcodebuild < RunLoop::DeviceAgent::Launcher
9
+
10
+ # @!visibility private
11
+ def self.log_file
12
+ path = File.join(Xcodebuild.dot_dir, "xcodebuild.log")
13
+ FileUtils.touch(path) if !File.exist?(path)
14
+ path
15
+ end
16
+
17
+ # @!visibility private
18
+ def to_s
19
+ "#<Xcodebuild #{workspace}>"
20
+ end
21
+
22
+ # @!visibility private
23
+ def inspect
24
+ to_s
25
+ end
26
+
27
+ # @!visibility private
28
+ def launch
29
+ workspace
30
+
31
+ if device.simulator?
32
+ # quits the simulator
33
+ sim = CoreSimulator.new(device, "")
34
+ sim.launch_simulator
35
+ end
36
+
37
+ start = Time.now
38
+ RunLoop.log_debug("Waiting for CBX-Runner to build...")
39
+ pid = xcodebuild
40
+ RunLoop.log_debug("Took #{Time.now - start} seconds to build and launch CBX-Runner")
41
+ pid
42
+ end
43
+
44
+ # @!visibility private
45
+ def workspace
46
+ @workspace ||= lambda do
47
+ path = RunLoop::Environment.send(:cbxws)
48
+ if path
49
+ path
50
+ else
51
+ raise "The CBXWS env var is undefined. Are you a maintainer?"
52
+ end
53
+ end.call
54
+ end
55
+
56
+ # @!visibility private
57
+ def xcodebuild
58
+ env = {
59
+ "COMMAND_LINE_BUILD" => "1"
60
+ }
61
+
62
+ args = [
63
+ "xcrun",
64
+ "xcodebuild",
65
+ "-scheme", "CBXAppStub",
66
+ "-workspace", workspace,
67
+ "-config", "Debug",
68
+ "-destination",
69
+ "id=#{device.udid}",
70
+ "clean",
71
+ "test"
72
+ ]
73
+
74
+ log_file = Xcodebuild.log_file
75
+
76
+ options = {
77
+ :out => log_file,
78
+ :err => log_file
79
+ }
80
+
81
+ command = "#{env.map.each { |k, v| "#{k}=#{v}" }.join(" ")} #{args.join(" ")}"
82
+ RunLoop.log_unix_cmd("#{command} >& #{log_file}")
83
+
84
+ pid = Process.spawn(env, *args, options)
85
+ Process.detach(pid)
86
+ pid.to_i
87
+ end
88
+ end
89
+ end
90
+ end
91
+
@@ -0,0 +1,109 @@
1
+
2
+ module RunLoop
3
+ # @!visibility private
4
+ module DeviceAgent
5
+ # @!visibility private
6
+ #
7
+ # A wrapper around the test-control binary.
8
+ class XCTestctl < RunLoop::DeviceAgent::Launcher
9
+
10
+ # @!visibility private
11
+ @@xctestctl = nil
12
+
13
+ # @!visibility private
14
+ def self.device_agent_dir
15
+ @@device_agent_dir ||= File.expand_path(File.dirname(__FILE__))
16
+ end
17
+
18
+ # @!visibility private
19
+ def self.xctestctl
20
+ @@xctestctl ||= lambda do
21
+ from_env = RunLoop::Environment.xctestctl
22
+ if from_env
23
+ if File.exist?(from_env)
24
+ RunLoop.log_debug("Using XCTESTCTL=#{from_env}")
25
+ from_env
26
+ else
27
+ raise RuntimeError, %Q[
28
+ XCTESTCTL environment variable defined:
29
+
30
+ #{from_env}
31
+
32
+ but binary does not exist at that path.
33
+ ]
34
+ end
35
+
36
+ else
37
+ File.join(self.device_agent_dir, "bin", "xctestctl")
38
+ end
39
+ end.call
40
+ end
41
+
42
+ # @!visibility private
43
+ def to_s
44
+ "#<Testctl: #{XCTestctl.xctestctl}>"
45
+ end
46
+
47
+ # @!visibility private
48
+ def inspect
49
+ to_s
50
+ end
51
+
52
+ # @!visibility private
53
+ def runner
54
+ @runner ||= RunLoop::DeviceAgent::CBXRunner.new(device)
55
+ end
56
+
57
+ # @!visibility private
58
+ def self.log_file
59
+ path = File.join(Launcher.dot_dir, "xctestctl.log")
60
+ FileUtils.touch(path) if !File.exist?(path)
61
+ path
62
+ end
63
+
64
+ # @!visibility private
65
+ def launch
66
+ RunLoop::DeviceAgent::Frameworks.instance.install
67
+
68
+ if device.simulator?
69
+ cbxapp = RunLoop::App.new(runner.runner)
70
+
71
+ # quits the simulator
72
+ sim = CoreSimulator.new(device, cbxapp)
73
+ sim.install
74
+ end
75
+
76
+ cmd = RunLoop::DeviceAgent::XCTestctl.xctestctl
77
+
78
+ args = ["-r", runner.runner,
79
+ "-t", runner.tester,
80
+ "-d", device.udid]
81
+
82
+ if device.physical_device?
83
+ args << "-c"
84
+ args << RunLoop::Environment.codesign_identity
85
+ end
86
+
87
+ log_file = XCTestctl.log_file
88
+ FileUtils.rm_rf(log_file)
89
+ FileUtils.touch(log_file)
90
+
91
+ options = {:out => log_file, :err => log_file}
92
+ RunLoop.log_unix_cmd("#{cmd} #{args.join(" ")} >& #{log_file}")
93
+
94
+ # Gotta keep the xctestctl process alive or the connection
95
+ # to testmanagerd will fail.
96
+ pid = Process.spawn(cmd, *args, options)
97
+ Process.detach(pid)
98
+
99
+ if device.simulator?
100
+ device.simulator_wait_for_stable_state
101
+ end
102
+
103
+ pid.to_i
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+
@@ -11,10 +11,19 @@ module RunLoop
11
11
  #
12
12
  # Try 3 times for 10 seconds each try with a sleep of 2 seconds
13
13
  # between tries.
14
+ #
15
+ # You can override these values if they do not work in your environment.
16
+ #
17
+ # For cucumber users, the best place to override would be in your
18
+ # features/support/env.rb.
19
+ #
20
+ # For example:
21
+ #
22
+ # RunLoop::DylibInjector::RETRY_OPTIONS[:timeout] = 60
14
23
  RETRY_OPTIONS = {
15
24
  :tries => 3,
16
25
  :interval => 2,
17
- :timeout => 10
26
+ :timeout => RunLoop::Environment.ci? ? 40 : 20
18
27
  }
19
28
 
20
29
  # @!attribute [r] process_name
@@ -162,6 +162,72 @@ module RunLoop
162
162
  end
163
163
  end
164
164
 
165
+ # Returns the value of CODESIGN_IDENTITY
166
+ def self.codesign_identity
167
+ value = ENV["CODESIGN_IDENTITY"]
168
+ if !value || value == ""
169
+ nil
170
+ else
171
+ value
172
+ end
173
+ end
174
+
175
+ # Returns the value of KEYCHAIN
176
+ #
177
+ # Use this to specify a non-default KEYCHAIN for code signing.
178
+ #
179
+ # The default KEYCHAIN is login.keychain.
180
+ def self.keychain
181
+ value = ENV["KEYCHAIN"]
182
+ if !value || value == ""
183
+ nil
184
+ else
185
+ value
186
+ end
187
+ end
188
+
189
+ # Returns the value of XCTESTCTL
190
+ #
191
+ # Use this to specify a non-default xctestctl binary.
192
+ #
193
+ # The default xctestctl binary is bundled with this gem.
194
+ def self.xctestctl
195
+ value = ENV["XCTESTCTL"]
196
+ if !value || value == ""
197
+ nil
198
+ else
199
+ value
200
+ end
201
+ end
202
+
203
+ # Returns the value of CBXDEVICE
204
+ #
205
+ # Use this to specify a non-default CBX-Runner for physical devices.
206
+ #
207
+ # The default CBX-Runner is bundled with this gem.
208
+ def self.cbxdevice
209
+ value = ENV["CBXDEVICE"]
210
+ if !value || value == ""
211
+ nil
212
+ else
213
+ value
214
+ end
215
+ end
216
+
217
+ # Returns the value of CBXSIM
218
+ #
219
+ # Use this to specify a non-default CBX-Runner for simulators.
220
+ #
221
+ # The default CBX-Runner is bundled with this gem.
222
+ def self.cbxsim
223
+ value = ENV["CBXSIM"]
224
+ if !value || value == ""
225
+ nil
226
+ else
227
+ value
228
+ end
229
+ end
230
+
165
231
  # Returns true if running in Jenkins CI
166
232
  #
167
233
  # Checks the value of JENKINS_HOME
@@ -24,11 +24,16 @@ module RunLoop
24
24
  # @return [String] Expanded path to the default cache directory.
25
25
  # @raise [RuntimeError] When the ~/.run_loop exists, but is not a directory.
26
26
  def self.default_directory
27
- run_loop_dir = File.expand_path('~/.run-loop')
27
+ run_loop_dir = File.join(RunLoop::Environment.user_home_directory, ".run-loop")
28
28
  if !File.exist?(run_loop_dir)
29
29
  FileUtils.mkdir(run_loop_dir)
30
30
  elsif !File.directory?(run_loop_dir)
31
- raise "Expected '#{run_loop_dir}' to be a directory.\nRunLoop requires this directory to cache files."
31
+ raise %Q[
32
+ Expected ~/.run_loop to be a directory.
33
+
34
+ RunLoop requires this directory to cache files
35
+ ]
36
+
32
37
  end
33
38
  run_loop_dir
34
39
  end
@@ -2,8 +2,54 @@ module RunLoop
2
2
  # @!visibility private
3
3
  module PhysicalDevice
4
4
 
5
+ # Raised when installation fails.
6
+ class InstallError < RuntimeError; end
7
+
8
+ # Raised when uninstall fails.
9
+ class UninstallError < RuntimeError; end
10
+
11
+ # Raised when tool cannot perform task.
12
+ class NotImplementedError < StandardError; end
13
+
14
+ # Controls the behavior of various life cycle commands.
15
+ #
16
+ # You can override these values if they do not work in your environment.
17
+ #
18
+ # For cucumber users, the best place to override would be in your
19
+ # features/support/env.rb.
20
+ #
21
+ # For example:
22
+ #
23
+ # RunLoop::PhysicalDevice::LifeCycle::DEFAULT_OPTIONS[:timeout] = 60
24
+ DEFAULT_OPTIONS = {
25
+ :install_timeout => RunLoop::Environment.ci? ? 120 : 30
26
+ }
27
+
5
28
  # @!visibility private
6
- class IDeviceInstaller < LifeCycle
29
+ class LifeCycle
30
+
31
+ require "run_loop/abstract"
32
+ include RunLoop::Abstract
33
+
34
+ require "run_loop/shell"
35
+ include RunLoop::Shell
36
+
37
+ attr_reader :device
38
+
39
+ # Create a new instance.
40
+ #
41
+ # @param [RunLoop::Device] device A physical device.
42
+ # @raise [ArgumentError] If device is a simulator.
43
+ def initialize(device)
44
+ if !device.physical_device?
45
+ raise ArgumentError, %Q[Device:
46
+
47
+ #{device}
48
+
49
+ must be a physical device.]
50
+ end
51
+ @device = device
52
+ end
7
53
 
8
54
  # Is the tool installed?
9
55
  def self.tool_is_installed?
@@ -19,6 +65,7 @@ module RunLoop
19
65
 
20
66
  # Is the app installed?
21
67
  #
68
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
22
69
  # @return [Boolean] true or false
23
70
  def app_installed?(bundle_id)
24
71
  abstract_method!
@@ -30,14 +77,19 @@ module RunLoop
30
77
  # no version check is performed.
31
78
  #
32
79
  # App data is never preserved. If you want to preserve the app data,
33
- # call `ensure_app_installed`.
80
+ # call `ensure_newest_installed`.
34
81
  #
35
82
  # Possible return values:
36
83
  #
37
84
  # * :reinstalled => app was installed, but app data was not preserved.
38
85
  # * :installed => app was not installed.
39
86
  #
87
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
88
+ # The caller is responsible for validating the ipa for the device by
89
+ # checking that the codesign and instruction set is correct.
90
+ #
40
91
  # @raise [InstallError] If app was not installed.
92
+ #
41
93
  # @return [Symbol] A keyword describing the action that was performed.
42
94
  def install_app(app_or_ipa)
43
95
  abstract_method!
@@ -47,15 +99,18 @@ module RunLoop
47
99
  #
48
100
  # App data is never preserved. If you want to install a new version of
49
101
  # an app and preserve app data (upgrade testing), call
50
- # `ensure_app_installed`.
102
+ # `ensure_newest_installed`.
51
103
  #
52
104
  # Possible return values:
53
105
  #
54
106
  # * :nothing => app was not installed
55
107
  # * :uninstall => app was uninstalled
56
108
  #
109
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
110
+ #
57
111
  # @raise [UninstallError] If the app cannot be uninstalled, usually
58
112
  # because it is a system app.
113
+ #
59
114
  # @return [Symbol] A keyword that describes what action was performed.
60
115
  def uninstall_app(bundle_id)
61
116
  abstract_method!
@@ -80,11 +135,15 @@ module RunLoop
80
135
  # but app data was not preserved.
81
136
  # * :installed => app was not installed.
82
137
  #
138
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
139
+ # The caller is responsible for validating the ipa for the device by
140
+ # checking that the codesign and instruction set is correct.
141
+ #
83
142
  # @raise [InstallError] If the app could not be installed.
84
143
  # @raise [UninstallError] If the app could not be uninstalled.
85
144
  #
86
145
  # @return [Symbol] A keyword that describes the action that was taken.
87
- def ensure_app_installed(app_or_ipa)
146
+ def ensure_newest_installed(app_or_ipa)
88
147
  abstract_method!
89
148
  end
90
149
 
@@ -94,18 +153,33 @@ module RunLoop
94
153
  # the CFBundleShortVersionString. If either are different, then this
95
154
  # method returns false.
96
155
  #
156
+ # @param [RunLoop::Ipa, RunLoop::App] app_or_ipa The ipa to install.
157
+ # The caller is responsible for validating the ipa for the device by
158
+ # checking that the codesign and instruction set is correct.
159
+ #
97
160
  # @raise [RuntimeError] If app is not already installed.
98
161
  def installed_app_same_as?(app_or_ipa)
99
162
  abstract_method!
100
163
  end
101
164
 
165
+ # Returns true if this tool can reset an app's sandbox without
166
+ # uninstalling the app.
167
+ def can_reset_app_sandbox?
168
+ abstract_method!
169
+ end
170
+
102
171
  # Clear the app sandbox.
103
172
  #
104
173
  # This method will never uninstall the app. If the concrete
105
174
  # implementation cannot reset the app data, this method should raise
106
- # an exception.
175
+ # a RunLoop::PhysicalDevice::NotImplementedError
107
176
  #
108
177
  # Does not clear Keychain. Use the Calabash iOS Keychain API.
178
+ #
179
+ # @param [String] bundle_id The CFBundleIdentifier of an app.
180
+ #
181
+ # @raise [RunLoop::PhysicalDevice::NotImplementedError] If this tool
182
+ # cannot reset the app sandbox without unintalling the app.
109
183
  def reset_app_sandbox(bundle_id)
110
184
  abstract_method!
111
185
  end
@@ -141,48 +215,21 @@ module RunLoop
141
215
  #
142
216
  # * sandbox/Documents
143
217
  # * sandbox/Library
144
- # * sandbox/Preferences
218
+ # * sandbox/Library/Preferences
145
219
  # * sandbox/tmp
146
220
  #
147
- # The data is a hash of arrays that define source/target file
148
- # path pairs.
149
- #
150
- # {
151
- # :documents => [
152
- # {
153
- # :source => "path/to/file/on/disk",
154
- # :target => "sub/dir/under/Documents"
155
- # },
156
- # {
157
- # :source => "path/to/other/file",
158
- # :target => "./"
159
- # }
160
- # ],
161
- #
162
- # :library => [ < ditto >],
163
- # :preferences => [ < ditto > ],
164
- # :tmp => [ < ditto >]
165
- # }
166
- #
167
- # * If a file exists at a target path, it will be replaced.
168
- # * Subdirectories will be created as necessary.
169
- # * :source files must exist.
221
+ # Behavior TBD.
170
222
  def sideload(data)
171
- abstract_method!
223
+ raise NotImplementedError,
224
+ "The behavior of the sideload method has not been determined"
172
225
  end
173
226
 
174
227
  # Removes a file or directory from the app sandbox.
175
228
  #
176
- # If the path does not exist, no error will be raised.
177
- #
178
- # Documents, Library, Preferences, and tmp directories will be
179
- # deleted, but then recreated. For example:
180
- #
181
- # remove_file_from_sandbox("Preferences")
182
- #
183
- # The Preferences directory will be deleted and then recreated.
229
+ # Behavior TBD.
184
230
  def remove_from_sandbox(path)
185
- abstract_method!
231
+ raise NotImplementedError,
232
+ "The behavior of the remove_from_sandbox method has not been determined"
186
233
  end
187
234
 
188
235
  # @!visibility private