#!/usr/bin/env python3
"""
Extract GPTDriver session URL from .xcresult bundle
"""
import json
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
def extract_session_url_from_xcresult(xcresult_path: str) -> str | None:
"""Extract session URL from .xcresult bundle using xcresulttool."""
try:
# Get JSON representation of attachments
result = subprocess.run(
["xcrun", "xcresulttool", "get", "--path", xcresult_path, "--format", "json"],
capture_output=True,
text=True,
check=True
)
data = json.loads(result.stdout)
# Recursively search for the attachment
def find_attachment(obj, target_name="GPTDriver Session URL Link"):
if isinstance(obj, dict):
if obj.get("name") == target_name:
# Check userInfo for link
if "userInfo" in obj and "link" in obj["userInfo"]:
return obj["userInfo"]["link"]
# Or check if there's a payload reference
if "payloadRef" in obj:
return extract_from_payload(obj["payloadRef"]["id"], xcresult_path)
# Recursively search
for value in obj.values():
result = find_attachment(value, target_name)
if result:
return result
elif isinstance(obj, list):
for item in obj:
result = find_attachment(item, target_name)
if result:
return result
return None
def extract_from_payload(id_ref: str, xcresult_path: str) -> str | None:
"""Extract URL from webloc file attachment."""
try:
# Export the attachment
temp_dir = Path("/tmp/xcresult_extract")
temp_dir.mkdir(exist_ok=True)
subprocess.run(
["xcrun", "xcresulttool", "export", "--path", xcresult_path,
"--id", id_ref, "--type", "file", "--output-path", str(temp_dir / "attachment.webloc")],
check=True,
capture_output=True
)
# Parse the webloc XML file
webloc_path = temp_dir / "attachment.webloc"
if webloc_path.exists():
tree = ET.parse(webloc_path)
root = tree.getroot()
# webloc format: <dict><key>URL</key><string>https://...</string></dict>
url_elem = root.find(".//string")
if url_elem is not None:
return url_elem.text
except Exception as e:
print(f"Error extracting payload: {e}", file=sys.stderr)
return None
return find_attachment(data)
except Exception as e:
print(f"Error processing xcresult: {e}", file=sys.stderr)
return None
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: extract_session_url.py <path-to-xcresult>")
sys.exit(1)
url = extract_session_url_from_xcresult(sys.argv[1])
if url:
print(url)
else:
print("Session URL not found", file=sys.stderr)
sys.exit(1)
#!/usr/bin/env swift
import Foundation
// Extract session URL from xcresult bundle
func extractSessionURL(from xcresultPath: String) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
process.arguments = ["xcresulttool", "get", "--path", xcresultPath, "--format", "json"]
let pipe = Pipe()
process.standardOutput = pipe
do {
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
// Recursively search for attachment with name "GPTDriver Session URL Link"
func findAttachment(in obj: Any) -> String? {
if let dict = obj as? [String: Any] {
if dict["name"] as? String == "GPTDriver Session URL Link",
let userInfo = dict["userInfo"] as? [String: Any],
let link = userInfo["link"] as? String {
return link
}
for value in dict.values {
if let result = findAttachment(in: value) {
return result
}
}
} else if let array = obj as? [Any] {
for item in array {
if let result = findAttachment(in: item) {
return result
}
}
}
return nil
}
return findAttachment(in: json)
} catch {
print("Error: \(error)", to: &FileHandle.standardError)
return nil
}
}
// Usage
if CommandLine.arguments.count < 2 {
print("Usage: extract_session_url.swift <path-to-xcresult>", to: &FileHandle.standardError)
exit(1)
}
if let url = extractSessionURL(from: CommandLine.arguments[1]) {
print(url)
} else {
print("Session URL not found", to: &FileHandle.standardError)
exit(1)
}
[gptdriver][session:<session-id>] Live Session View | sessionURL=https://app.mobileboost.io/gpt-driver/sessions/<session-id>
# Show recent GPTDriver logs
log show --predicate 'subsystem == "com.gptdriver"' --last 1h
# Extract session URLs from logs
log show --predicate 'subsystem == "com.gptdriver" AND eventMessage CONTAINS "Live Session View"' \
--last 1h | grep -o 'sessionURL=[^|]*' | cut -d= -f2
# Save to file
log show --predicate 'subsystem == "com.gptdriver"' --last 1h > gptdriver_logs.txt
grep "sessionURL" gptdriver_logs.txt | sed 's/.*sessionURL=\([^|]*\).*/\1/'
# In your CI script, export logs before test completion
xcrun simctl spawn booted log collect --output gptdriver_logs.logarchive 2>/dev/null || true
# Extract session URLs
log show --archive gptdriver_logs.logarchive \
--predicate 'subsystem == "com.gptdriver" AND eventMessage CONTAINS "Live Session View"' \
| grep -o 'sessionURL=[^|]*' | cut -d= -f2
let driver = GptDriver(apiKey: "your-api-key")
try await driver.execute("Your command")
// Access session URL directly
if let sessionURL = driver.sessionURL {
print("Session URL: \(sessionURL)")
// Upload to CI artifact storage, send to Slack, etc.
}
let driver = GptDriver(apiKey: "your-api-key")
// Set up callback to capture session URL immediately
driver.onSessionCreated = { sessionURL in
print("Session created: \(sessionURL)")
// Example: Save to file for CI
let url = URL(fileURLWithPath: "session_url.txt")
try? sessionURL.write(to: url, atomically: true, encoding: .utf8)
// Example: Set as environment variable (if running in CI)
setenv("GPTDRIVER_SESSION_URL", sessionURL, 1)
// Example: Send to CI notification service
// sendToSlack(sessionURL)
// postToGitHubActions(sessionURL)
}
try await driver.execute("Your command")
name: XCTest CI
on: [push]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run Tests
run: |
xcodebuild test \
-scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-resultBundlePath TestResults.xcresult
- name: Extract Session URL
id: extract_url
run: |
SESSION_URL=$(xcrun xcresulttool get --path TestResults.xcresult --format json | \
python3 -c "import sys, json; \
data = json.load(sys.stdin); \
def find_url(obj): \
if isinstance(obj, dict): \
if obj.get('name') == 'GPTDriver Session URL Link' and 'userInfo' in obj: \
return obj['userInfo'].get('link'); \
for v in obj.values(): \
r = find_url(v); \
if r: return r; \
elif isinstance(obj, list): \
for item in obj: \
r = find_url(item); \
if r: return r; \
return None; \
print(find_url(data) or '')")
echo "url=$SESSION_URL" >> $GITHUB_OUTPUT
- name: Comment Session URL
if: steps.extract_url.outputs.url != ''
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `GPTDriver Session: ${{ steps.extract_url.outputs.url }}`
})
- name: Upload Test Results
uses: actions/upload-artifact@v3
with:
name: test-results
path: TestResults.xcresult