@@ -10,15 +10,20 @@ jobs:
1010 name : Integration Test
1111 runs-on : ubuntu-latest
1212
13+ env :
14+ SAUCE_USERNAME : ${{ secrets.SAUCE_USERNAME }}
15+ SAUCE_ACCESS_KEY : ${{ secrets.SAUCE_ACCESS_KEY }}
16+ GITHUB_TOKEN : ${{ github.token }}
17+ SAUCE_REGION : eu-central-1
18+
1319 steps :
1420 - uses : actions/checkout@v4
15- with :
16- submodules : recursive
1721
18- - name : Download sample build
19- uses : actions/ download-artifact@v4
22+ - name : Download artifact from workflow run
23+ uses : dawidd6/action- download-artifact@v6
2024 with :
21- name : UE ${{ inputs.unreal-version }} sample build (Android)
25+ run_id : 19533023161
26+ name : " UE ${{ inputs.unreal-version }} sample build (Android)"
2227 path : sample-build
2328
2429 - name : List downloaded files
2732 ls -lah sample-build/
2833 echo "Using x64 APK for emulator testing"
2934
30- - name : Enable KVM group perms
31- run : |
32- echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
33- sudo udevadm control --reload-rules
34- sudo udevadm trigger --name-match=kvm
35-
36- - name : Setup Android directories
37- run : |
38- mkdir -p $HOME/.android/avd
39- touch $HOME/.android/repositories.cfg
40-
4135 - name : Install Pester
4236 shell : pwsh
4337 run : Install-Module -Name Pester -Force -SkipPublisherCheck
@@ -49,37 +43,197 @@ jobs:
4943 mkdir build
5044 cmake -B build -S .
5145
52- - name : Run Android Integration Tests
53- uses : reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b
54- id : integration-test
55- timeout-minutes : 45
56- with :
57- api-level : 34
58- target : ' google_apis'
59- arch : x86_64
60- force-avd-creation : true
61- disable-animations : true
62- disable-spellchecker : true
63- emulator-options : >
64- -no-window
65- -no-snapshot-save
66- -gpu swiftshader_indirect
67- -noaudio
68- -no-boot-anim
69- -camera-back none
70- -camera-front none
71- script : |
72- adb wait-for-device
73- cd integration-test && pwsh -Command "Invoke-Pester Integration.Tests.Android.ps1 -CI"
74- env :
75- SENTRY_UNREAL_TEST_DSN : ${{ secrets.SENTRY_UNREAL_TEST_DSN }}
76- SENTRY_AUTH_TOKEN : ${{ secrets.SENTRY_API_TOKEN }}
77- SENTRY_UNREAL_TEST_APP_PATH : ${{ github.workspace }}/sample-build/SentryPlayground-x64.apk
46+ - name : Upload APK to SauceLabs Storage
47+ id : upload_apk
48+ run : |
49+ sudo apt-get install -y jq
50+
51+ APK=$(ls sample-build/*.apk | head -n1)
52+ echo "Uploading: $APK"
53+
54+ RESPONSE=$(curl -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
55+ -X POST "https://api.${SAUCE_REGION}.saucelabs.com/v1/storage/upload" \
56+ -F "payload=@${APK}" \
57+ -F "name=$(basename "$APK")")
58+
59+ echo "Upload Response: $RESPONSE"
60+ STORAGE_ID=$(echo "$RESPONSE" | jq -r '.item.id')
61+
62+ echo "storage_id=$STORAGE_ID" >> $GITHUB_OUTPUT
63+
64+ - name : Start UE5 test on real device
65+ id : start_session
66+ run : |
67+ REQUEST=$(cat <<EOF
68+ {
69+ "capabilities": {
70+ "alwaysMatch": {
71+ "platformName": "Android",
72+ "appium:app": "storage:${{ steps.upload_apk.outputs.storage_id }}",
73+ "appium:deviceName": "Samsung_Galaxy_S23_FE_free",
74+ "appium:automationName": "UiAutomator2",
75+ "appium:noReset": true,
76+ "appium:autoLaunch": false,
77+ "sauce:options": {
78+ "name": "${{ inputs.unreal-version }} Android Integration Test",
79+ "appiumVersion": "latest"
80+ }
81+ }
82+ }
83+ }
84+ EOF
85+ )
86+
87+ echo "Sending Appium session request..."
88+ echo "$REQUEST"
89+
90+ RESPONSE=$(curl -sS -w "\nHTTP_CODE:%{http_code}" \
91+ -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
92+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session" \
93+ -H "Content-Type: application/json" \
94+ -d "$REQUEST")
95+
96+ echo "Session Response: $RESPONSE"
97+
98+ SESSION_ID=$(echo "$RESPONSE" | grep -v "HTTP_CODE" | jq -r '.value.sessionId // .sessionId')
99+ echo "Extracted session_id: $SESSION_ID"
100+ echo "session_id=$SESSION_ID" >> $GITHUB_OUTPUT
101+
102+ - name : First launch (crash capture)
103+ run : |
104+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
105+
106+ RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \
107+ -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
108+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \
109+ -H "Content-Type: application/json" \
110+ -d '{
111+ "appPackage": "io.sentry.unreal.sample",
112+ "appActivity": "com.epicgames.unreal.GameActivity",
113+ "appWaitActivity": "*",
114+ "intentAction": "android.intent.action.MAIN",
115+ "intentCategory": "android.intent.category.LAUNCHER",
116+ "optionalIntentArguments": "-e cmdline '-crash-capture'"
117+ }')
118+ echo "Launch response: $RESPONSE"
119+
120+ - name : Wait for app to finish (first run)
121+ run : |
122+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
123+
124+ echo "Waiting for app to finish..."
125+ MAX_WAIT=20 # Max wait time in seconds
126+ POLL_INTERVAL=2
127+ ELAPSED=0
128+
129+ while [ $ELAPSED -lt $MAX_WAIT ]; do
130+ STATE=$(curl -sSL \
131+ -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
132+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \
133+ -H "Content-Type: application/json" \
134+ -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \
135+ | jq -r '.value')
136+
137+ echo "App state: $STATE (elapsed: ${ELAPSED}s)"
138+
139+ # State 1 = not running, 0 = not installed
140+ if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then
141+ echo "App finished/crashed"
142+ break
143+ fi
144+
145+ sleep $POLL_INTERVAL
146+ ELAPSED=$((ELAPSED + POLL_INTERVAL))
147+ done
148+
149+ if [ $ELAPSED -ge $MAX_WAIT ]; then
150+ echo "Timeout waiting for app to finish"
151+ fi
152+
153+ - name : Get first run logs
154+ run : |
155+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
156+ curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
157+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \
158+ -H "Content-Type: application/json" \
159+ -d '{"type": "logcat"}' \
160+ -o first_run_logs.json
161+
162+ - name : Second launch (message capture)
163+ run : |
164+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
165+
166+ RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \
167+ -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
168+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \
169+ -H "Content-Type: application/json" \
170+ -d '{
171+ "appPackage": "io.sentry.unreal.sample",
172+ "appActivity": "com.epicgames.unreal.GameActivity",
173+ "appWaitActivity": "*",
174+ "intentAction": "android.intent.action.MAIN",
175+ "intentCategory": "android.intent.category.LAUNCHER",
176+ "optionalIntentArguments": "-e cmdline '-message-capture'"
177+ }')
178+ echo "Launch response: $RESPONSE"
179+
180+ - name : Wait for app to finish (second run)
181+ run : |
182+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
183+
184+ echo "Waiting for app to finish..."
185+ MAX_WAIT=20 # Max wait time in seconds
186+ POLL_INTERVAL=2
187+ ELAPSED=0
188+
189+ while [ $ELAPSED -lt $MAX_WAIT ]; do
190+ STATE=$(curl -sSL \
191+ -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
192+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \
193+ -H "Content-Type: application/json" \
194+ -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \
195+ | jq -r '.value')
196+
197+ echo "App state: $STATE (elapsed: ${ELAPSED}s)"
198+
199+ # State 1 = not running, 0 = not installed
200+ if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then
201+ echo "App finished/crashed"
202+ break
203+ fi
204+
205+ sleep $POLL_INTERVAL
206+ ELAPSED=$((ELAPSED + POLL_INTERVAL))
207+ done
208+
209+ if [ $ELAPSED -ge $MAX_WAIT ]; then
210+ echo "Timeout waiting for app to finish"
211+ fi
212+
213+ - name : Get second run logs
214+ run : |
215+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
216+ curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
217+ -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \
218+ -H "Content-Type: application/json" \
219+ -d '{"type": "logcat"}' \
220+ -o second_run_logs.json
221+
222+ - name : End session
223+ if : always()
224+ run : |
225+ SESSION_ID="${{ steps.start_session.outputs.session_id }}"
226+ if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "null" ]; then
227+ curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
228+ -X DELETE "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID"
229+ fi
78230
79231 - name : Upload integration test output
80- if : ${{ always() && steps.integration-test.outcome == 'failure' }}
232+ if : ${{ always() }}
81233 uses : actions/upload-artifact@v4
82234 with :
83- name : UE ${{ inputs.unreal-version }} integration test output (Android)
84- path : integration-test/output/
235+ name : UE ${{ inputs.unreal-version }} device logs (Android)
236+ path : |
237+ first_run_logs.json
238+ second_run_logs.json
85239 retention-days : 14
0 commit comments