Skip to content

Commit 2d90eba

Browse files
authored
Fix Android and iOS emulator screen recording in GitHub Actions (#1058)
1 parent d8714f7 commit 2d90eba

File tree

2 files changed

+139
-60
lines changed

2 files changed

+139
-60
lines changed

.github/workflows/integration_tests.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,13 @@ jobs:
12671267
name: log-artifact
12681268
path: testapps/test-results-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }}*
12691269
retention-days: ${{ env.artifactRetentionDays }}
1270+
- name: Upload tvOS test video artifact
1271+
if: ${{ !cancelled() }}
1272+
uses: actions/upload-artifact@v3
1273+
with:
1274+
name: mobile-simulator-test-video-artifact
1275+
path: testapps/video-*-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }}.mp4
1276+
retention-days: ${{ env.artifactRetentionDays }}
12701277
- name: Download log artifacts
12711278
if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }}
12721279
uses: actions/download-artifact@v3

scripts/gha/test_simulator.py

Lines changed: 132 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@
7979
import json
8080
import os
8181
import pathlib
82+
import platform
8283
import shutil
8384
import signal
8485
import subprocess
86+
import tempfile
8587
import time
8688

8789
from absl import app
@@ -113,7 +115,7 @@
113115
}
114116

115117
_RESULT_FILE = "Results1.json"
116-
_TEST_RETRY = 3
118+
_MAX_ATTEMPTS = 3
117119
_CMD_TIMEOUT = 300
118120

119121
_DEVICE_NONE = "None"
@@ -251,7 +253,7 @@ def main(argv):
251253

252254
for app_path in ios_testapps:
253255
bundle_id = _get_bundle_id(app_path, config)
254-
logs=_run_apple_test(testapp_dir, bundle_id, app_path, ios_helper_app, device_id, _TEST_RETRY)
256+
logs=_run_apple_test(testapp_dir, bundle_id, app_path, ios_helper_app, device_id)
255257
tests.append(Test(testapp_path=app_path, logs=logs))
256258

257259
_shutdown_simulator()
@@ -291,7 +293,7 @@ def main(argv):
291293

292294
for app_path in tvos_testapps:
293295
bundle_id = _get_bundle_id(app_path, config)
294-
logs=_run_apple_test(testapp_dir, bundle_id, app_path, tvos_helper_app, device_id, _TEST_RETRY)
296+
logs=_run_apple_test(testapp_dir, bundle_id, app_path, tvos_helper_app, device_id, record_video_display="external")
295297
tests.append(Test(testapp_path=app_path, logs=logs))
296298

297299
_shutdown_simulator()
@@ -326,7 +328,7 @@ def main(argv):
326328

327329
for app_path in android_testapps:
328330
package_name = _get_package_name(app_path)
329-
logs=_run_android_test(testapp_dir, package_name, app_path, android_helper_project, _TEST_RETRY)
331+
logs=_run_android_test(testapp_dir, package_name, app_path, android_helper_project)
330332
tests.append(Test(testapp_path=app_path, logs=logs))
331333

332334
_shutdown_emulator()
@@ -387,23 +389,77 @@ def _build_tvos_helper(helper_project, device_name, device_os):
387389
return os.path.join(file_dir, file_name)
388390

389391

390-
def _record_apple_tests(video_name):
391-
command = "xcrun simctl io booted recordVideo -f --codec=h264 %s" % video_name
392-
logging.info("Recording test: %s", command)
393-
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, preexec_fn=os.setsid)
394-
395-
396-
def _stop_recording(record_process):
397-
logging.info("Stop recording test")
398-
try:
399-
os.killpg(record_process.pid, signal.SIGINT)
400-
except:
401-
logging.info("Stop recording test failed!!!")
392+
def _record_apple_tests(video_name, display):
393+
args = [
394+
"xcrun",
395+
"simctl",
396+
"io",
397+
"booted",
398+
"recordVideo",
399+
"-f",
400+
"--codec=h264",
401+
]
402+
if display is not None:
403+
args.append("--display=" + display)
404+
args.append(video_name)
405+
return _start_recording(args)
406+
407+
408+
def _start_recording(args):
409+
logging.info("Starting screen recording: %s", subprocess.list2cmdline(args))
410+
411+
output_file = tempfile.TemporaryFile()
412+
413+
# Specify CREATE_NEW_PROCESS_GROUP on Windows because it is required for
414+
# sending CTRL_C_SIGNAL, which is done by _stop_recording().
415+
proc = subprocess.Popen(
416+
args,
417+
stdout=output_file,
418+
stderr=subprocess.STDOUT,
419+
creationflags=
420+
subprocess.CREATE_NEW_PROCESS_GROUP
421+
if platform.system() == "Windows"
422+
else 0,
423+
)
424+
logging.info("Started screen recording with PID %s", proc.pid)
425+
return (proc, output_file)
426+
427+
428+
def _stop_recording(start_recording_retval):
429+
(proc, output_file) = start_recording_retval
430+
logging.info("Stopping screen recording with PID %s by sending CTRL+C",
431+
proc.pid)
432+
433+
# Make sure that `returncode` is set on the `Popen` object.
434+
proc.poll()
435+
436+
if proc.returncode is not None:
437+
logging.warning(
438+
"WARNING: Screen recording process ended prematurely with exit code %s",
439+
proc.returncode
440+
)
441+
output_file.seek(0)
442+
logging.warning("==== BEGIN screen recording output ====")
443+
for line in output_file.read().decode("utf8", errors="replace").splitlines():
444+
logging.warning("%s", line.rstrip())
445+
logging.warning("==== END screen recording output ====")
446+
447+
output_file.close()
448+
449+
proc.send_signal(
450+
signal.CTRL_C_SIGNAL
451+
if platform.system() == "Windows"
452+
else signal.SIGINT
453+
)
454+
proc.wait()
402455
time.sleep(5)
403456

