Skip to content

Commit ee4ee67

Browse files
authored
Replace the FTL test script with FTL GHA (#451)
1 parent 719e5e6 commit ee4ee67

File tree

4 files changed

+159
-402
lines changed

4 files changed

+159
-402
lines changed

.github/workflows/integration_tests.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,26 +618,27 @@ jobs:
618618
run: |
619619
echo "::set-output name=device_type::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.mobile_device }} -get_device_type)"
620620
echo "::set-output name=device_platform::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.mobile_device }} -get_device_platform)"
621-
- name: Download Desktop integration tests artifact
621+
echo "::set-output name=device::$( python scripts/gha/print_matrix_configuration.py -k ${{ matrix.mobile_device }} -get_ftl_device)"
622+
- name: Download Mobile integration tests artifact
622623
uses: actions/download-artifact@v3
623624
with:
624625
path: testapps
625626
name: testapps-${{ matrix.unity_version }}-${{ matrix.build_os }}-${{ steps.device-info.outputs.device_platform }}
626-
- name: Install Cloud SDK
627+
- id: ftl_test
627628
if: steps.device-info.outputs.device_type == 'real'
628-
uses: google-github-actions/setup-gcloud@v0
629-
- name: Run Mobile integration tests on Real Device via FTL
629+
uses: FirebaseExtended/github-actions/[email protected]
630+
with:
631+
credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CREDENTIALS }}
632+
testapp_dir: testapps
633+
test_type: "game-loop"
634+
test_devices: ${{ steps.device-info.outputs.device }}
635+
- name: Read FTL Test Result
630636
if: steps.device-info.outputs.device_type == 'real'
631637
timeout-minutes: 60
632638
shell: bash
633639
run: |
634-
python scripts/gha/restore_secrets.py --passphrase "${{ secrets.TEST_SECRET }}"
635-
python scripts/gha/test_lab.py --testapp_dir testapps \
636-
--ios_device "${{ matrix.mobile_device }}" \
637-
--android_device "${{ matrix.mobile_device }}" \
638-
--logfile_name "${{ matrix.unity_version }}-${{ matrix.build_os }}-${{ matrix.mobile_device }}-mobile" \
639-
--code_platform unity \
640-
--key_file scripts/gha-encrypted/gcs_key_file.json
640+
python scripts/gha/read_ftl_test_result.py --test_result '${{ steps.ftl_test.outputs.test_summary }}' \
641+
--output_path testapps/test-results-${{ matrix.unity_version }}-${{ matrix.build_os }}-${{ matrix.mobile_device }}-mobile.log
641642
- name: Run Mobile integration tests on virtual device locally
642643
timeout-minutes: 60
643644
if: steps.device-info.outputs.device_type == 'virtual'

scripts/gha/print_matrix_configuration.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
EXPANDED_KEY: {
6464
"build_os": ["macos-latest","windows-latest"],
65-
"unity_version": ["2020", "2019", "2018", "2017"],
65+
"unity_version": ["2020", "2019", "2018"],
6666
"mobile_device": ["android_target", "emulator_latest", "ios_target", "simulator_target"],
6767
}
6868
},
@@ -132,16 +132,16 @@
132132
BUILD_CONFIGS = ["Unity Version(s)", "Build OS(s)", "Platform(s)", "Test Device(s)"]
133133

