run_loop 2.1.2 → 2.1.3
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 +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
|