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.
- checksums.yaml +4 -4
- data/lib/run_loop.rb +7 -0
- data/lib/run_loop/abstract.rb +18 -0
- data/lib/run_loop/core.rb +30 -1
- data/lib/run_loop/core_simulator.rb +5 -1
- data/lib/run_loop/detect_aut/detect.rb +8 -2
- data/lib/run_loop/detect_aut/xcode.rb +6 -2
- data/lib/run_loop/device.rb +9 -2
- data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/bin/xctestctl +0 -0
- data/lib/run_loop/device_agent/cbxrunner.rb +155 -0
- data/lib/run_loop/device_agent/frameworks.rb +64 -0
- data/lib/run_loop/device_agent/frameworks/Frameworks.zip +0 -0
- data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/launcher.rb +51 -0
- data/lib/run_loop/device_agent/xcodebuild.rb +91 -0
- data/lib/run_loop/device_agent/xctestctl.rb +109 -0
- data/lib/run_loop/dylib_injector.rb +10 -1
- data/lib/run_loop/environment.rb +66 -0
- data/lib/run_loop/host_cache.rb +7 -2
- data/lib/run_loop/physical_device/{ideviceinstaller.rb → life_cycle.rb} +86 -39
- data/lib/run_loop/sim_control.rb +8 -3
- data/lib/run_loop/version.rb +1 -1
- data/lib/run_loop/xcuitest.rb +134 -116
- data/scripts/lib/log.js +1 -1
- data/scripts/lib/on_alert.js +102 -17
- data/vendor-licenses/FBSimulatorControl.LICENSE +30 -0
- data/vendor-licenses/xctestctl.LICENSE +32 -0
- metadata +15 -3
@@ -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 =>
|
26
|
+
:timeout => RunLoop::Environment.ci? ? 40 : 20
|
18
27
|
}
|
19
28
|
|
20
29
|
# @!attribute [r] process_name
|
data/lib/run_loop/environment.rb
CHANGED
@@ -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
|
data/lib/run_loop/host_cache.rb
CHANGED
@@ -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.
|
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
|
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
|
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 `
|
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
|
-
# `
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|