Skip to content

Commit 2a543dc

Browse files
authored
Add playmode (in editor mode) test in CI (#305)
1 parent 12162cc commit 2a543dc

File tree

5 files changed

+209
-28
lines changed

5 files changed

+209
-28
lines changed

.github/workflows/integration_tests.yml

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ on:
1818
default: 'macos-latest'
1919
required: true
2020
platforms:
21-
description: 'CSV of Android,iOS,Windows,macOS,Linux'
22-
default: 'Android,iOS,Windows,macOS,Linux'
21+
description: 'CSV of Android,iOS,Windows,macOS,Linux,Playmode'
22+
default: 'Android,iOS,Windows,macOS,Linux,Playmode'
2323
required: true
2424
apis:
2525
description: 'CSV of apis to build and test'
@@ -167,7 +167,6 @@ jobs:
167167
TEST_MATRIX_PARAM="--auto_diff origin/${{github.event.pull_request.head.ref}}..${MERGE_BASE}"
168168
fi
169169
fi
170-
171170
# To feed input into the job matrix, we first need to convert to a JSON
172171
# list. Then we can use fromJson to define the field in the matrix for the tests job.
173172
echo "::set-output name=apis::$( python scripts/gha/print_matrix_configuration.py -c -w integration_tests -k apis -o "${{github.event.inputs.apis}}" ${TEST_MATRIX_PARAM} )"
@@ -195,6 +194,7 @@ jobs:
195194
build:
196195
name: build-${{ matrix.unity_version }}-${{matrix.os}}-${{ matrix.platform }}
197196
runs-on: ${{matrix.os}}
197+
if: ${{ needs.check_and_prepare.outputs.matrix_build_platform != '[]' }}
198198
needs: check_and_prepare
199199
strategy:
200200
fail-fast: false
@@ -393,6 +393,139 @@ jobs:
393393
exit 1
394394
fi
395395
396+
test_playmode:
397+
name: test-${{ matrix.unity_version }}-${{matrix.os}}-Playmode
398+
runs-on: ${{matrix.os}}
399+
if: contains(needs.check_and_prepare.outputs.platform, 'Playmode') && !cancelled()
400+
needs: check_and_prepare
401+
strategy:
402+
fail-fast: false
403+
matrix:
404+
unity_version: ${{ fromJson(needs.check_and_prepare.outputs.matrix_unity_versions) }}
405+
os: ${{ fromJson(needs.check_and_prepare.outputs.matrix_build_os) }}
406+
env:
407+
# LC_ALL, LANG and U3D_PASSWORD are needed for U3D.
408+
LC_ALL: en_US.UTF-8
409+
LANG: en_US.UTF-8
410+
U3D_PASSWORD: ""
411+
steps:
412+
- uses: actions/checkout@v2
413+
- name: Install Unity installer (U3D)
414+
timeout-minutes: 10
415+
shell: bash
416+
run: |
417+
gem install u3d
418+
u3d available
419+
# u3d available -u ${{ matrix.unity_version }} -p
420+
- name: Setup python
421+
uses: actions/setup-python@v2
422+
with:
423+
python-version: '3.7'
424+
- name: Install python deps
425+
timeout-minutes: 10
426+
shell: bash
427+
run: |
428+
pip install -r scripts/gha/requirements.txt
429+
- name: Install Unity
430+
timeout-minutes: 30
431+
shell: bash
432+
run: |
433+
python scripts/gha/unity_installer.py --install \
434+
--version ${{ matrix.unity_version }}
435+
- name: Activate Unity license
436+
timeout-minutes: 10
437+
shell: bash
438+
run: |
439+
python scripts/gha/unity_installer.py --activate_license \
440+
--version ${{ matrix.unity_version }} \
441+
--username "${{ secrets.UNITY_USERNAME }}" \
442+
--password "${{ secrets.UNITY_PASSWORD }}" \
443+
--serial_ids "${{ secrets.SERIAL_ID }}" \
444+
--logfile "testapps/activate_license.log"
445+
- name: Prepare for integration tests
446+
timeout-minutes: 10
447+
shell: bash
448+
run: |
449+
python scripts/gha/restore_secrets.py --passphrase "${{ secrets.TEST_SECRET }}"
450+
- name: Fetch prebuilt packaged SDK from previous run
451+
uses: dawidd6/action-download-artifact@v2
452+
if: ${{ github.event.inputs.test_packaged_sdk != '' }}
453+
with:
454+
name: 'firebase_unity_sdk.zip'
455+
workflow: 'packaging.yml'
456+
run_id: ${{ github.event.inputs.test_packaged_sdk }}
457+
458+
- name: Run Playmode (in editor mode) integration tests
459+
shell: bash
460+
run: |
461+
if [[ -n "${{ github.event.inputs.test_packaged_sdk }}" ]]; then
462+
unzip -q firebase_unity_sdk.zip -d ~/Downloads/
463+
else
464+
if [[ -z "${{ github.event.inputs.sdk_url }}" ]];then
465+
sdk_url="https://dl.google.com/firebase/sdk/unity/firebase_unity_sdk_8.7.0.zip"
466+
else
467+
sdk_url=${{ github.event.inputs.sdk_url }}
468+
fi
469+
curl ${sdk_url} -o ~/Downloads/firebase_unity_sdk.zip
470+
unzip -q ~/Downloads/firebase_unity_sdk.zip -d ~/Downloads/
471+
fi
472+
python scripts/gha/build_testapps.py \
473+
--t ${{ needs.check_and_prepare.outputs.apis }} \
474+
--u $( python scripts/gha/print_matrix_configuration.py -k version -u ${{matrix.unity_version}}) \
475+
--p Playmode \
476+
--ios_sdk ${{ needs.check_and_prepare.outputs.mobile_test_on }} \
477+
--plugin_dir ~/Downloads/firebase_unity_sdk \
478+
--output_directory "${{ github.workspace }}" \
479+
--artifact_name "${{ matrix.unity_version }}-${{matrix.os}}-Playmode" \
480+
--force_latest_runtime \
481+
--ci
482+
- name: Return Unity license
483+
# Always returns true, even when job failed or canceled. But will not run when a critical failure prevents the task from running.
484+
if: always()
485+
# Retry at most 3 times when Return Unity license fails.
486+
uses: nick-invision/retry@v2
487+
with:
488+
timeout_minutes: 10
489+
max_attempts: 3
490+
shell: bash
491+
command: |
492+
python scripts/gha/unity_installer.py --release_license \
493+
--version ${{ matrix.unity_version }} \
494+
--logfile "testapps/release_license.log"
495+
cat testapps/release_license.log
496+
- name: Prepare results summary artifact
497+
if: ${{ !cancelled() }}
498+
shell: bash
499+
run: |
500+
if [ ! -f test-results-${{ matrix.unity_version }}-${{matrix.os}}-Playmode.log.json ]; then
501+
# No summary was created, make a placeholder one.
502+
echo "__SUMMARY_MISSING__" > test-results-${{ matrix.unity_version }}-${{matrix.os}}-Playmode.log.json
503+
fi
504+
- name: Upload test results artifact
505+
uses: actions/upload-artifact@v3
506+
if: ${{ !cancelled() }}
507+
with:
508+
name: log-artifact
509+
path: test-results-${{ matrix.unity_version }}-${{matrix.os}}-Playmode*
510+
retention-days: ${{ env.artifactRetentionDays }}
511+
- name: Update PR label and comment
512+
if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }}
513+
shell: bash
514+
run: |
515+
python scripts/gha/it_workflow.py --stage progress \
516+
--token ${{github.token}} \
517+
--issue_number ${{needs.check_and_prepare.outputs.pr_number}}\
518+
--actor ${{github.actor}} \
519+
--commit ${{needs.check_and_prepare.outputs.github_ref}} \
520+
--run_id ${{github.run_id}}
521+
- name: Summarize test results
522+
if: ${{ !cancelled() }}
523+
shell: bash
524+
run: |
525+
cat test-results-${{ matrix.unity_version }}-${{matrix.os}}-Playmode.log
526+
if [[ "${{ job.status }}" != "success" ]]; then
527+
exit 1
528+
fi
396529
397530
test_desktop:
398531
name: test-${{ matrix.unity_version }}-${{ matrix.build_os }}-${{ matrix.os }}-desktop
@@ -572,10 +705,9 @@ jobs:
572705
exit 1
573706
fi
574707
575-
576708
summarize_results:
577709
name: "summarize-results"
578-
needs: [check_and_prepare, build, test_desktop, test_mobile]
710+
needs: [check_and_prepare, build, test_playmode, test_desktop, test_mobile]
579711
runs-on: ubuntu-latest
580712
if: ${{ !cancelled() }}
581713
steps:

scripts/gha/build_testapps.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import glob
9595
import os
9696
import platform
97+
import stat
9798
import shutil
9899
import subprocess
99100
import time
@@ -145,7 +146,8 @@
145146
_IOS: "iOS",
146147
_WINDOWS: "Win64",
147148
_MACOS: "OSXUniversal",
148-
_LINUX: "Linux64"
149+
_LINUX: "Linux64",
150+
_PLAYMODE: "Playmode"
149151
}
150152

151153
_SUPPORTED_PLATFORMS = (
@@ -264,6 +266,13 @@
264266
" c can be a combination of digits and letters.")
265267

266268

269+
@attr.s(frozen=False, eq=False)
270+
class Test(object):
271+
"""Holds data related to the testing of one testapp."""
272+
testapp_path = attr.ib()
273+
logs = attr.ib()
274+
275+
267276
def main(argv):
268277
del argv # Unused.
269278

@@ -286,6 +295,7 @@ def main(argv):
286295
platforms = validate_platforms(FLAGS.platforms)
287296

288297
output_root = os.path.join(root_output_dir, "testapps")
298+
playmode_tests = []
289299
failures = []
290300
for version in unity_versions:
291301
runtime = get_runtime(version, FLAGS.force_latest_runtime)
@@ -329,14 +339,17 @@ def main(argv):
329339
if p == _DESKTOP: # e.g. 'Desktop' -> 'OSXUniversal'
330340
p = get_desktop_platform()
331341
if p == _PLAYMODE:
332-
perform_in_editor_tests(dir_helper)
342+
logs = perform_in_editor_tests(dir_helper)
343+
playmode_tests.append(Test(testapp_path=dir_helper.unity_project_dir, logs=logs))
333344
else:
334345
build_testapp(
335346
dir_helper=dir_helper,
336347
api_config=api_config,
337348
ios_config=ios_config,
338349
target=_BUILD_TARGET[p])
339350
except (subprocess.SubprocessError, RuntimeError) as e:
351+
if p == _PLAYMODE:
352+
playmode_tests.append(Test(testapp_path=dir_helper.unity_project_dir, logs=str(e)))
340353
failures.append(
341354
Failure(
342355
testapp=testapp,
@@ -350,20 +363,32 @@ def main(argv):
350363
logging.info(f.read())
351364
# Free up space by removing unneeded Unity directory.
352365
if FLAGS.ci:
353-
shutil.rmtree(dir_helper.unity_project_dir)
366+
_rm_dir_safe(dir_helper.unity_project_dir)
354367
else:
355-
shutil.rmtree(os.path.join(dir_helper.unity_project_dir, "Library"))
368+
_rm_dir_safe(os.path.join(dir_helper.unity_project_dir, "Library"))
356369
logging.info("END %s", build_desc)
357370

358-
_collect_integration_tests(config, testapps, root_output_dir, output_dir, FLAGS.artifact_name)
359-
360-
return _summarize_results(
361-
testapps=testapps,
362-
platforms=platforms,
363-
versions=unity_versions,
364-
failures=failures,
365-
output_dir=root_output_dir,
366-
artifact_name=FLAGS.artifact_name)
371+
playmode_passes = True
372+
build_passes = True
373+
if _PLAYMODE in platforms:
374+
platforms.remove(_PLAYMODE)
375+
playmode_passes = test_validation.summarize_test_results(
376+
playmode_tests,
377+
test_validation.UNITY,
378+
root_output_dir,
379+
file_name="test-results-" + FLAGS.artifact_name + ".log")
380+
381+
if platforms:
382+
_collect_integration_tests(config, testapps, root_output_dir, output_dir, FLAGS.artifact_name)
383+
build_passes = _summarize_build_results(
384+
testapps=testapps,
385+
platforms=platforms,
386+
versions=unity_versions,
387+
failures=failures,
388+
output_dir=root_output_dir,
389+
artifact_name=FLAGS.artifact_name)
390+
391+
return (playmode_passes and build_passes)
367392

368393

369394
def setup_unity_project(dir_helper, setup_options):
@@ -531,7 +556,7 @@ def perform_in_editor_tests(dir_helper, retry_on_license_check=True):
531556
dir_helper.unity_path,
532557
dir_helper.unity_project_dir,
533558
shared_args=["-batchmode", "-nographics", "-accept-apiupdate"])
534-
log = dir_helper.make_log_path("editor_tests")
559+
log = dir_helper.make_log_path("build_Playmode")
535560
arg_builder.set_log_file(log)
536561
run_args = arg_builder.get_args_for_method("InEditorRunner.EditorRun")
537562
dir_helper.copy_editor_script("InEditorRunner.cs")
@@ -554,7 +579,7 @@ def perform_in_editor_tests(dir_helper, retry_on_license_check=True):
554579
open_process.kill()
555580
logging.info("Finished running playmode tests")
556581

557-
results = test_validation.validate_results_unity(text)
582+
results = test_validation.validate_results(text, test_validation.UNITY)
558583
if results.complete:
559584
if results.passes and not results.fails: # Success
560585
logging.info(results.summary)
@@ -564,6 +589,8 @@ def perform_in_editor_tests(dir_helper, retry_on_license_check=True):
564589
raise RuntimeError(
565590
"Tests did not finish running. Log tail:\n" + results.summary)
566591

592+
return text
593+
567594

568595
def run_xcodebuild(dir_helper, ios_config, device_type):
569596
"""Uses xcode project generated by Unity to build an iOS binary."""
@@ -619,7 +646,7 @@ def _collect_integration_tests(config, testapps, root_output_dir, output_dir, ar
619646
artifact_path = os.path.join(root_output_dir, testapps_artifact_dir)
620647
logging.info("Collecting artifacts to: %s", artifact_path)
621648
try:
622-
shutil.rmtree(artifact_path)
649+
_rm_dir_safe(artifact_path)
623650
except OSError as e:
624651
logging.warning("Failed to remove directory:\n%s", e.strerror)
625652

@@ -647,7 +674,7 @@ def _collect_integration_tests_platform(config, testapps, artifact_path, testapp
647674
break
648675

649676

650-
def _summarize_results(testapps, platforms, versions, failures, output_dir, artifact_name):
677+
def _summarize_build_results(testapps, platforms, versions, failures, output_dir, artifact_name):
651678
"""Logs a readable summary of the results of the build."""
652679
file_name = "build-results-" + artifact_name + ".log"
653680
summary = []
@@ -956,6 +983,24 @@ def update_unity_versions(version_path_map, log=logging.error):
956983
return valid_versions
957984

958985

986+
def _handle_readonly_file(func, path, excinfo):
987+
"""Function passed into shutil.rmtree to handle Access Denied error"""
988+
os.chmod(path, stat.S_IWRITE)
989+
func(path) # will re-throw if a different error occurrs
990+
991+
992+
def _rm_dir_safe(directory_path):
993+
"""Removes directory at given path. No error if dir doesn't exist."""
994+
logging.info("Deleting %s...", directory_path)
995+
try:
996+
shutil.rmtree(directory_path, onerror=_handle_readonly_file)
997+
except OSError as e:
998+
# There are two known cases where this can happen:
999+
# The directory doesn't exist (FileNotFoundError)
1000+
# A file in the directory is open in another process (PermissionError)
1001+
logging.warning("Failed to remove directory:\n%s", e.strerror)
1002+
1003+
9591004
def _fix_path(path):
9601005
"""Expands ~, normalizes slashes, and converts relative paths to absolute."""
9611006
if not path:

scripts/gha/integration_testing/test_validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def get_name(testapp_path):
321321
"""Returns testapp api."""
322322
testapps = PARAMETERS["integration_tests"]["config"]["apis"].split(",")
323323
for testapp in testapps:
324-
if testapp in testapp_path:
324+
if testapp.replace("_", "") in testapp_path.lower():
325325
return testapp
326326
return testapp_path
327327

scripts/gha/print_matrix_configuration.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
}
6868
},
6969
"config": {
70-
"platform": "Windows,macOS,Linux,Android,iOS",
70+
"platform": "Windows,macOS,Linux,Android,iOS,Playmode",
7171
"apis": "analytics,auth,crashlytics,database,dynamic_links,firestore,functions,installations,messaging,remote_config,storage",
7272
"mobile_test_on": "real"
7373
}
@@ -253,8 +253,10 @@ def filter_build_platform(platform):
253253
platform = platform.split(",")
254254
build_platform = []
255255
build_platform.extend(filter_mobile_platform(platform))
256+
# testapps from different desktop platforms are built in one job.
256257
desktop_platform = ','.join(list(filter(lambda p: p in platform, ["Windows", "macOS", "Linux"])))
257-
build_platform.append(desktop_platform)
258+
if desktop_platform:
259+
build_platform.append(desktop_platform)
258260
return build_platform
259261

260262

0 commit comments

Comments
 (0)