Skip to content

Commit 1758e1a

Browse files
Improve integration test logging
These makes a couple of changes, all with the end goal of making it easier to debug failures: 1) Separate the building of desktop integration tests and their execution into separate steps. It was unintuitive that the build step ran desktop tests, but not mobile tests. A new script desktop_tester.py finds and executes desktop tests. Separating them into steps also organizes the logs better. 2) Rearrange log output for tests. Previously tests output logs in arbitrary order: now successes will report first, failures last, so that when scrolling up from the overall summary, one will immediately see the failures. 3) Add a workflow step to report terse summaries. This will make it possible to quickly determine what went wrong without trudging through multiple steps and long logs. It may still be necessary to go through detailed logs for more context, but this should help with determining if that's necessary. To achieve this, every build/test script will report a summary, which gets written to a local file (summary.log in the top level output directory where the tests are built). The final step logs the contents of this file. The new function in test_validation.py, 'summarize_tests_results' standardizes how all test runners output results.
1 parent 1dda499 commit 1758e1a

File tree

6 files changed

+352
-141
lines changed

6 files changed

+352
-141
lines changed

.github/workflows/integration_tests.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,17 @@ jobs:
116116
pip install -r scripts/gha/requirements.txt
117117
python scripts/gha/restore_secrets.py --passphrase "${{ secrets.TEST_SECRET }}"
118118
119-
- name: Build integration tests (and run Desktop tests)
119+
- name: Build integration tests
120120
# The set up script for Android will download the NDK here.
121121
env:
122122
NDK_ROOT: '/tmp/android-ndk-r16b'
123123
run: |
124-
python scripts/gha/build_testapps.py --t ${{ github.event.inputs.apis }} --p ${{ matrix.target_platform }} --output_directory ${{ github.workspace }} --use_vcpkg --execute_desktop_testapp --noadd_timestamp
124+
python scripts/gha/build_testapps.py --t ${{ github.event.inputs.apis }} --p ${{ matrix.target_platform }} --output_directory ${{ github.workspace }} --use_vcpkg --noadd_timestamp
125125
126+
- name: Run desktop integration tests
127+
if: matrix.target_platform == 'Desktop' && !cancelled()
128+
run: |
129+
python scripts/gha/desktop_tester.py --testapp_dir testapps
126130
# Workaround for https://github.com/GoogleCloudPlatform/github-actions/issues/100
127131
# Must be run after the Python setup action
128132
- name: Set CLOUDSDK_PYTHON (Windows)
@@ -139,3 +143,7 @@ jobs:
139143
if: matrix.target_platform != 'Desktop' && !cancelled()
140144
run: |
141145
python scripts/gha/test_lab.py --android_model ${{ github.event.inputs.android_device }} --android_api ${{ github.event.inputs.android_api }} --ios_model ${{ github.event.inputs.ios_device }} --ios_version ${{ github.event.inputs.ios_version }} --testapp_dir ${{ github.workspace }}/testapps --code_platform cpp --key_file ${{ github.workspace }}/scripts/gha-encrypted/gcs_key_file.json
146+
- name: Summarize build and test results
147+
if: ${{ !cancelled() }}
148+
shell: bash
149+
run: cat testapps/summary.log

scripts/gha/build_testapps.py

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
import datetime
7070
from distutils import dir_util
7171
import os
72-
import pathlib
7372
import platform
7473
import shutil
7574
import subprocess
@@ -81,7 +80,7 @@
8180
import attr
8281

8382
from integration_testing import config_reader
84-
from integration_testing import provisioning
83+
from integration_testing import test_validation
8584
from integration_testing import xcodebuild
8685

8786
# Environment variables
@@ -145,11 +144,6 @@
145144
" the local spec repos available on this machine. Must also include iOS"
146145
" in platforms flag.")
147146

148-
flags.DEFINE_bool(
149-
"execute_desktop_testapp", True,
150-
"(Desktop only) Run the testapp after building it. Will return non-zero"
151-
" code if any tests fail inside the testapp.")
152-
153147
flags.DEFINE_string(
154148
"compiler", None,
155149
"(Desktop only) Specify the compiler with CMake during the testapps build."
@@ -187,6 +181,7 @@ def main(argv):
187181
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
188182
else:
189183
timestamp = ""
184+
output_dir = os.path.join(output_dir, "testapps" + timestamp)
190185

191186
ios_framework_dir = os.path.join(sdk_dir, "frameworks")
192187
ios_framework_exist = os.path.isdir(ios_framework_dir)
@@ -195,7 +190,7 @@ def main(argv):
195190

196191
if update_pod_repo and _IOS in platforms:
197192
_run(["pod", "repo", "update"])
198-
193+
199194
config = config_reader.read_config()
200195
cmake_flags = _get_desktop_compiler_flags(FLAGS.compiler, config.compilers)
201196
if _DESKTOP in platforms and FLAGS.use_vcpkg:
@@ -214,25 +209,22 @@ def main(argv):
214209
output_dir=output_dir,
215210
sdk_dir=sdk_dir,
216211
ios_framework_exist=ios_framework_exist,
217-
timestamp=timestamp,
218212
root_dir=root_dir,
219213
ios_sdk=FLAGS.ios_sdk,
220-
cmake_flags=cmake_flags,
221-
execute_desktop_testapp=FLAGS.execute_desktop_testapp)
214+
cmake_flags=cmake_flags)
222215
logging.info("END building for %s", testapp)
223216

