240 lines
8.6 KiB
Python
240 lines
8.6 KiB
Python
import os
|
|
import json
|
|
import sys
|
|
import subprocess
|
|
import pytest
|
|
import shutil
|
|
|
|
|
|
class CMake:
|
|
def __init__(self, factory):
|
|
self.runs = dict()
|
|
self.factory = factory
|
|
|
|
def compile(self, targets, options=None):
|
|
if options is None:
|
|
options = dict()
|
|
key = (
|
|
";".join(targets),
|
|
";".join(f"{k}={v}" for k, v in options.items()),
|
|
)
|
|
|
|
if key not in self.runs:
|
|
cwd = self.factory.mktemp("cmake")
|
|
self.runs[key] = cwd
|
|
cmake(cwd, targets, options)
|
|
|
|
return self.runs[key]
|
|
|
|
def destroy(self):
|
|
sourcedir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
coveragedir = os.path.join(sourcedir, "coverage")
|
|
shutil.rmtree(coveragedir, ignore_errors=True)
|
|
os.mkdir(coveragedir)
|
|
|
|
if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""):
|
|
for i, d in enumerate(self.runs.values()):
|
|
# first merge the raw profiling runs
|
|
files = [f for f in os.listdir(d) if f.endswith(".profraw")]
|
|
if len(files) == 0:
|
|
continue
|
|
cmd = [
|
|
"llvm-profdata",
|
|
"merge",
|
|
"-sparse",
|
|
"-o=sentry.profdata",
|
|
*files,
|
|
]
|
|
print("{} > {}".format(d, " ".join(cmd)))
|
|
subprocess.run(cmd, cwd=d)
|
|
|
|
# then export lcov from the profiling data, since this needs access
|
|
# to the object files, we need to do it per-test
|
|
objects = [
|
|
"sentry_example",
|
|
"sentry_test_unit",
|
|
"libsentry.dylib" if sys.platform == "darwin" else "libsentry.so",
|
|
]
|
|
cmd = [
|
|
"llvm-cov",
|
|
"export",
|
|
"-format=lcov",
|
|
"-instr-profile=sentry.profdata",
|
|
"--ignore-filename-regex=(external|vendor|tests|examples)",
|
|
*[f"-object={o}" for o in objects if d.joinpath(o).exists()],
|
|
]
|
|
lcov = os.path.join(coveragedir, f"run-{i}.lcov")
|
|
with open(lcov, "w") as lcov_file:
|
|
print("{} > {} > {}".format(d, " ".join(cmd), lcov))
|
|
subprocess.run(cmd, stdout=lcov_file, cwd=d)
|
|
|
|
if "kcov" in os.environ.get("RUN_ANALYZER", ""):
|
|
coverage_dirs = [
|
|
d
|
|
for d in [d.joinpath("coverage") for d in self.runs.values()]
|
|
if d.exists()
|
|
]
|
|
if len(coverage_dirs) > 0:
|
|
subprocess.run(
|
|
[
|
|
"kcov",
|
|
"--clean",
|
|
"--merge",
|
|
coveragedir,
|
|
*coverage_dirs,
|
|
]
|
|
)
|
|
|
|
|
|
def cmake(cwd, targets, options=None):
|
|
__tracebackhide__ = True
|
|
if options is None:
|
|
options = {}
|
|
options.update(
|
|
{
|
|
"CMAKE_RUNTIME_OUTPUT_DIRECTORY": cwd,
|
|
"CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG": cwd,
|
|
"CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE": cwd,
|
|
}
|
|
)
|
|
if os.environ.get("ANDROID_API") and os.environ.get("ANDROID_NDK"):
|
|
# See: https://developer.android.com/ndk/guides/cmake
|
|
toolchain = "{}/ndk/{}/build/cmake/android.toolchain.cmake".format(
|
|
os.environ["ANDROID_HOME"], os.environ["ANDROID_NDK"]
|
|
)
|
|
options.update(
|
|
{
|
|
"CMAKE_TOOLCHAIN_FILE": toolchain,
|
|
"ANDROID_ABI": os.environ.get("ANDROID_ARCH") or "x86",
|
|
"ANDROID_NATIVE_API_LEVEL": os.environ["ANDROID_API"],
|
|
}
|
|
)
|
|
|
|
source_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
|
|
cmake = ["cmake"]
|
|
if "scan-build" in os.environ.get("RUN_ANALYZER", ""):
|
|
cc = os.environ.get("CC")
|
|
cxx = os.environ.get("CXX")
|
|
cmake = [
|
|
"scan-build",
|
|
*(["--use-cc", cc] if cc else []),
|
|
*(["--use-c++", cxx] if cxx else []),
|
|
"--status-bugs",
|
|
"--exclude",
|
|
os.path.join(source_dir, "external"),
|
|
"cmake",
|
|
]
|
|
|
|
configcmd = cmake.copy()
|
|
for key, value in options.items():
|
|
configcmd.append("-D{}={}".format(key, value))
|
|
if sys.platform == "win32" and os.environ.get("TEST_X86"):
|
|
configcmd.append("-AWin32")
|
|
elif sys.platform == "linux" and os.environ.get("TEST_X86"):
|
|
configcmd.append("-DSENTRY_BUILD_FORCE32=ON")
|
|
if "asan" in os.environ.get("RUN_ANALYZER", ""):
|
|
configcmd.append("-DWITH_ASAN_OPTION=ON")
|
|
if "tsan" in os.environ.get("RUN_ANALYZER", ""):
|
|
configcmd.append("-DWITH_TSAN_OPTION=ON")
|
|
|
|
# we have to set `-Werror` for this cmake invocation only, otherwise
|
|
# completely unrelated things will break
|
|
cflags = []
|
|
if os.environ.get("ERROR_ON_WARNINGS"):
|
|
cflags.append("-Werror")
|
|
if sys.platform == "win32" and not os.environ.get("TEST_MINGW"):
|
|
# MP = object level parallelism, WX = warnings as errors
|
|
cpus = os.cpu_count()
|
|
cflags.append("/WX /MP{}".format(cpus))
|
|
if "gcc" in os.environ.get("RUN_ANALYZER", ""):
|
|
cflags.append("-fanalyzer")
|
|
if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""):
|
|
flags = "-fprofile-instr-generate -fcoverage-mapping"
|
|
configcmd.append("-DCMAKE_C_FLAGS='{}'".format(flags))
|
|
configcmd.append("-DCMAKE_CXX_FLAGS='{}'".format(flags))
|
|
if "CMAKE_DEFINES" in os.environ:
|
|
configcmd.extend(os.environ.get("CMAKE_DEFINES").split())
|
|
env = dict(os.environ)
|
|
env["CFLAGS"] = env["CXXFLAGS"] = " ".join(cflags)
|
|
|
|
configcmd.append(source_dir)
|
|
|
|
print("\n{} > {}".format(cwd, " ".join(configcmd)), flush=True)
|
|
try:
|
|
subprocess.run(configcmd, cwd=cwd, env=env, check=True)
|
|
except subprocess.CalledProcessError:
|
|
raise pytest.fail.Exception("cmake configure failed") from None
|
|
|
|
# CodeChecker invocations and options are documented here:
|
|
# https://github.com/Ericsson/codechecker/blob/master/docs/analyzer/user_guide.md
|
|
|
|
buildcmd = [*cmake, "--build", "."]
|
|
for target in targets:
|
|
buildcmd.extend(["--target", target])
|
|
buildcmd.append("--parallel")
|
|
if "code-checker" in os.environ.get("RUN_ANALYZER", ""):
|
|
buildcmd = [
|
|
"codechecker",
|
|
"log",
|
|
"--output",
|
|
"compilation.json",
|
|
"--build",
|
|
" ".join(buildcmd),
|
|
]
|
|
|
|
print("{} > {}".format(cwd, " ".join(buildcmd)), flush=True)
|
|
try:
|
|
subprocess.run(buildcmd, cwd=cwd, check=True)
|
|
except subprocess.CalledProcessError:
|
|
raise pytest.fail.Exception("cmake build failed") from None
|
|
|
|
if "code-checker" in os.environ.get("RUN_ANALYZER", ""):
|
|
# For whatever reason, the compilation summary contains duplicate entries,
|
|
# one with the correct absolute path, and the other one just with the basename,
|
|
# which would fail.
|
|
with open(os.path.join(cwd, "compilation.json")) as f:
|
|
compilation = json.load(f)
|
|
compilation = list(filter(lambda c: c["file"].startswith("/"), compilation))
|
|
with open(os.path.join(cwd, "compilation.json"), "w") as f:
|
|
json.dump(compilation, f)
|
|
|
|
disable = [
|
|
"readability-magic-numbers",
|
|
"cppcoreguidelines-avoid-magic-numbers",
|
|
"readability-else-after-return",
|
|
]
|
|
disables = ["--disable={}".format(d) for d in disable]
|
|
checkcmd = [
|
|
"codechecker",
|
|
"check",
|
|
"--jobs",
|
|
str(os.cpu_count()),
|
|
# NOTE: The clang version on CI does not support CTU :-(
|
|
# Also, when testing locally, CTU spews a ton of (possibly) false positives
|
|
# "--ctu-all",
|
|
# TODO: we currently get >300 reports with `enable-all`
|
|
# "--enable-all",
|
|
*disables,
|
|
"--print-steps",
|
|
"--ignore",
|
|
os.path.join(source_dir, ".codechecker-ignore"),
|
|
"--logfile",
|
|
"compilation.json",
|
|
]
|
|
print("{} > {}".format(cwd, " ".join(checkcmd)), flush=True)
|
|
child = subprocess.run(checkcmd, cwd=cwd, check=True)
|
|
|
|
if os.environ.get("ANDROID_API"):
|
|
# copy the output to the android image via adb
|
|
subprocess.run(
|
|
[
|
|
"{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]),
|
|
"push",
|
|
"./",
|
|
"/data/local/tmp",
|
|
],
|
|
cwd=cwd,
|
|
check=True,
|
|
)
|