@astur-mobile/ios 0.1.0-beta.0
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.
- package/assets/ios-xctest-agent/AsturAgent.swift +1872 -0
- package/assets/ios-xctest-agent/AsturAgentBridgeClient.swift +144 -0
- package/assets/ios-xctest-agent/AsturAgentServer.swift +249 -0
- package/assets/ios-xctest-agent/AsturAgentUITests-Info.plist +30 -0
- package/assets/ios-xctest-agent/AsturAgentUITests.swift +55 -0
- package/assets/ios-xctest-agent/AsturIOSAgent.xcodeproj/project.pbxproj +448 -0
- package/assets/ios-xctest-agent/AsturIOSAgent.xcodeproj/xcshareddata/xcschemes/AsturIOSAgent.xcscheme +133 -0
- package/assets/ios-xctest-agent/HostApp/AppDelegate.swift +105 -0
- package/assets/ios-xctest-agent/HostApp/Info.plist +32 -0
- package/assets/ios-xctest-agent/README.md +74 -0
- package/dist/command.d.ts +9 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +37 -0
- package/dist/command.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2039 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class AsturAgentBridgeClient {
|
|
4
|
+
private let agent: AsturAgent
|
|
5
|
+
private let bridgeUrl: URL
|
|
6
|
+
|
|
7
|
+
/// Long-poll timeout: the agent holds the GET /command request open for up to
|
|
8
|
+
/// this duration on the server side, eliminating the 100ms polling gap.
|
|
9
|
+
/// Falls back to a short retry on network errors so the loop stays resilient.
|
|
10
|
+
private let longPollTimeout: TimeInterval = 30
|
|
11
|
+
|
|
12
|
+
init(agent: AsturAgent, bridgeUrl: URL) {
|
|
13
|
+
self.agent = agent
|
|
14
|
+
self.bridgeUrl = bridgeUrl
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func run() {
|
|
18
|
+
register()
|
|
19
|
+
|
|
20
|
+
while true {
|
|
21
|
+
autoreleasepool {
|
|
22
|
+
if let command = fetchCommand() {
|
|
23
|
+
postResult(agent.dispatch(command))
|
|
24
|
+
}
|
|
25
|
+
// No sleep — fetchCommand blocks via long-poll or returns immediately
|
|
26
|
+
// on error. A tiny back-off only on consecutive transport failures.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private func register() {
|
|
32
|
+
let info = agent.dispatch(AsturCommand(id: "register", method: "agent.ping", params: [:])).result ?? [:]
|
|
33
|
+
_ = request(
|
|
34
|
+
path: "/register",
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: [
|
|
37
|
+
"id": "register",
|
|
38
|
+
"ok": true,
|
|
39
|
+
"result": info,
|
|
40
|
+
"data": info
|
|
41
|
+
],
|
|
42
|
+
timeout: 5
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private func fetchCommand() -> AsturCommand? {
|
|
47
|
+
// Long-poll: server holds the connection open until a command is queued
|
|
48
|
+
// or the timeout elapses (returns 204). This removes the old 100ms
|
|
49
|
+
// polling gap and delivers commands with near-zero latency.
|
|
50
|
+
let response = request(path: "/command", method: "GET", timeout: longPollTimeout + 2)
|
|
51
|
+
guard response.status == 200, let data = response.data else {
|
|
52
|
+
// On 204 (no command yet) or transient error, return nil and
|
|
53
|
+
// immediately re-enter the loop — no Thread.sleep needed.
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard
|
|
58
|
+
let raw = try? JSONSerialization.jsonObject(with: data),
|
|
59
|
+
let json = raw as? [String: Any]
|
|
60
|
+
else {
|
|
61
|
+
return nil
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let id = (json["id"] as? String)?.nonEmpty ?? "unknown"
|
|
65
|
+
let method = (json["method"] as? String)?.nonEmpty
|
|
66
|
+
?? (json["command"] as? String)?.nonEmpty
|
|
67
|
+
?? "unknown"
|
|
68
|
+
let params = (json["params"] as? [String: Any])
|
|
69
|
+
?? (json["payload"] as? [String: Any])
|
|
70
|
+
?? [:]
|
|
71
|
+
|
|
72
|
+
return AsturCommand(id: id, method: method, params: params)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func postResult(_ result: AsturCommandResult) {
|
|
76
|
+
_ = request(path: "/response", method: "POST", body: resultJson(result), timeout: 5)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func resultJson(_ result: AsturCommandResult) -> [String: Any] {
|
|
80
|
+
var body: [String: Any] = [
|
|
81
|
+
"id": result.id,
|
|
82
|
+
"ok": result.ok,
|
|
83
|
+
"timing": [
|
|
84
|
+
"totalMs": 0,
|
|
85
|
+
"agentMs": 0,
|
|
86
|
+
"hostRoundTrips": 1,
|
|
87
|
+
"usedSnapshot": false,
|
|
88
|
+
"usedLegacyFallback": false
|
|
89
|
+
]
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if result.ok {
|
|
93
|
+
body["result"] = result.result ?? NSNull()
|
|
94
|
+
body["data"] = result.result ?? NSNull()
|
|
95
|
+
} else {
|
|
96
|
+
var error: [String: Any] = [
|
|
97
|
+
"code": result.error?.code ?? "UNKNOWN",
|
|
98
|
+
"message": result.error?.message ?? "Unknown iOS agent error."
|
|
99
|
+
]
|
|
100
|
+
if let details = result.error?.details {
|
|
101
|
+
error["details"] = details
|
|
102
|
+
if let detailMap = details as? [String: Any] {
|
|
103
|
+
if let diagnostics = detailMap["diagnostics"] {
|
|
104
|
+
body["diagnostics"] = diagnostics
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
body["error"] = error
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return body
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func request(path: String, method: String, body: [String: Any]? = nil, timeout: TimeInterval = 5) -> (status: Int, data: Data?) {
|
|
115
|
+
let url = bridgeUrl.appendingPathComponent(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
|
116
|
+
var request = URLRequest(url: url)
|
|
117
|
+
request.httpMethod = method
|
|
118
|
+
request.timeoutInterval = timeout
|
|
119
|
+
|
|
120
|
+
if let body {
|
|
121
|
+
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
|
122
|
+
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
126
|
+
var status = 0
|
|
127
|
+
var data: Data?
|
|
128
|
+
|
|
129
|
+
URLSession.shared.dataTask(with: request) { responseData, response, _ in
|
|
130
|
+
status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
131
|
+
data = responseData
|
|
132
|
+
semaphore.signal()
|
|
133
|
+
}.resume()
|
|
134
|
+
|
|
135
|
+
_ = semaphore.wait(timeout: .now() + timeout + 1)
|
|
136
|
+
return (status, data)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private extension String {
|
|
141
|
+
var nonEmpty: String? {
|
|
142
|
+
isEmpty ? nil : self
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
final class AsturAgentServer {
|
|
5
|
+
private let port: UInt16
|
|
6
|
+
private let agent: AsturAgent
|
|
7
|
+
private let queue = DispatchQueue(label: "dev.astur.ios-agent.server")
|
|
8
|
+
private var listener: NWListener?
|
|
9
|
+
|
|
10
|
+
init(port: UInt16, agent: AsturAgent) {
|
|
11
|
+
self.port = port
|
|
12
|
+
self.agent = agent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func start() throws {
|
|
16
|
+
let listener = try NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: port)!)
|
|
17
|
+
listener.stateUpdateHandler = { state in
|
|
18
|
+
print("Astur iOS agent listener state: \(state)")
|
|
19
|
+
}
|
|
20
|
+
listener.newConnectionHandler = { [weak self] connection in
|
|
21
|
+
self?.handle(connection)
|
|
22
|
+
}
|
|
23
|
+
listener.start(queue: queue)
|
|
24
|
+
self.listener = listener
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func stop() {
|
|
28
|
+
listener?.cancel()
|
|
29
|
+
listener = nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private func handle(_ connection: NWConnection) {
|
|
33
|
+
connection.start(queue: queue)
|
|
34
|
+
read(connection, buffer: Data())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private func read(_ connection: NWConnection, buffer: Data) {
|
|
38
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] data, _, isComplete, error in
|
|
39
|
+
guard let self else {
|
|
40
|
+
connection.cancel()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if error != nil {
|
|
45
|
+
connection.cancel()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var next = buffer
|
|
50
|
+
if let data {
|
|
51
|
+
next.append(data)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if let request = HttpRequest(data: next) {
|
|
55
|
+
self.respond(to: request, connection: connection)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if isComplete {
|
|
60
|
+
self.write(
|
|
61
|
+
connection,
|
|
62
|
+
status: 400,
|
|
63
|
+
body: self.errorJson(id: "unknown", code: "BAD_REQUEST", message: "Incomplete Astur HTTP command.")
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self.read(connection, buffer: next)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func respond(to request: HttpRequest, connection: NWConnection) {
|
|
73
|
+
do {
|
|
74
|
+
let body: [String: Any]
|
|
75
|
+
if request.method == "GET" && request.path == "/health" {
|
|
76
|
+
let startedAt = Date()
|
|
77
|
+
body = resultJson(
|
|
78
|
+
agent.dispatch(AsturCommand(id: "health", method: "agent.ping", params: [:])),
|
|
79
|
+
elapsedMs: elapsedMs(startedAt)
|
|
80
|
+
)
|
|
81
|
+
} else if request.method == "POST" {
|
|
82
|
+
let command = try parseCommand(request.body)
|
|
83
|
+
let startedAt = Date()
|
|
84
|
+
body = resultJson(agent.dispatch(command), elapsedMs: elapsedMs(startedAt))
|
|
85
|
+
} else {
|
|
86
|
+
body = errorJson(
|
|
87
|
+
id: "unknown",
|
|
88
|
+
code: "UNSUPPORTED_HTTP_METHOD",
|
|
89
|
+
message: "Astur agent expects POST commands."
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
write(connection, status: 200, body: body)
|
|
94
|
+
} catch {
|
|
95
|
+
write(
|
|
96
|
+
connection,
|
|
97
|
+
status: 500,
|
|
98
|
+
body: errorJson(
|
|
99
|
+
id: "unknown",
|
|
100
|
+
code: "SERVER_ERROR",
|
|
101
|
+
message: String(describing: error)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func parseCommand(_ body: Data) throws -> AsturCommand {
|
|
108
|
+
let raw = try JSONSerialization.jsonObject(with: body)
|
|
109
|
+
guard let json = raw as? [String: Any] else {
|
|
110
|
+
throw ServerFailure("Astur command must be a JSON object.")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let id = (json["id"] as? String)?.nonEmpty ?? "unknown"
|
|
114
|
+
let method = (json["method"] as? String)?.nonEmpty
|
|
115
|
+
?? (json["command"] as? String)?.nonEmpty
|
|
116
|
+
|
|
117
|
+
guard let method else {
|
|
118
|
+
throw ServerFailure("Astur command must include method or command.")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let params = (json["params"] as? [String: Any])
|
|
122
|
+
?? (json["payload"] as? [String: Any])
|
|
123
|
+
?? [:]
|
|
124
|
+
|
|
125
|
+
return AsturCommand(id: id, method: method, params: params)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private func resultJson(_ result: AsturCommandResult, elapsedMs: Int) -> [String: Any] {
|
|
129
|
+
var body: [String: Any] = [
|
|
130
|
+
"id": result.id,
|
|
131
|
+
"ok": result.ok,
|
|
132
|
+
"timing": [
|
|
133
|
+
"totalMs": elapsedMs,
|
|
134
|
+
"agentMs": elapsedMs,
|
|
135
|
+
"hostRoundTrips": 1,
|
|
136
|
+
"usedSnapshot": false,
|
|
137
|
+
"usedLegacyFallback": false
|
|
138
|
+
]
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if result.ok {
|
|
142
|
+
body["result"] = result.result ?? NSNull()
|
|
143
|
+
body["data"] = result.result ?? NSNull()
|
|
144
|
+
} else {
|
|
145
|
+
var error: [String: Any] = [
|
|
146
|
+
"code": result.error?.code ?? "UNKNOWN",
|
|
147
|
+
"message": result.error?.message ?? "Unknown iOS agent error."
|
|
148
|
+
]
|
|
149
|
+
if let details = result.error?.details {
|
|
150
|
+
error["details"] = details
|
|
151
|
+
if let detailMap = details as? [String: Any] {
|
|
152
|
+
body["diagnostics"] = detailMap["diagnostics"] ?? detailMap
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
body["error"] = error
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return body
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func errorJson(id: String, code: String, message: String) -> [String: Any] {
|
|
162
|
+
[
|
|
163
|
+
"id": id,
|
|
164
|
+
"ok": false,
|
|
165
|
+
"error": [
|
|
166
|
+
"code": code,
|
|
167
|
+
"message": message
|
|
168
|
+
],
|
|
169
|
+
"timing": [
|
|
170
|
+
"totalMs": 0,
|
|
171
|
+
"agentMs": 0,
|
|
172
|
+
"hostRoundTrips": 1
|
|
173
|
+
]
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func write(_ connection: NWConnection, status: Int, body: [String: Any]) {
|
|
178
|
+
let payload = (try? JSONSerialization.data(withJSONObject: body)) ?? Data()
|
|
179
|
+
var response = Data()
|
|
180
|
+
let header = """
|
|
181
|
+
HTTP/1.1 \(status) \(status == 200 ? "OK" : "Error")\r
|
|
182
|
+
content-type: application/json\r
|
|
183
|
+
content-length: \(payload.count)\r
|
|
184
|
+
connection: close\r
|
|
185
|
+
\r
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
response.append(header.data(using: .utf8)!)
|
|
189
|
+
response.append(payload)
|
|
190
|
+
connection.send(content: response, completion: .contentProcessed { _ in
|
|
191
|
+
connection.cancel()
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func elapsedMs(_ startedAt: Date) -> Int {
|
|
196
|
+
max(0, Int(Date().timeIntervalSince(startedAt) * 1_000))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private struct HttpRequest {
|
|
201
|
+
let method: String
|
|
202
|
+
let path: String
|
|
203
|
+
let body: Data
|
|
204
|
+
|
|
205
|
+
init?(data: Data) {
|
|
206
|
+
guard let separator = data.range(of: Data("\r\n\r\n".utf8)) else {
|
|
207
|
+
return nil
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let headerData = data[..<separator.lowerBound]
|
|
211
|
+
guard let header = String(data: headerData, encoding: .utf8) else {
|
|
212
|
+
return nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let lines = header.components(separatedBy: "\r\n")
|
|
216
|
+
let requestLine = lines.first?.split(separator: " ").map(String.init) ?? []
|
|
217
|
+
guard requestLine.count >= 2 else {
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let contentLength = lines
|
|
222
|
+
.first { $0.lowercased().hasPrefix("content-length:") }
|
|
223
|
+
.flatMap { Int($0.split(separator: ":", maxSplits: 1).last?.trimmingCharacters(in: .whitespaces) ?? "") }
|
|
224
|
+
?? 0
|
|
225
|
+
|
|
226
|
+
let bodyStart = separator.upperBound
|
|
227
|
+
guard data.count - bodyStart >= contentLength else {
|
|
228
|
+
return nil
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
self.method = requestLine[0]
|
|
232
|
+
self.path = requestLine[1]
|
|
233
|
+
self.body = data[bodyStart..<(bodyStart + contentLength)]
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private struct ServerFailure: Error, CustomStringConvertible {
|
|
238
|
+
let description: String
|
|
239
|
+
|
|
240
|
+
init(_ description: String) {
|
|
241
|
+
self.description = description
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private extension String {
|
|
246
|
+
var nonEmpty: String? {
|
|
247
|
+
isEmpty ? nil : self
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>ASTUR_AUT_BUNDLE_ID</key>
|
|
6
|
+
<string>$(ASTUR_AUT_BUNDLE_ID)</string>
|
|
7
|
+
<key>ASTUR_AUT_LAUNCH</key>
|
|
8
|
+
<string>$(ASTUR_AUT_LAUNCH)</string>
|
|
9
|
+
<key>ASTUR_IOS_AGENT_BRIDGE_URL</key>
|
|
10
|
+
<string>$(ASTUR_IOS_AGENT_BRIDGE_URL)</string>
|
|
11
|
+
<key>ASTUR_IOS_AGENT_PORT</key>
|
|
12
|
+
<string>$(ASTUR_IOS_AGENT_PORT)</string>
|
|
13
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
14
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
15
|
+
<key>CFBundleExecutable</key>
|
|
16
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
17
|
+
<key>CFBundleIdentifier</key>
|
|
18
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
19
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
20
|
+
<string>6.0</string>
|
|
21
|
+
<key>CFBundleName</key>
|
|
22
|
+
<string>$(PRODUCT_NAME)</string>
|
|
23
|
+
<key>CFBundlePackageType</key>
|
|
24
|
+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
25
|
+
<key>CFBundleShortVersionString</key>
|
|
26
|
+
<string>1.0</string>
|
|
27
|
+
<key>CFBundleVersion</key>
|
|
28
|
+
<string>1</string>
|
|
29
|
+
</dict>
|
|
30
|
+
</plist>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
final class AsturAgentUITests: XCTestCase {
|
|
4
|
+
private var server: AsturAgentServer?
|
|
5
|
+
|
|
6
|
+
override func tearDown() {
|
|
7
|
+
server?.stop()
|
|
8
|
+
server = nil
|
|
9
|
+
super.tearDown()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func testAgentServer() throws {
|
|
13
|
+
let environment = ProcessInfo.processInfo.environment
|
|
14
|
+
let bundleIdentifier = runtimeValue("ASTUR_AUT_BUNDLE_ID", environment: environment) ?? "com.astur.demo"
|
|
15
|
+
let port = UInt16(runtimeValue("ASTUR_IOS_AGENT_PORT", environment: environment) ?? "8788") ?? 8788
|
|
16
|
+
let shouldLaunch = runtimeValue("ASTUR_AUT_LAUNCH", environment: environment) != "0"
|
|
17
|
+
let bridgeUrl = runtimeValue("ASTUR_IOS_AGENT_BRIDGE_URL", environment: environment).flatMap(URL.init(string:))
|
|
18
|
+
|
|
19
|
+
let agent = AsturAgent(bundleIdentifier: bundleIdentifier)
|
|
20
|
+
if shouldLaunch {
|
|
21
|
+
agent.launchIfNeeded()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if let bridgeUrl {
|
|
25
|
+
AsturAgentBridgeClient(agent: agent, bridgeUrl: bridgeUrl).run()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let server = AsturAgentServer(port: port, agent: agent)
|
|
30
|
+
try server.start()
|
|
31
|
+
self.server = server
|
|
32
|
+
|
|
33
|
+
RunLoop.current.run()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func runtimeValue(_ key: String, environment: [String: String]) -> String? {
|
|
37
|
+
if let value = environment[key]?.nonEmpty, !value.hasPrefix("$(") {
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if let value = Bundle(for: AsturAgentUITests.self).object(forInfoDictionaryKey: key) as? String,
|
|
42
|
+
let nonEmpty = value.nonEmpty,
|
|
43
|
+
!nonEmpty.hasPrefix("$(") {
|
|
44
|
+
return nonEmpty
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return nil
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private extension String {
|
|
52
|
+
var nonEmpty: String? {
|
|
53
|
+
isEmpty ? nil : self
|
|
54
|
+
}
|
|
55
|
+
}
|