404457

405458
def _save_recorded_apple_video(video_name, summary_dir):
406-
logging.info("Save test video to: %s", summary_dir)
459+
logging.info("Save test video %s to: %s", video_name, summary_dir)
460+
if not os.path.exists(video_name):
461+
logging.warning("Save test video failed: file not found: %s", video_name)
462+
return
407463
shutil.move(video_name, os.path.join(summary_dir, video_name))
408464

409465

@@ -502,23 +558,24 @@ def _has_uitests(app_path, config):
502558
return api.get("has_uitests", False)
503559

504560

505-
def _run_apple_test(testapp_dir, bundle_id, app_path, helper_app, device_id, retry=1):
561+
def _run_apple_test(testapp_dir, bundle_id, app_path, helper_app, device_id, max_attempts=_MAX_ATTEMPTS, record_video_display=None):
506562
"""Run helper test and collect test result."""
507-
logging.info("Running apple helper test: %s, %s, %s, %s", bundle_id, app_path, helper_app, device_id)
508-
_install_apple_app(app_path, device_id)
509-
video_name = "video-%s-%s-%s.mp4" % (bundle_id, retry, FLAGS.logfile_name)
510-
record_process = _record_apple_tests(video_name)
511-
_run_xctest(helper_app, device_id)
512-
_stop_recording(record_process)
513-
log = _get_apple_test_log(bundle_id, app_path, device_id)
514-
_uninstall_apple_app(bundle_id, device_id)
515-
result = test_validation.validate_results(log, test_validation.CPP)
516-
if not result.complete or (FLAGS.test_type=="uitest" and result.fails>0):
563+
attempt_num = 1
564+
while attempt_num <= max_attempts:
565+
logging.info("Running apple helper test (attempt %s of %s): %s, %s, %s, %s", attempt_num, max_attempts, bundle_id, app_path, helper_app, device_id)
566+
_install_apple_app(app_path, device_id)
567+
video_name = "video-%s-%s-%s.mp4" % (bundle_id, attempt_num, FLAGS.logfile_name)
568+
record_process = _record_apple_tests(video_name, display=record_video_display)
569+
_run_xctest(helper_app, device_id)
570+
_stop_recording(record_process)
571+
log = _get_apple_test_log(bundle_id, app_path, device_id)
572+
_uninstall_apple_app(bundle_id, device_id)
573+
result = test_validation.validate_results(log, test_validation.CPP)
574+
if result.complete and (FLAGS.test_type != "uitest" or result.fails == 0):
575+
break
517576
_save_recorded_apple_video(video_name, testapp_dir)
518-
if retry > 1:
519-
logging.info("Retry _run_apple_test. Remaining retry: %s", retry-1)
520-
return _run_apple_test(testapp_dir, bundle_id, app_path, helper_app, device_id, retry=retry-1)
521-
577+
attempt_num += 1
578+
522579
return log
523580

524581

@@ -629,7 +686,7 @@ def _create_and_boot_emulator(sdk_id):
629686
if not FLAGS.ci:
630687
command = "$ANDROID_HOME/emulator/emulator -avd test_emulator &"
631688
else:
632-
command = "$ANDROID_HOME/emulator/emulator -avd test_emulator -no-audio -no-boot-anim -gpu auto &"
689+
command = "$ANDROID_HOME/emulator/emulator -avd test_emulator -no-window -no-audio -no-boot-anim -gpu auto &"
633690
logging.info("Boot test emulator: %s", command)
634691
subprocess.Popen(command, universal_newlines=True, shell=True, stdout=subprocess.PIPE)
635692

@@ -673,25 +730,27 @@ def _get_package_name(app_path):
673730
return package_name
674731

675732

