[ci] Run the C++ unit tests on macOS without a sanitizer whose runtime aborts (#21000)

This commit is contained in:
J. Nick Koston
2026-10-17 09:00:51 -10:00
committed by GitHub
parent e110613ef1
commit 5c2b05d0eb
+53 -4
View File
@@ -3,7 +3,9 @@ import argparse
from functools import partial
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from build_helpers import (
PLATFORMIO_GOOGLE_TEST_LIB,
@@ -12,6 +14,9 @@ from build_helpers import (
)
from helpers import get_all_components, root_path
from esphome.core import EsphomeError
from esphome.host.toolchain import find_compilers
# Path to /tests/components
COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components"
@@ -20,11 +25,55 @@ BUILD_FLAGS = [
"-g3", # max debug info
"-ggdb3",
"-DESPHOME_DEBUG", # enable debug assertions
# Enable the address and undefined behavior sanitizers
"-fsanitize=address",
"-fsanitize=undefined",
"-fno-omit-frame-pointer",
]
SANITIZER_FLAGS = ["-fsanitize=address", "-fsanitize=undefined"]
def sanitizer_flags() -> list[str]:
"""The sanitizer flags to build with: all of them, except on macOS the ones whose runtime
does not start. Apple clang 15's AddressSanitizer aborts before main() on macOS 26, which
would fail every test for a toolchain problem, so each flag is tried on an empty program
first. A compiler that cannot build the empty program is an error, not a dropped flag.
CI keeps every flag.
"""
if sys.platform != "darwin":
return SANITIZER_FLAGS
try:
cxx = find_compilers().cxx
except EsphomeError as err:
sys.exit(str(err))
flags: list[str] = []
with tempfile.TemporaryDirectory() as tmp:
program = Path(tmp) / "probe"
for flag in SANITIZER_FLAGS:
compiled = subprocess.run(
[*cxx, flag, "-x", "c++", "-", "-o", program],
check=False,
input=b"int main() { return 0; }",
capture_output=True,
)
if compiled.returncode != 0:
sys.exit(
f"{cxx[0]} cannot build an empty program with {flag}:\n"
f"{compiled.stderr.decode(errors='replace').strip()}"
)
ran = subprocess.run([program], check=False, capture_output=True)
if ran.returncode == 0:
flags.append(flag)
continue
lines = [
line
for line in ran.stderr.decode(errors="replace").splitlines()
if line.strip()
]
print(
f"{flag} aborts at start with {cxx[0]}: "
f"{lines[0] if lines else f'exit code {ran.returncode}'}\n"
" building the tests without it; set CXX to a compiler whose runtime works to keep it",
file=sys.stderr,
)
return flags
def run_tests(selected_components: list[str]) -> int:
@@ -39,7 +88,7 @@ def run_tests(selected_components: list[str]) -> int:
config_prefix="cpptests",
friendly_name="CPP Unit Tests",
libraries=PLATFORMIO_GOOGLE_TEST_LIB,
build_flags=BUILD_FLAGS,
build_flags=BUILD_FLAGS + sanitizer_flags(),
main_entry="main.cpp",
label="unit tests",
)