134134
TEST_DEVICES = {
135-
"android_min": {"platform": "Android", "type": "real", "model": "Nexus10", "version": "19"},
136-
"android_target": {"platform": "Android", "type": "real", "model": "blueline", "version": "28"},
137-
"android_latest": {"platform": "Android", "type": "real", "model": "flame", "version": "29"},
135+
"android_min": {"platform": "Android", "type": "real", "device": "model=Nexus10,version=19"},
136+
"android_target": {"platform": "Android", "type": "real", "device": "model=blueline,version=28"},
137+
"android_latest": {"platform": "Android", "type": "real", "device": "model=oriole,version=33"},
138138
"emulator_min": {"platform": "Android", "type": "virtual", "image": "system-images;android-18;google_apis;x86"},
139139
"emulator_target": {"platform": "Android", "type": "virtual", "image": "system-images;android-28;google_apis;x86_64"},
140140
"emulator_latest": {"platform": "Android", "type": "virtual", "image": "system-images;android-30;google_apis;x86_64"},
141141
"emulator_32bit": {"platform": "Android", "type": "virtual", "image": "system-images;android-30;google_apis;x86"},
142-
"ios_min": {"platform": "iOS", "type": "real", "model": "iphone8", "version": "11.4"},
143-
"ios_target": {"platform": "iOS", "type": "real", "model": "iphone8", "version": "14.7"},
144-
"ios_latest": {"platform": "iOS", "type": "real", "model": "iphone11", "version": "13.6"},
142+
"ios_min": {"platform": "iOS", "type": "real", "device": "model=iphone8,version=11.4"},
143+
"ios_target": {"platform": "iOS", "type": "real", "device": "model=iphone11,version=13.6"},
144+
"ios_latest": {"platform": "iOS", "type": "real", "device": "model=iphone8,version=14.7"},
145145
"simulator_min": {"platform": "iOS", "type": "virtual", "name": "iPhone 6", "version": "11.4"},
146146
"simulator_target": {"platform": "iOS", "type": "virtual", "name": "iPhone 8", "version": "14.5"},
147147
"simulator_latest": {"platform": "iOS", "type": "virtual", "name": "iPhone 11", "version": "14.4"},
@@ -276,6 +276,9 @@ def main():
276276
if args.get_device_platform:
277277
print(TEST_DEVICES.get(args.parm_key).get("platform"))
278278
return
279+
if args.get_ftl_device:
280+
print(TEST_DEVICES.get(args.parm_key).get("device"))
281+
return
279282
if args.desktop_os:
280283
print(filterdesktop_os(platform=args.parm_key))
281284
return
@@ -322,6 +325,7 @@ def parse_cmdline_args():
322325
parser.add_argument('-u', '--unity_version', help='Get unity setting based on unity major version. Used with "-k $unity_setting -u $unity_major_version"')
323326
parser.add_argument('-get_device_type', action='store_true', help='Get the device type, used with -k $device')
324327
parser.add_argument('-get_device_platform', action='store_true', help='Get the device platform, used with -k $device')
328+
parser.add_argument('-get_ftl_device', action='store_true', help='Get the ftl test device, used with -k $device')
325329
parser.add_argument('-desktop_os', type=bool, default=False, help='Get desktop test OS. Use with "-k $build_platform -desktop_os=1"')
326330
parser.add_argument('-mobile_platform', type=bool, default=False, help='Get mobile test platform. Use with "-k $build_platform -mobile_platform=1"')
327331
parser.add_argument('-build_platform', type=bool, default=False, help='Get build platform. Use with "-k $build_platform -build_platform=1"')

scripts/gha/read_ftl_test_result.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
r"""Tool for reading & validating game-loop test log from Firebase Test Lab
18+
GitHub Action (FTL GHA).
19+
https://github.com/FirebaseExtended/github-actions
20+
21+
This tool will read files from GCS storage bucket. Requires Cloud SDK installed
22+
with gsutil. (Should be installed by FTL GHA already.)
23+
24+
Usage:
25+
26+
python read_ftl_test_result.py --test_result ${JSON_format_output_from_FTL_GHA} \
27+
--output_path ${log_path}
28+
29+
"""
30+
31+
import os
32+
import attr
33+
import subprocess
34+
import json
35+
36+
from absl import app
37+
from absl import flags
38+
from absl import logging
39+
40+
from integration_testing import test_validation
41+
from integration_testing import gcs
42+
43+
44+
FLAGS = flags.FLAGS
45+
flags.DEFINE_string("test_result", None, "FTL test result in JSON format.")
46+
flags.DEFINE_string("output_path", None, "Log will be write into this path.")
47+
48+
@attr.s(frozen=False, eq=False)
49+
class Test(object):
50+
"""Holds data related to the testing of one testapp."""
51+
testapp_path = attr.ib()
52+
logs = attr.ib()
53+
54+
55+
def main(argv):
56+
if len(argv) > 1:
57+
raise app.UsageError("Too many command-line arguments.")
58+
59+
test_result = json.loads(FLAGS.test_result)
60+
tests = []
61+
for app in test_result.get("apps"):
62+
app_path = app.get("testapp_path")
63+
return_code = app.get("return_code")
64+
logging.info("testapp: %s\nreturn code: %s" % (app_path, return_code))
65+
if return_code == 0:
66+
gcs_dir = app.get("raw_result_link").replace("https://console.developers.google.com/storage/browser/", "gs://")
67+
logging.info("gcs_dir: %s" % gcs_dir)
68+
logs = _get_testapp_log_text_from_gcs(gcs_dir)
69+
logging.info("Test result: %s", logs)
70+
tests.append(Test(testapp_path=app_path, logs=logs))
71+
else:
72+
logging.error("Test failed: %s", app)
73+
tests.append(Test(testapp_path=app_path, logs=None))
74+
75+
(output_dir, file_name) = os.path.split(os.path.abspath(FLAGS.output_path))
76+
return test_validation.summarize_test_results(
77+
tests,
78+
"unity",
79+
output_dir,
80+
file_name=file_name)
81+
82+
83+
def _get_testapp_log_text_from_gcs(gcs_path):
84+
"""Gets the testapp log text generated by game loops."""
85+
try:
86+
gcs_contents = _gcs_list_dir(gcs_path)
87+
except subprocess.CalledProcessError as e:
88+
logging.error("Unexpected error searching GCS logs:\n%s", e)
89+
return None
90+
# The path to the testapp log depends on the platform, device, and scenario
91+
# being tested. Search for a json file with 'results' in the name to avoid
92+
# hard-coding too many assumptions about the path. The testapp log should be
93+
# the only json, but 'results' adds some redundancy in case this changes.
94+
matching_gcs_logs = [
95+
line for line in gcs_contents
96+
if line.endswith(".json") and "results" in line.lower()
97+
]
98+
if not matching_gcs_logs:
99+
logging.error("Failed to find results log on GCS.")
100+
return None
101+
# This assumes testapps only have one scenario. Could change in the future.
102+
if len(matching_gcs_logs) > 1:
103+
logging.warning("Multiple scenario logs found.")
104+
gcs_log = matching_gcs_logs[0]
105+
try:
106+
logging.info("Found results log: %s", gcs_log)
107+
log_text = _gcs_read_file(gcs_log)
108+
if not log_text:
109+
logging.warning("Testapp log is empty. App may have crashed.")
110+
return log_text
111+
except subprocess.CalledProcessError as e:
112+
logging.error("Unexpected error reading GCS log:\n%s", e)
113+
return None
114+
115+
116+
def _gcs_list_dir(gcs_path):
117+
"""Recursively returns a list of contents for a directory on GCS."""
118+
args = [gcs.GSUTIL, "ls", "-r", gcs_path]
119+
logging.info("Listing GCS contents: %s", " ".join(args))
120+
result = subprocess.run(args=args, capture_output=True, text=True, check=True)
121+
return result.stdout.splitlines()
122+
123+
124+
def _gcs_read_file(gcs_path):
125+
"""Extracts the contents of a file on GCS."""
126+
gcs_path = gcs.relative_path_to_gs_uri(gcs_path)
127+
args = [gcs.GSUTIL, "cat", gcs_path]
128+
logging.info("Reading GCS file: %s", " ".join(args))
129+
result = subprocess.run(args=args, capture_output=True, text=True, check=True)
130+
return result.stdout
131+
132+
133+
if __name__ == '__main__':
134+
flags.mark_flag_as_required("test_result")
135+
flags.mark_flag_as_required("output_path")
136+
app.run(main)

0 commit comments

Comments
 (0)