Skip to content

Commit 4b3d841

Browse files
authored
Replace the FTL test script with FTL GHA (#1060)
1 parent 78eef72 commit 4b3d841

File tree

5 files changed

+262
-448
lines changed

5 files changed

+262
-448
lines changed

.github/workflows/integration_tests.yml

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -988,9 +988,10 @@ jobs:
988988
timeout_minutes: 5
989989
max_attempts: 3
990990
command: scripts/gha/install_test_workflow_prereqs.sh -p Android -t true
991-
- id: get-device-type
991+
- id: device-info
992992
run: |
993-
echo "::set-output name=device_type::$( python scripts/gha/print_matrix_configuration.py -d -k ${{ matrix.android_device }} )"
993+
echo "::set-output name=device_type::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.android_device }} -get_device_type)"
994+
echo "::set-output name=device::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.android_device }} -get_ftl_device)"
994995
- name: Set up Node (16)
995996
uses: actions/setup-node@v2
996997
with:
@@ -1001,15 +1002,15 @@ jobs:
10011002
distribution: 'temurin'
10021003
java-version: '17'
10031004
- name: Setup Firestore Emulator
1004-
if: steps.get-device-type.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
1005+
if: steps.device-info.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
10051006
uses: nick-invision/retry@v2
10061007
with:
10071008
shell: bash
10081009
timeout_minutes: 5
10091010
max_attempts: 3
10101011
command: npm install -g firebase-tools
10111012
- name: Start Firestore Emulator
1012-
if: steps.get-device-type.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
1013+
if: steps.device-info.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
10131014
run: |
10141015
firebase emulators:start --only firestore --project demo-example &
10151016
- name: Setup java 8 for test_simulator.py
@@ -1018,27 +1019,31 @@ jobs:
10181019
distribution: 'temurin'
10191020
java-version: '8'
10201021
- name: Run Android integration tests on Emulator locally
1021-
timeout-minutes: 120
1022-
if: steps.get-device-type.outputs.device_type == 'virtual'
1022+
timeout-minutes: 150
1023+
if: steps.device-info.outputs.device_type == 'virtual'
10231024
run: |
10241025
python scripts/gha/test_simulator.py --testapp_dir testapps \
10251026
--test_type "${{ matrix.test_type }}" \
10261027
--android_device "${{ matrix.android_device }}" \
10271028
--logfile_name "android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}" \
10281029
--ci
1029-
- name: Install Cloud SDK
1030-
if: steps.get-device-type.outputs.device_type == 'real'
1031-
uses: google-github-actions/setup-gcloud@v0
1032-
- name: Run Android integration tests on Real Device via FTL
1033-
timeout-minutes: 60
1034-
if: steps.get-device-type.outputs.device_type == 'real'
1030+
- id: ftl_test
1031+
if: steps.device-info.outputs.device_type == 'real'
1032+
uses: FirebaseExtended/github-actions/[email protected]
1033+
timeout-minutes: 90
1034+
with:
1035+
credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CREDENTIALS }}
1036+
testapp_dir: testapps
1037+
test_type: "game-loop"
1038+
test_devices: ${{ steps.device-info.outputs.device }}
1039+
max_attempts: 3
1040+
validator: ${GITHUB_WORKSPACE}/scripts/gha/integration_testing/ftl_gha_validator.py
1041+
- name: Read FTL Test Result
1042+
if: steps.device-info.outputs.device_type == 'real'
1043+
shell: bash
10351044
run: |
1036-
python scripts/gha/test_lab.py --testapp_dir testapps \
1037-
--android_device "${{ matrix.android_device }}" \
1038-
--logfile_name "android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}" \
1039-
--code_platform cpp \
1040-
--key_file_encrypted scripts/gha-encrypted/gcs_key_file.json.gpg \
1041-
--key_file_passphrase "${{ secrets.TEST_SECRET }}"
1045+
python scripts/gha/read_ftl_test_result.py --test_result '${{ steps.ftl_test.outputs.test_summary }}' \
1046+
--output_path testapps/test-results-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}.log
10421047
- name: Prepare results summary artifact
10431048
if: ${{ !cancelled() }}
10441049
shell: bash
@@ -1055,14 +1060,14 @@ jobs:
10551060
path: testapps/test-results-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}*
10561061
retention-days: ${{ env.artifactRetentionDays }}
10571062
- name: Upload Android test video artifact
1058-
if: ${{ steps.get-device-type.outputs.device_type == 'virtual' && !cancelled() }}
1063+
if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }}
10591064
uses: actions/upload-artifact@v3
10601065
with:
10611066
name: mobile-simulator-test-video-artifact
10621067
path: testapps/video-*-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}.mp4
10631068
retention-days: ${{ env.artifactRetentionDays }}
10641069
- name: Upload Android test logcat artifact
1065-
if: ${{ steps.get-device-type.outputs.device_type == 'virtual' && !cancelled() }}
1070+
if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }}
10661071
uses: actions/upload-artifact@v3
10671072
with:
10681073
name: mobile-simulator-test-logcat-artifact
@@ -1130,9 +1135,10 @@ jobs:
11301135
timeout_minutes: 3
11311136
max_attempts: 3
11321137
command: scripts/gha/install_test_workflow_prereqs.sh -p iOS -t true
1133-
- id: get-device-type
1138+
- id: device-info
11341139
run: |
1135-
echo "::set-output name=device_type::$( python scripts/gha/print_matrix_configuration.py -d -k ${{ matrix.ios_device }} )"
1140+
echo "::set-output name=device_type::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.ios_device }} -get_device_type)"
1141+
echo "::set-output name=device::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.ios_device }} -get_ftl_device)"
11361142
- name: Set up Node (16)
11371143
uses: actions/setup-node@v2
11381144
with:
@@ -1143,40 +1149,43 @@ jobs:
11431149
distribution: 'temurin'
11441150
java-version: '17'
11451151
- name: Setup Firestore Emulator
1146-
if: steps.get-device-type.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
1152+
if: steps.device-info.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
11471153
uses: nick-invision/retry@v2
11481154
with:
11491155
shell: bash
11501156
timeout_minutes: 5
11511157
max_attempts: 3
11521158
command: npm install -g firebase-tools
11531159
- name: Start Firestore Emulator
1154-
if: steps.get-device-type.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
1160+
if: steps.device-info.outputs.device_type == 'virtual' && contains(needs.check_and_prepare.outputs.apis, 'firestore')
11551161
run: |
11561162
firebase emulators:start --only firestore --project demo-example &
11571163
- name: Run iOS integration tests on Simulator locally
1158-
timeout-minutes: 120
1159-
if: steps.get-device-type.outputs.device_type == 'virtual'
1164+
timeout-minutes: 150
1165+
if: steps.device-info.outputs.device_type == 'virtual'
11601166
run: |
11611167
python scripts/gha/test_simulator.py --testapp_dir testapps \
11621168
--test_type "${{ matrix.test_type }}" \
11631169
--ios_device "${{ matrix.ios_device }}" \
11641170
--logfile_name "ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}" \
11651171
--ci
1166-
- name: Install Cloud SDK
1167-
if: steps.get-device-type.outputs.device_type == 'real'
1168-
uses: google-github-actions/setup-gcloud@v0
1169-
- name: Run iOS integration tests on Real Device via FTL
1170-
# max 3 retry and 10m timeout for each testapp, plus other steps
1171-
timeout-minutes: 60
1172-
if: steps.get-device-type.outputs.device_type == 'real'
1172+
- id: ftl_test
1173+
if: steps.device-info.outputs.device_type == 'real'
1174+
uses: FirebaseExtended/github-actions/[email protected]
1175+
timeout-minutes: 90
1176+
with:
1177+
credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CREDENTIALS }}
1178+
testapp_dir: testapps
1179+
test_type: "game-loop"
1180+
test_devices: ${{ steps.device-info.outputs.device }}
1181+
max_attempts: 3
1182+
validator: ${GITHUB_WORKSPACE}/scripts/gha/integration_testing/ftl_gha_validator.py
1183+
- name: Read FTL Test Result
1184+
if: steps.device-info.outputs.device_type == 'real'
1185+
shell: bash
11731186
run: |
1174-
python scripts/gha/test_lab.py --testapp_dir testapps \
1175-
--ios_device "${{ matrix.ios_device }}" \
1176-
--logfile_name "ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}" \
1177-
--code_platform cpp \
1178-
--key_file_encrypted scripts/gha-encrypted/gcs_key_file.json.gpg \
1179-
--key_file_passphrase "${{ secrets.TEST_SECRET }}"
1187+
python scripts/gha/read_ftl_test_result.py --test_result '${{ steps.ftl_test.outputs.test_summary }}' \
1188+
--output_path testapps/test-results-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}.log
11801189
- name: Prepare results summary artifact
11811190
if: ${{ !cancelled() }}
11821191
shell: bash
@@ -1193,7 +1202,7 @@ jobs:
11931202
path: testapps/test-results-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}*
11941203
retention-days: ${{ env.artifactRetentionDays }}
11951204
- name: Upload iOS test video artifact
1196-
if: ${{ steps.get-device-type.outputs.device_type == 'virtual' && !cancelled() }}
1205+
if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }}
11971206
uses: actions/upload-artifact@v3
11981207
with:
11991208
name: mobile-simulator-test-video-artifact
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2022 Google
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
18+
import logging
19+
import subprocess
20+
import shutil
21+
import re
22+
23+
GSUTIL = shutil.which("gsutil")
24+
25+
def validate(test_summary):
26+
if not test_summary:
27+
logging.error("Nothing to be validate! Please provide test_summary")
28+
return False
29+
30+
app_path = test_summary.get("testapp_path")
31+
return_code = test_summary.get("return_code")
32+
logging.info("testapp: %s\nreturn code: %s" % (app_path, return_code))
33+
if return_code == 0:
34+
gcs_dir = test_summary.get("raw_result_link").replace("https://console.developers.google.com/storage/browser/", "gs://")
35+
logging.info("gcs_dir: %s" % gcs_dir)
36+
logs = _get_testapp_log_text_from_gcs(gcs_dir)
37+
logging.info("Test result: %s", logs)
38+
return _validate_logs(logs)
39+
else:
40+
logging.error("Test failed: %s", app_path)
41+
return False
42+
43+
44+
def _get_testapp_log_text_from_gcs(gcs_path):
45+
"""Gets the testapp log text generated by game loops."""
46+
try:
47+
gcs_contents = _gcs_list_dir(gcs_path)
48+
except subprocess.CalledProcessError as e:
49+
logging.error("Unexpected error searching GCS logs:\n%s", e)
50+
return None
51+
# The path to the testapp log depends on the platform, device, and scenario
52+
# being tested. Search for a json file with 'results' in the name to avoid
53+
# hard-coding too many assumptions about the path. The testapp log should be
54+
# the only json, but 'results' adds some redundancy in case this changes.
55+
matching_gcs_logs = [
56+
line for line in gcs_contents
57+
if line.endswith(".json") and "results" in line.lower()
58+
]
59+
if not matching_gcs_logs:
60+
logging.error("Failed to find results log on GCS.")
61+
return None
62+
# This assumes testapps only have one scenario. Could change in the future.
63+
if len(matching_gcs_logs) > 1:
64+
logging.warning("Multiple scenario logs found.")
65+
gcs_log = matching_gcs_logs[0]
66+
try:
67+
logging.info("Found results log: %s", gcs_log)
68+
log_text = _gcs_read_file(gcs_log)
69+
if not log_text:
70+
logging.warning("Testapp log is empty. App may have crashed.")
71+
return log_text
72+
except subprocess.CalledProcessError as e:
73+
logging.error("Unexpected error reading GCS log:\n%s", e)
74+
return None
75+
76+
77+
def _gcs_list_dir(gcs_path):
78+
"""Recursively returns a list of contents for a directory on GCS."""
79+
args = [GSUTIL, "ls", "-r", gcs_path]
80+
logging.info("Listing GCS contents: %s", " ".join(args))
81+
result = subprocess.run(args=args, capture_output=True, text=True, check=True)
82+
return result.stdout.splitlines()
83+
84+
85+
def _gcs_read_file(gcs_path):
86+
"""Extracts the contents of a file on GCS."""
87+
args = [GSUTIL, "cat", gcs_path]
88+
logging.info("Reading GCS file: %s", " ".join(args))
89+
result = subprocess.run(args=args, capture_output=True, text=True, check=True)
90+
return result.stdout
91+
92+
93+
def _validate_logs(log_text):
94+
if not log_text:
95+
return False
96+
97+
# The gtest runner dumps a useful summary of tests after the tear down.
98+
end_marker = "Global test environment tear-down"
99+
complete = end_marker in log_text
100+
101+
if complete:
102+
# rpartition splits a string into three components around the final
103+
# occurrence of the end marker, returning a triplet (before, marker, after)
104+
result_summary = log_text.rpartition(end_marker)[2].lstrip()
105+
failures = re.search(r"\[ FAILED \] (?P<count>[0-9]+) test", result_summary)
106+
logging.info("_validate_logs failures: %s", failures)
107+
if failures:
108+
return False
109+
else:
110+
return True
111+
else:
112+
return False