224-
_summarize_results(testapps, platforms, failures)
217+
_summarize_results(testapps, platforms, failures, output_dir)
225218
return 1 if failures else 0
226219

227220

228221
def _build(
229-
testapp, platforms, api_config, output_dir, sdk_dir, ios_framework_exist,
230-
timestamp, root_dir, ios_sdk, cmake_flags, execute_desktop_testapp):
222+
testapp, platforms, api_config, output_dir, sdk_dir, ios_framework_exist,
223+
root_dir, ios_sdk, cmake_flags):
231224
"""Builds one testapp on each of the specified platforms."""
232225
testapp_dir = os.path.join(root_dir, api_config.testapp_path)
233226
project_dir = os.path.join(
234-
output_dir, "testapps" + timestamp, api_config.full_name,
235-
os.path.basename(testapp_dir))
227+
output_dir, api_config.full_name, os.path.basename(testapp_dir))
236228

237229
logging.info("Copying testapp project to %s", project_dir)
238230
os.makedirs(project_dir)
@@ -249,8 +241,6 @@ def _build(
249241
logging.info("BEGIN %s, %s", testapp, _DESKTOP)
250242
try:
251243
_build_desktop(sdk_dir, cmake_flags)
252-
if execute_desktop_testapp:
253-
_execute_desktop_testapp(project_dir)
254244
except subprocess.SubprocessError as e:
255245
failures.append(
256246
Failure(testapp=testapp, platform=_DESKTOP, error_message=str(e)))
@@ -287,36 +277,30 @@ def _build(
287277
return failures
288278

289279

290-
def _summarize_results(testapps, platforms, failures):
280+
def _summarize_results(testapps, platforms, failures, output_dir):
291281
"""Logs a readable summary of the results of the build."""
292-
logging.info(
293-
"FINISHED BUILDING TESTAPPS.\n\n\n"
294-
"Tried to build these testapps: %s\n"
295-
"On these platforms: %s",
296-
", ".join(testapps), ", ".join(platforms))
282+
summary = []
283+
summary.append("BUILD SUMMARY:")
284+
summary.append("TRIED TO BUILD: " + ",".join(testapps))
285+
summary.append("ON PLATFORMS: " + ",".join(platforms))
286+
297287
if not failures:
298-
logging.info("No failures occurred")
288+
summary.append("ALL BUILDS SUCCEEDED")
299289
else:
300-
# Collect lines, then log once, to reduce logging noise from timestamps etc.
301-
lines = ["Some failures occurred:"]
290+
summary.append("SOME FAILURES OCCURRED:")
302291
for i, failure in enumerate(failures, start=1):
303-
lines.append("%d: %s" % (i, failure.describe()))
304-
logging.info("\n".join(lines))
292+
summary.append("%d: %s" % (i, failure.describe()))
293+
summary = "\n".join(summary)
294+
295+
logging.info(summary)
296+
test_validation.write_summary(output_dir, summary)
305297

306298

307299
def _build_desktop(sdk_dir, cmake_flags):
308300
_run(["cmake", ".", "-DFIREBASE_CPP_SDK_DIR=" + sdk_dir] + cmake_flags)
309301
_run(["cmake", "--build", "."])
310302

311303

312-
def _execute_desktop_testapp(project_dir):
313-
if platform.system() == "Windows":
314-
testapp_path = os.path.join(project_dir, "Debug", "integration_test.exe")
315-
else:
316-
testapp_path = os.path.join(project_dir, "integration_test")
317-
_run([testapp_path], timeout=300)
318-
319-
320304
def _get_desktop_compiler_flags(compiler, compiler_table):
321305
"""Returns the command line flags for this compiler."""
322306
if not compiler: # None is an acceptable default value
@@ -362,31 +346,45 @@ def _validate_android_environment_variables():
362346
"""Checks environment variables that may be required for Android."""
363347
# Ultimately we let the gradle build be the source of truth on what env vars
364348
# are required, but try to repair holes and log warnings if we can't.
365-
logging.info("Checking environment variables for the Android build")
366-
if not os.environ.get(_ANDROID_NDK_HOME):
367-
ndk_root = os.environ.get(_NDK_ROOT)
368-
if ndk_root: # Use NDK_ROOT as a backup for ANDROID_NDK_HOME
369-
os.environ[_ANDROID_NDK_HOME] = ndk_root
370-
logging.info("%s not found, using %s", _ANDROID_NDK_HOME, _NDK_ROOT)
371-
else:
372-
logging.warning("Neither %s nor %s is set.", _ANDROID_NDK_HOME, _NDK_ROOT)
349+
android_home = os.environ.get(_ANDROID_HOME)
373350
if not os.environ.get(_JAVA_HOME):
374351
logging.warning("%s not set", _JAVA_HOME)
375352
if not os.environ.get(_ANDROID_SDK_HOME):
376-
android_home = os.environ.get(_ANDROID_HOME)
377353
if android_home: # Use ANDROID_HOME as backup for ANDROID_SDK_HOME
378354
os.environ[_ANDROID_SDK_HOME] = android_home
379355
logging.info("%s not found, using %s", _ANDROID_SDK_HOME, _ANDROID_HOME)
380356
else:
381-
logging.warning(
382-
"Neither %s nor %s is set", _ANDROID_SDK_HOME, _ANDROID_HOME)
357+
logging.warning("Missing: %s and %s", _ANDROID_SDK_HOME, _ANDROID_HOME)
358+
# Different environments may have different NDK env vars specified. We look
359+
# for these, in this order, and set the others to the first found.
360+
# If none are set, we check the default location for the ndk.
361+
ndk_path = None
362+
ndk_vars = [_NDK_ROOT, _ANDROID_NDK_HOME]
363+
for env_var in ndk_vars:
364+
val = os.environ.get(env_var)
365+
if val:
366+
ndk_path = val
367+
break
368+
if not ndk_path:
369+
if android_home:
370+
default_ndk_path = os.path.join(android_home, "ndk-bundle")
371+
if os.path.isdir(default_ndk_path):
372+
ndk_path = default_ndk_path
373+
if ndk_path:
374+
logging.info("Found ndk: %s", ndk_path)
375+
for env_var in ndk_vars:
376+
if os.environ.get(env_var) != ndk_path:
377+
logging.info("Setting %s to %s", env_var, ndk_path)
378+
os.environ[env_var] = ndk_path
379+
else:
380+
logging.warning("No NDK env var set. Set one of %s", ", ".join(ndk_vars))
383381

384382

385-
# If sdk_dir contains no framework, consider it is Github repo, then
383+
# If sdk_dir contains no framework, consider it is Github repo, then
386384
# generate makefiles for ios frameworks
387385
def _generate_makefiles_from_repo(sdk_dir):
388386
ios_framework_builder = os.path.join(
389-
sdk_dir, "build_scripts", "ios", "build.sh")
387+
sdk_dir, "build_scripts", "ios", "build.sh")
390388

391389
framework_builder_args = [
392390
ios_framework_builder,
@@ -400,15 +398,15 @@ def _generate_makefiles_from_repo(sdk_dir):
400398
# build required ios frameworks based on makefiles
401399
def _build_ios_framework_from_repo(sdk_dir, api_config):
402400
ios_framework_builder = os.path.join(
403-
sdk_dir, "build_scripts", "ios", "build.sh")
404-
401+
sdk_dir, "build_scripts", "ios", "build.sh")
402+
405403
# build only required targets to save time
406404
target = set()
407405
for framework in api_config.frameworks:
408406
target.add(os.path.splitext(framework)[0])
409407
# firebase is not a target in CMake, firebase_app is the target
410-
# firebase_app will be built by other target as well
411-
target.remove("firebase")
408+
# firebase_app will be built by other target as well
409+
target.remove("firebase")
412410

413411
framework_builder_args = [
414412
ios_framework_builder,
@@ -421,10 +419,10 @@ def _build_ios_framework_from_repo(sdk_dir, api_config):
421419

422420
def _build_ios(
423421
sdk_dir, ios_framework_exist, project_dir, root_dir, api_config, ios_sdk):
422+
"""Builds an iOS application (.app, .ipa or both)."""
424423
if not ios_framework_exist:
425424
_build_ios_framework_from_repo(sdk_dir, api_config)
426425

427-
"""Builds an iOS application (.app, .ipa or both)."""
428426
build_dir = os.path.join(project_dir, "ios_build")
429427
os.makedirs(build_dir)
430428

@@ -439,14 +437,10 @@ def _build_ios(
439437

440438
podfile_tool_path = os.path.join(
441439
root_dir, "scripts", "gha", "integration_testing", "update_podfile.py")
442-
sdk_podfile_path = os.path.join(
443-
root_dir, "ios_pod", "Podfile")
444-
app_podfile_path = os.path.join(
445-
project_dir, "Podfile")
446440
podfile_patcher_args = [
447441
"python", podfile_tool_path,
448-
"--sdk_podfile", sdk_podfile_path,
449-
"--app_podfile", app_podfile_path
442+
"--sdk_podfile", os.path.join(root_dir, "ios_pod", "Podfile"),
443+
"--app_podfile", os.path.join(project_dir, "Podfile")
450444
]
451445
_run(podfile_patcher_args)
452446
_run(["pod", "install"])
@@ -487,7 +481,8 @@ def _build_ios(
487481
ios_sdk=_IOS_SDK_DEVICE,
488482
configuration="Debug"))
489483

490-
xcodebuild.generate_unsigned_ipa(output_dir=build_dir, configuration="Debug")
484+
xcodebuild.generate_unsigned_ipa(
485+
output_dir=build_dir, configuration="Debug")
491486

492487

493488
# This script is responsible for copying shared files into the integration

scripts/gha/desktop_tester.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Runs and validates desktop C++ testapps.
16+
17+
Usage:
18+
python desktop_tester.py --testapp_dir ~/Downloads/testapps
19+
20+
This will search --testapp_dir for files whose name is given by --testapp_name
21+
(default: "integration_test", or "integration_test.exe" on Windows), execute
22+
them, parse their log output according to the C++ test summary format, and
23+
report a summary of results.
24+
25+
"""
26+
27+
import os
28+
import platform
29+
import subprocess
30+
import threading
31+
32+
from absl import app
33+
from absl import flags
34+
from absl import logging
35+
import attr
36+
37+
from integration_testing import test_validation
38+
39+
FLAGS = flags.FLAGS
40+
41+
flags.DEFINE_string("testapp_dir", None, "Look for testapps in this directory.")
42+
flags.DEFINE_string("testapp_name", "integration_test", "Name of the testapps.")
43+
44+
45+
def main(argv):
46+
if len(argv) > 1:
47+
raise app.UsageError("Too many command-line arguments.")
48+
49+
testapp_dir = _fix_path(FLAGS.testapp_dir)
50+
51+
testapp_name = FLAGS.testapp_name
52+
if platform.system() == "Windows":
53+
testapp_name += ".exe"
54+
55+
testapps = []
56+
for file_dir, _, file_names in os.walk(testapp_dir):
57+
for file_name in file_names:
58+
if file_name == testapp_name:
59+
testapps.append(os.path.join(file_dir, file_name))
60+
if not testapps:
61+
logging.error("No testapps found.")
62+
test_validation.write_summary(testapp_dir, "Desktop tests: none found.")
63+
return 1
64+
logging.info("Testapps found: %s\n", "\n".join(testapps))
65+
66+
tests = [Test(testapp_path=testapp) for testapp in testapps]
67+
68+
logging.info("Running tests...")
69+
threads = []
70+
for test in tests:
71+
thread = threading.Thread(target=test.run)
72+
threads.append(thread)
73+
thread.start()
74+
for thread in threads:
75+
thread.join()
76+
77+
return test_validation.summarize_test_results(
78+
tests, test_validation.CPP, testapp_dir)
79+
80+
81+
def _fix_path(path):
82+
"""Expands ~, normalizes slashes, and converts relative paths to absolute."""
83+
return os.path.abspath(os.path.expanduser(path))
84+
85+
86+
@attr.s(frozen=False, eq=False)
87+
class Test(object):
88+
"""Holds data related to the testing of one testapp."""
89+
testapp_path = attr.ib()
90+
# This will be populated after the test completes, instead of initialization.
91+
logs = attr.ib(init=False, default=None)
92+
93+
# This runs in a separate thread, so instead of returning values we store
94+
# them as fields so they can be accessed from the main thread.
95+
def run(self):
96+
"""Executes this testapp."""
97+
result = subprocess.run(
98+
args=[self.testapp_path],
99+
cwd=os.path.dirname(self.testapp_path), # Testapp checks CWD for config
100+
stdout=subprocess.PIPE,
101+
stderr=subprocess.STDOUT,
102+
text=True,
103+
check=False,
104+
timeout=300)
105+
logging.info("Finished running %s", self.testapp_path)
106+
107+
self.logs = result.stdout
108+
self.return_code = result.returncode
109+
110+
111+
if __name__ == "__main__":
112+
app.run(main)

0 commit comments

Comments
 (0)