mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 12:12:48 +00:00
120 lines
3.9 KiB
Python
Executable File
120 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
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,
|
|
build_and_run,
|
|
load_test_manifest_overrides,
|
|
)
|
|
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"
|
|
|
|
BUILD_FLAGS = [
|
|
"-Og", # optimize for debug
|
|
"-g3", # max debug info
|
|
"-ggdb3",
|
|
"-DESPHOME_DEBUG", # enable debug assertions
|
|
"-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:
|
|
# allocator_may_return_null: an oversized request must come back empty, not abort the run
|
|
os.environ["ASAN_OPTIONS"] = "detect_leaks=0:allocator_may_return_null=1"
|
|
return build_and_run(
|
|
selected_components=selected_components,
|
|
tests_dir=COMPONENTS_TESTS_DIR,
|
|
manifest_override_loader=partial(
|
|
load_test_manifest_overrides, tests_dir=COMPONENTS_TESTS_DIR
|
|
),
|
|
config_prefix="cpptests",
|
|
friendly_name="CPP Unit Tests",
|
|
libraries=PLATFORMIO_GOOGLE_TEST_LIB,
|
|
build_flags=BUILD_FLAGS + sanitizer_flags(),
|
|
main_entry="main.cpp",
|
|
label="unit tests",
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run C++ unit tests for ESPHome components."
|
|
)
|
|
parser.add_argument(
|
|
"components",
|
|
nargs="*",
|
|
help="List of components to test. Use --all to test all known components.",
|
|
)
|
|
parser.add_argument("--all", action="store_true", help="Test all known components.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.all:
|
|
components: list[str] = get_all_components()
|
|
else:
|
|
components: list[str] = args.components
|
|
|
|
sys.exit(run_tests(components))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|