676-
def _run_android_test(testapp_dir, package_name, app_path, helper_project, retry=1):
677-
logging.info("Running android helper test: %s, %s, %s", package_name, app_path, helper_project)
678-
_install_android_app(app_path)
679-
video_name = "video-%s-%s-%s.mp4" % (package_name, retry, FLAGS.logfile_name)
680-
logcat_name = "logcat-%s-%s-%s.txt" % (package_name, retry, FLAGS.logfile_name)
681-
record_process = _record_android_tests(video_name)
682-
_clear_android_logcat()
683-
_run_instrumented_test()
684-
_stop_recording(record_process)
685-
log = _get_android_test_log(package_name)
686-
_uninstall_android_app(package_name)
687-
688-
result = test_validation.validate_results(log, test_validation.CPP)
689-
if not result.complete or (FLAGS.test_type=="uitest" and result.fails>0):
733+
def _run_android_test(testapp_dir, package_name, app_path, helper_project, max_attempts=_MAX_ATTEMPTS):
734+
attempt_num = 1
735+
while attempt_num <= max_attempts:
736+
logging.info("Running android helper test (attempt %s of %s): %s, %s, %s", attempt_num, max_attempts, package_name, app_path, helper_project)
737+
_install_android_app(app_path)
738+
video_name = "video-%s-%s-%s.mp4" % (package_name, attempt_num, FLAGS.logfile_name)
739+
logcat_name = "logcat-%s-%s-%s.txt" % (package_name, attempt_num, FLAGS.logfile_name)
740+
record_process = _record_android_tests(video_name)
741+
_clear_android_logcat()
742+
_run_instrumented_test()
743+
_stop_recording(record_process)
744+
log = _get_android_test_log(package_name)
745+
_uninstall_android_app(package_name)
746+
747+
result = test_validation.validate_results(log, test_validation.CPP)
748+
if result.complete and (FLAGS.test_type != "uitest" or result.fails == 0):
749+
break
750+
690751
_save_recorded_android_video(video_name, testapp_dir)
691752
_save_android_logcat(logcat_name, testapp_dir)
692-
if retry > 1:
693-
logging.info("Retry _run_android_test. Remaining retry: %s", retry-1)
694-
return _run_android_test(testapp_dir, package_name, app_path, helper_project, retry=retry-1)
753+
attempt_num += 1
695754

696755
return log
697756

@@ -726,9 +785,13 @@ def _install_android_helper_app(helper_project):
726785

727786

728787
def _record_android_tests(video_name):
729-
command = "adb shell screenrecord /sdcard/%s" % video_name
730-
logging.info("Recording test: %s", command)
731-
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, preexec_fn=os.setsid)
788+
return _start_recording([
789+
"adb",
790+
"shell",
791+
"screenrecord",
792+
"--bugreport",
793+
"/sdcard/%s" % video_name,
794+
])
732795

733796

734797
def _save_recorded_android_video(video_name, summary_dir):
@@ -774,12 +837,20 @@ def _get_android_test_log(test_package):
774837
return result.stdout
775838

776839

777-
def _run_with_retry(args, shell=False, check=True, timeout=_CMD_TIMEOUT, retry_time=_TEST_RETRY, device=_DEVICE_NONE, type=_RESET_TYPE_REBOOT):
778-
logging.info("run_with_retry: %s; remaining retry: %s", args, retry_time)
779-
if retry_time > 1:
840+
def _run_with_retry(args, shell=False, check=True, timeout=_CMD_TIMEOUT, max_attempts=_MAX_ATTEMPTS, device=_DEVICE_NONE, type=_RESET_TYPE_REBOOT):
841+
attempt_num = 1
842+
while attempt_num <= max_attempts:
843+
logging.info("run_with_retry: %s (attempt %s of %s)", args, attempt_num, max_attempts)
780844
try:
781845
subprocess.run(args, shell=shell, check=check, timeout=timeout)
782-
except:
846+
except subprocess.SubprocessError:
847+
logging.exception("run_with_retry: %s (attempt %s of %s) FAILED", args, attempt_num, max_attempts)
848+
849+
# If retries have been exhausted, just raise the exception
850+
if attempt_num >= max_attempts:
851+
raise
852+
853+
# Otherwise, reset the emulator/simulator and try again.
783854
if device == _DEVICE_NONE:
784855
pass
785856
elif device == _DEVICE_ANDROID:
@@ -788,9 +859,10 @@ def _run_with_retry(args, shell=False, check=True, timeout=_CMD_TIMEOUT, retry_t
788859
else:
789860
# Apple
790861
_reset_simulator_on_error(device, type)
791-
_run_with_retry(args, shell, check, timeout, retry_time-1, device, type)
792-
else:
793-
subprocess.run(args, shell=shell, check=False, timeout=timeout)
862+
else:
863+
break
864+
865+
attempt_num += 1
794866

795867

796868
if __name__ == '__main__':

0 commit comments

Comments
 (0)