scripts/gha/print_matrix_configuration.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,20 @@
161161
"tvos": ["os", "xcode_version", "tvos_device"]
162162
}
163163

164+
# Check currently supported models and versions with the following commands:
165+
# gcloud firebase test android models list
166+
# gcloud firebase test ios models list
164167
TEST_DEVICES = {
165-
"android_min": {"type": "real", "model":"Nexus10", "version":"19"},
166-
"android_target": {"type": "real", "model":"gts4lltevzw", "version":"28"},
167-
"android_latest": {"type": "real", "model":"redfin", "version":"30"},
168+
"android_min": {"type": "real", "device": "model=Nexus10,version=19"},
169+
"android_target": {"type": "real", "device": "model=gts4lltevzw,version=28"},
170+
"android_latest": {"type": "real", "device": "model=oriole,version=33"},
168171
"emulator_min": {"type": "virtual", "image":"system-images;android-18;google_apis;x86"},
169172
"emulator_target": {"type": "virtual", "image":"system-images;android-30;google_apis;x86_64"},
170173
"emulator_latest": {"type": "virtual", "image":"system-images;android-32;google_apis;x86_64"},
171174
"emulator_32bit": {"type": "virtual", "image":"system-images;android-30;google_apis;x86"},
172-
"ios_min": {"type": "real", "model":"iphone8", "version":"12.4"},
173-
"ios_target": {"type": "real", "model":"iphone8", "version":"13.6"},
174-
"ios_latest": {"type": "real", "model":"iphone11pro", "version":"14.7"},
175+
"ios_min": {"type": "real", "device": "model=iphonexr,version=13.2"},
176+
"ios_target": {"type": "real", "device": "model=iphone8,version=13.6"},
177+
"ios_latest": {"type": "real", "device": "model=iphone11pro,version=14.7"},
175178
"simulator_min": {"type": "virtual", "name":"iPhone 8", "version":"13.7"},
176179
"simulator_target": {"type": "virtual", "name":"iPhone 8", "version":"14.5"},
177180
"simulator_latest": {"type": "virtual", "name":"iPhone 11", "version":"15.2"},
@@ -356,9 +359,12 @@ def main():
356359
print_value(args.override)
357360
return
358361

359-
if args.device:
362+
if args.get_device_type:
360363
print(TEST_DEVICES.get(args.parm_key).get("type"))
361364
return
365+
if args.get_ftl_device:
366+
print(TEST_DEVICES.get(args.parm_key).get("device"))
367+
return
362368

363369
if args.expanded:
364370
test_matrix = EXPANDED_KEY
@@ -383,7 +389,8 @@ def parse_cmdline_args():
383389
parser.add_argument('-k', '--parm_key', required=True, help='Print the value of specified key from matrix or config maps.')
384390
parser.add_argument('-a', '--auto_diff', metavar='BRANCH', help='Compare with specified base branch to automatically set matrix options')
385391
parser.add_argument('-o', '--override', help='Override existing value with provided value')
386-
parser.add_argument('-d', '--device', action='store_true', help='Get the device type, used with -k $device')
392+
parser.add_argument('-get_device_type', action='store_true', help='Get the device type, used with -k $device')
393+
parser.add_argument('-get_ftl_device', action='store_true', help='Get the ftl test device, used with -k $device')
387394
parser.add_argument('-t', '--device_type', default=['real', 'virtual'], help='Test on which type of mobile devices')
388395
parser.add_argument('--apis', default=PARAMETERS["integration_tests"]["config"]["apis"],
389396
help='Exclude platform based on apis. Certain platform does not support all apis. e.g. tvOS does not support messaging')

0 commit comments

Comments
 (0)