Skip to content

Commit 99fc727

Browse files
committed
Adding a CLI for doing e2e tests
1 parent bfa4696 commit 99fc727

File tree

4 files changed

+449
-0
lines changed

4 files changed

+449
-0
lines changed

e2ecli/Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version:5.3
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "e2ecli",
6+
platforms: [
7+
.macOS(.v10_15)
8+
],
9+
products: [
10+
.executable(name: "e2ecli", targets: ["e2ecli"]),
11+
],
12+
dependencies: [
13+
.package(name: "Segment", path: "../../analytics-swift"),
14+
],
15+
targets: [
16+
.target(
17+
name: "e2ecli",
18+
dependencies: [
19+
"Segment"
20+
]
21+
),
22+
23+
]
24+
)

e2ecli/Sources/e2ecli/main.swift

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
#!/usr/bin/env swift
2+
//
3+
// main.swift
4+
//
5+
// A command‐line tool to test Segment Analytics‑Swift SDK actions from a JSON file.
6+
//
7+
8+
import Foundation
9+
import Segment
10+
11+
var writeKey: String = ""
12+
var apiHost: String = ""
13+
var analytics: Analytics? = nil
14+
15+
// MARK: - Action Handlers
16+
17+
func processConfigure(_ action: [String: Any]) {
18+
print("writeKey: \(writeKey)")
19+
print("apihost: \(apiHost)")
20+
let config = Configuration(writeKey: writeKey)
21+
var waitUntilStarted = true
22+
23+
// Set apiHost from command line argument or environment variable. May be overridden by test.
24+
if !apiHost.isEmpty {
25+
config.apiHost(apiHost)
26+
}
27+
28+
for option in action {
29+
switch option.key {
30+
case "action", "writekey":
31+
// Ignore, already handled.
32+
continue
33+
case "flushAt":
34+
if let flushAt = option.value as? Int {
35+
config.flushAt(flushAt)
36+
}
37+
case "flushInterval":
38+
if let flushInterval = option.value as? TimeInterval {
39+
config.flushInterval(flushInterval)
40+
}
41+
case "trackApplicationLifecycleEvents":
42+
if let flag = option.value as? Bool {
43+
config.trackApplicationLifecycleEvents(flag)
44+
}
45+
case "autoAddSegmentDestination":
46+
if let flag = option.value as? Bool {
47+
config.autoAddSegmentDestination(flag)
48+
}
49+
case "apiHost":
50+
if let apiHost = option.value as? String {
51+
config.apiHost(apiHost)
52+
}
53+
case "cdnHost":
54+
if let cdnHost = option.value as? String {
55+
config.cdnHost(cdnHost)
56+
}
57+
case "operatingMode":
58+
if let modeString = option.value as? String {
59+
if modeString.lowercased() == "synchronous" {
60+
config.operatingMode(.synchronous)
61+
} else {
62+
config.operatingMode(.asynchronous)
63+
}
64+
}
65+
case "userAgent":
66+
if let userAgent = option.value as? String {
67+
config.userAgent(userAgent)
68+
}
69+
case "jsonNonConformingNumberStrategy":
70+
if let strategy = option.value as? String {
71+
switch strategy.lowercased() {
72+
case "zero":
73+
config.jsonNonConformingNumberStrategy(.zero)
74+
case "throw":
75+
config.jsonNonConformingNumberStrategy(.throw)
76+
case "null":
77+
config.jsonNonConformingNumberStrategy(.null)
78+
default:
79+
print("Unsupported jsonNonConformingNumberStrategy: \(strategy)")
80+
}
81+
}
82+
case "storageMode":
83+
if let modeValue = option.value as? String {
84+
switch modeValue.lowercased() {
85+
case "disk":
86+
config.storageMode(.disk)
87+
default:
88+
print("Unsupported storageMode string value: \(modeValue)")
89+
}
90+
} else if let memoryCount = option.value as? Int {
91+
config.storageMode(.memory(memoryCount))
92+
}
93+
case "waitUntilStarted":
94+
// Not a config option but a behavior modification - default true, can set false for specific tests
95+
if let wait = option.value as? Bool {
96+
waitUntilStarted = wait
97+
}
98+
default:
99+
print("Unknown option: \(option.key)")
100+
}
101+
}
102+
103+
analytics = Analytics(configuration: config)
104+
if waitUntilStarted {
105+
analytics?.waitUntilStarted()
106+
}
107+
if let analytics = analytics {
108+
print("Configured analytics: \(analytics)")
109+
} else {
110+
print("Failed to configure analytics.")
111+
}
112+
}
113+
114+
// Process an 'identify' action.
115+
func processIdentify(_ action: [String: Any]) {
116+
guard let userId = action["userId"] as? String else {
117+
print("Missing userId in identify action.")
118+
return
119+
}
120+
// Optional traits dictionary.
121+
let traits = action["traits"] as? [String: Any]
122+
print("Identifying userId: \(userId)")
123+
analytics?.identify(userId: userId, traits: traits)
124+
}
125+
126+
// Process a 'track' action.
127+
func processTrack(_ action: [String: Any]) {
128+
guard let event = action["event"] as? String else {
129+
print("Missing event in track action.")
130+
return
131+
}
132+
let properties = action["properties"] as? [String: Any]
133+
print("Tracking event: \(event)")
134+
analytics?.track(name: event, properties: properties)
135+
}
136+
137+
// Process a 'screen' action.
138+
func processScreen(_ action: [String: Any]) {
139+
guard let name = action["name"] as? String else {
140+
print("Missing name in screen action.")
141+
return
142+
}
143+
let category = action["category"] as? String
144+
let properties = action["properties"] as? [String: Any]
145+
print("Screening with name: \(name)")
146+
analytics?.screen(title: name, category: category, properties: properties)
147+
}
148+
149+
func processGroup(_ action: [String: Any]) {
150+
guard let groupId = action["groupId"] as? String else {
151+
print("Missing groupId in group action.")
152+
return
153+
}
154+
print("Grouping with groupId: \(groupId)")
155+
analytics?.group(groupId: groupId)
156+
}
157+
158+
func processAlias(_ action: [String: Any]) {
159+
guard let alias = action["newId"] as? String else {
160+
print("Missing newId in alias action.")
161+
return
162+
}
163+
print("Alias to newId: \(alias)")
164+
analytics?.alias(newId: alias)
165+
}
166+
167+
func processFlush(_ action: [String: Any]) {
168+
if let wait = action["wait"] as? Bool, wait {
169+
let semaphore = DispatchSemaphore(value: 0)
170+
analytics?.flush {
171+
semaphore.signal()
172+
}
173+
let timeout = DispatchTime.now() + .seconds(10)
174+
if semaphore.wait(timeout: timeout) == .timedOut {
175+
print("Flush timed out.")
176+
}
177+
print("Flush completed.")
178+
} else {
179+
analytics?.flush {
180+
print("Flush completed.")
181+
}
182+
print("Flush scheduled.")
183+
}
184+
}
185+
186+
func processEnabled(_ action: [String: Any]) {
187+
guard let enabled = action["enabled"] as? Bool else {
188+
print("Missing enabled in enabled action.")
189+
return
190+
}
191+
print("Setting enabled to: \(enabled)")
192+
analytics?.enabled = enabled
193+
}
194+
195+
func processWait(_ action: [String: Any]) {
196+
guard let seconds = action["seconds"] as? Int else {
197+
print("Missing seconds in wait action.")
198+
return
199+
}
200+
print("Waiting for \(seconds) seconds.")
201+
sleep(UInt32(seconds))
202+
}
203+
204+
// Process one generic action according to its type.
205+
func processAction(_ action: [String: Any]) {
206+
guard let actionType = action["action"] as? String else {
207+
if let comment = action["comment"] as? String {
208+
print("Comment: \(comment)")
209+
} else {
210+
print("Missing action type in action: \(action)")
211+
}
212+
return
213+
}
214+
215+
switch actionType.lowercased() {
216+
case "configure":
217+
processConfigure(action)
218+
case "identify":
219+
processIdentify(action)
220+
case "track":
221+
processTrack(action)
222+
case "screen", "page":
223+
processScreen(action)
224+
case "group":
225+
processGroup(action)
226+
case "alias":
227+
processAlias(action)
228+
case "flush":
229+
processFlush(action)
230+
case "enabled":
231+
processEnabled(action)
232+
case "reset":
233+
analytics?.reset()
234+
case "purgeStorage":
235+
analytics?.purgeStorage()
236+
case "waitUntilStarted": // Only useful if `waitUntilStarted` is set to false in the configuration.
237+
analytics?.waitUntilStarted()
238+
case "wait":
239+
processWait(action)
240+
default:
241+
print("Unknown action: \(actionType)")
242+
}
243+
}
244+
245+
// MARK: - Main Program
246+
Telemetry.shared.enable = false
247+
Analytics.debugLogsEnabled = true
248+
249+
// Ensure the JSON filename is passed as a command line argument.
250+
guard CommandLine.arguments.count >= 2 else {
251+
print("Usage: \(CommandLine.arguments[0]) path/to/actions.json [-wWRITEKEY] [-aAPIHOST]")
252+
exit(1)
253+
}
254+
255+
// Get the JSON file path from the command line arguments.
256+
let jsonFilePath = CommandLine.arguments[1]
257+
let fileUrl = URL(fileURLWithPath: jsonFilePath)
258+
259+
// Get the writeKey and apiHost from the command line arguments or environment variable.
260+
writeKey = ""
261+
apiHost = ""
262+
for argument in CommandLine.arguments {
263+
if argument.hasPrefix("-w") {
264+
writeKey = String(argument.dropFirst(2))
265+
} else if argument.hasPrefix("-a") {
266+
apiHost = String(argument.dropFirst(2))
267+
}
268+
}
269+
270+
// Get the writeKey and apiHost from environment variables if not provided as command line arguments.
271+
if writeKey.isEmpty, let envWriteKey = ProcessInfo.processInfo.environment["E2E_WRITEKEY"] {
272+
writeKey = envWriteKey
273+
}
274+
275+
if apiHost.isEmpty, let envApiHost = ProcessInfo.processInfo.environment["E2E_APIHOST"] {
276+
apiHost = envApiHost
277+
}
278+
279+
if writeKey.isEmpty {
280+
print("Missing writeKey. Provide it as a command line argument with -wWRITEKEY or set the E2E_WRITEKEY environment variable.")
281+
exit(1)
282+
}
283+
284+
do {
285+
let jsonData = try Data(contentsOf: fileUrl)
286+
// Expecting the JSON file to contain an array of actions:
287+
// [
288+
// { "action": "identify", "userId": "user123", "traits": {"email": "[email protected]"} },
289+
// { "action": "track", "event": "Item Purchased", "properties": {"item": "book", "price": 10} },
290+
// { "action": "screen", "name": "Home", "category": "Landing", "properties": {"title": "Welcome"} },
291+
// { "action": "group", "groupId": "group123" }
292+
// ]
293+
guard
294+
let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: [])
295+
as? [[String: Any]]
296+
else {
297+
print("JSON file does not contain an array of actions")
298+
exit(1)
299+
}
300+
301+
// Process each action in the order received.
302+
for action in jsonArray {
303+
processAction(action)
304+
// Optionally flush after each action if needed:
305+
// Analytics.sharedInstance.flush()
306+
}
307+
308+
} catch {
309+
print("Error reading or parsing the JSON file: \(error)")
310+
exit(1)
311+
}
312+
313+
print("All actions processed.")
314+
// End of main.swift

0 commit comments

Comments
 (0)