blob: 293159ac6d025b54b9bb9dc19b1fdd67c509a1be [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Opt-in native builds requiring CMake, Ninja, pkg-config, LZ4 and an archive cache."""
import os
import re
import shlex
import shutil
import subprocess
import tempfile
import textwrap
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = ROOT / "cpp/cmake/tests/projects"
class PublicHeaderCompatibilityTest(unittest.TestCase):
def test_simd_int64_reduction_normalizes_lane_types(self):
statistic = (ROOT / "cpp/src/common/statistic.h").read_text()
self.assertIn("std::min<int64_t>(simde_mm_cvtsi128_si64(vmin)", statistic)
self.assertIn("std::max<int64_t>(simde_mm_cvtsi128_si64(vmax)", statistic)
source = textwrap.dedent("""\
#include <algorithm>
#include <cstdint>
long simde_mm_cvtsi128_si64(int);
long long simde_mm_extract_epi64(int, int);
std::int64_t reduce_min(int value) {
return std::min<std::int64_t>(simde_mm_cvtsi128_si64(value),
simde_mm_extract_epi64(value, 1));
}
""")
result = subprocess.run(
[
*shlex.split(os.environ.get("CXX", "c++")),
"-fsyntax-only",
"-x",
"c++",
"-std=c++11",
"-",
],
input=source,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
self.assertEqual(result.returncode, 0, result.stdout)
def test_msvc_old_cplusplus_uses_native_cxx11_features(self):
source = textwrap.dedent("""\
#include "utils/util_define.h"
class Probe { STATIC_ASSERT(true, MSVC_old_cplusplus); };
class Base { virtual void reset(); };
class Derived : public Base { void reset() OVERRIDE; };
""")
result = subprocess.run(
[
*shlex.split(os.environ.get("CXX", "c++")),
"-E",
"-P",
"-x",
"c++",
"-std=c++11",
"-D_MSC_VER=1930",
"-DTSFILE_STATIC",
"-D__cplusplus=199711L",
f"-I{ROOT / 'cpp/src'}",
"-",
],
input=source,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
self.assertEqual(result.returncode, 0, result.stdout[-4000:])
self.assertRegex(
result.stdout, r"class Probe \{\s*(?:static_assert|_Static_assert)"
)
self.assertNotIn("static_assertion_MSVC_old_cplusplus", result.stdout)
self.assertIn("void reset() override", result.stdout)
def test_msvc_old_cplusplus_does_not_define_nullptr(self):
compatibility = (ROOT / "cpp/src/utils/util_define.h").read_text()
section = re.search(
r"/\* ======== nullptr ======== \*/\n(.*?)\n/\* ======== cache line",
compatibility,
re.S,
)
self.assertIsNotNone(section)
source = section.group(1) + textwrap.dedent("""\
#ifdef nullptr
#error nullptr must remain a native MSVC keyword
#endif
class Base { virtual void reset(); };
class Derived : public Base { void reset() OVERRIDE; };
int* pointer = nullptr;
""")
result = subprocess.run(
[
*shlex.split(os.environ.get("CXX", "c++")),
"-E",
"-P",
"-nostdinc",
"-x",
"c++",
"-std=c++11",
"-D_MSC_VER=1930",
"-D__cplusplus=199711L",
"-",
],
input=source,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertIn("void reset() override", result.stdout)
self.assertRegex(result.stdout, r"int\s*\*\s*pointer\s*=\s*nullptr")
class StaticANTLRPackageTest(unittest.TestCase):
"""Exercise installed metadata without compiling the ANTLR runtime."""
def test_installed_system_antlr_requires_compatible_runtime(self):
with tempfile.TemporaryDirectory(prefix="tsfile-antlr-config-") as temporary:
directory = Path(temporary)
producer = directory / "producer"
(producer / "cmake").mkdir(parents=True)
shutil.copyfile(
ROOT / "cpp/cmake/TsFileStaticDependencies.cmake.in",
producer / "cmake/TsFileStaticDependencies.cmake.in",
)
(producer / "CMakeLists.txt").write_text(textwrap.dedent("""\
cmake_minimum_required(VERSION 3.11)
project(StaticANTLRPackage NONE)
include(CMakePackageConfigHelpers)
include("${TSFILE_CMAKE_DIR}/DependencySource.cmake")
include("${TSFILE_CMAKE_DIR}/ANTLR4Dependency.cmake")
set(CMAKE_INSTALL_LIBDIR lib)
set(ENABLE_ANTLR4 ON)
set(ENABLE_THREADS OFF)
set(TSFILE_BUILD_SHARED OFF)
add_library(tsfile INTERFACE)
add_library(TsFile::ANTLR4 INTERFACE IMPORTED)
include("${TSFILE_CMAKE_DIR}/TsFileStaticDependencies.cmake")
configure_package_config_file(
"${TSFILE_CMAKE_DIR}/TsFileConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
INSTALL_DESTINATION lib/cmake/TsFile)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/TsFileConfig.cmake"
DESTINATION lib/cmake/TsFile)
install(TARGETS tsfile EXPORT TsFileTargets)
install(EXPORT TsFileTargets NAMESPACE TsFile::
DESTINATION lib/cmake/TsFile)
"""))
consumer = directory / "consumer"
consumer.mkdir()
(consumer / "CMakeLists.txt").write_text(textwrap.dedent("""\
cmake_minimum_required(VERSION 3.11)
project(StaticANTLRConsumer NONE)
find_package(TsFile CONFIG REQUIRED)
get_target_property(runtime TsFile::Static_ANTLR4
INTERFACE_LINK_LIBRARIES)
if (NOT runtime STREQUAL EXPECTED_TARGET OR NOT TARGET ${runtime})
message(FATAL_ERROR "Static TsFile lost its system target")
endif ()
if (NOT TARGET utf8cpp)
message(FATAL_ERROR "Static TsFile lost utf8cpp compatibility")
endif ()
"""))
def configure(source, build, *options):
return subprocess.run(
[
"cmake",
"-S",
str(source),
"-B",
str(build),
"-DCMAKE_FIND_USE_PACKAGE_REGISTRY=FALSE",
"-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=FALSE",
"-DCMAKE_FIND_USE_CMAKE_SYSTEM_PATH=FALSE",
"-DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=FALSE",
f"-DCMAKE_MAKE_PROGRAM={shutil.which('make')}",
"-G",
"Unix Makefiles",
*options,
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
def package(prefix, version, target, version_field):
prefix.mkdir(parents=True)
(prefix / "utf8cppConfig.cmake").write_text(
"add_library(utf8cpp INTERFACE IMPORTED)\n"
)
if version == "missing-package":
return
content = ""
if version is not None:
content += f'set({version_field} "{version}")\n'
if target is not None:
content += (
f"add_library({target} INTERFACE IMPORTED)\n"
f"set_property(TARGET {target} PROPERTY "
"INTERFACE_LINK_LIBRARIES utf8cpp)\n"
)
(prefix / "antlr4-runtime-config.cmake").write_text(content)
targets = (
"antlr4_static",
"antlr4-runtime::antlr4_static",
"antlr4_shared",
"antlr4-runtime::antlr4_shared",
)
for index, target in enumerate(targets):
build = directory / f"producer-build-{index}"
stage = directory / f"stage-{index}"
build_package = directory / f"build-package-{index}"
package(build_package, "4.9.3", target, "ANTLR_VERSION")
result = configure(
producer,
build,
f"-DTSFILE_CMAKE_DIR={ROOT / 'cpp/cmake'}",
"-DTSFILE_DEPENDENCY_SOURCE=SYSTEM",
f"-DCMAKE_PREFIX_PATH={build_package}",
f"-DCMAKE_INSTALL_PREFIX={stage}",
)
self.assertEqual(result.returncode, 0, result.stdout)
result = subprocess.run(
["cmake", "--install", str(build)],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
self.assertEqual(result.returncode, 0, result.stdout)
relocated = directory / f"relocated-{index}"
stage.rename(relocated)
cases = (
("4.9.3", target, True),
("4.12.0", target, True),
("4.13.0", target, False),
("4.13.2", target, False),
("4.9.2", target, False),
(None, target, False),
("4.9.3", None, False),
("missing-package", None, False),
)
for field in ("antlr4-runtime_VERSION", "ANTLR_VERSION"):
for case, (version, consumer_target, succeeds) in enumerate(cases):
with self.subTest(
target=target,
field=field,
version=version,
consumer_target=consumer_target,
):
suffix = f"{index}-{field}-{case}"
runtime = directory / f"runtime-{suffix}"
package(runtime, version, consumer_target, field)
result = configure(
consumer,
directory / f"consumer-{suffix}",
f"-DCMAKE_PREFIX_PATH={relocated};{runtime}",
f"-DEXPECTED_TARGET={target}",
)
if succeeds:
self.assertEqual(result.returncode, 0, result.stdout)
else:
self.assertNotEqual(
result.returncode,
0,
f"Installed static TsFile accepted {version}:\n"
+ result.stdout,
)
diagnostic = " ".join(result.stdout.split())
self.assertIn("compatible system ANTLR4", diagnostic)
self.assertIn(">=4.9.3", diagnostic)
self.assertIn("<4.13.0", diagnostic)
@unittest.skipUnless(
os.environ.get("TSFILE_RUN_INSTALL_TESTS") == "1", "opt-in native builds"
)
class NativeInstallTest(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="tsfile-install-")
self.addCleanup(self.temporary.cleanup)
self.directory = Path(self.temporary.name)
self.build = self.directory / "build"
self.stage = self.directory / "stage"
def run_command(self, *arguments, env=None):
result = subprocess.run(
[str(argument) for argument in arguments],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
self.assertEqual(result.returncode, 0, result.stdout[-14000:])
return result.stdout.strip()
def configure(self, *options):
arguments = [
"cmake",
"-S",
ROOT / "cpp",
"-B",
self.build,
"-DCMAKE_BUILD_TYPE=Release",
"-DBUILD_TEST=OFF",
"-DBUILD_TOOLS=OFF",
"-DTSFILE_DEPENDENCY_SOURCE=SYSTEM",
"-DTSFILE_ENABLE_NATIVE_ARCH=OFF",
f"-DCMAKE_INSTALL_PREFIX={self.stage}",
]
arguments.extend(
f"-DENABLE_{name}=OFF"
for name in (
"ANTLR4",
"SNAPPY",
"LZ4",
"LZOKAY",
"ZLIB",
"ZSTD",
"LZMA2",
"SIMD",
)
)
self.run_command(*arguments, *options)
def install(self):
self.run_command(
"cmake", "--build", self.build, "--config", "Release", "--parallel", "8"
)
self.run_command("cmake", "--install", self.build, "--config", "Release")
relocated = self.directory / "relocated"
self.stage.rename(relocated)
self.stage = relocated
for path in self.stage.rglob("*.cmake"):
self.assertNotIn(str(self.build), path.read_text(), str(path))
self.assertNotIn(str(ROOT / "cpp"), path.read_text(), str(path))
def consumer(self, fixture, target, env=None, *options):
build = self.directory / fixture
self.run_command(
"cmake",
"-S",
FIXTURES / fixture,
"-B",
build,
f"-DCMAKE_PREFIX_PATH={self.stage}",
*options,
env=env,
)
self.run_command("cmake", "--build", build, "--parallel", "8", env=env)
self.run_command(build / target, env=env)
def test_static_system_lz4_install_and_relocated_consumer(self):
self.configure("-DTSFILE_BUILD_SHARED=OFF", "-DENABLE_LZ4=ON")
self.install()
self.consumer("InstalledConsumer", "tsfile_installed_consumer")
self.pkgconfig_consumers(static=True)
def test_static_bundled_install_and_relocated_consumer(self):
self.configure(
"-G",
"Ninja Multi-Config",
"-DTSFILE_BUILD_SHARED=OFF",
"-DTSFILE_DEPENDENCY_SOURCE=BUNDLED",
"-DENABLE_ANTLR4=ON",
"-DENABLE_SNAPPY=ON",
"-DENABLE_LZ4=ON",
"-DENABLE_ZLIB=ON",
"-DENABLE_LZOKAY=ON",
"-DENABLE_ZSTD=ON",
"-DENABLE_LZMA2=ON",
"-DTSFILE_DEPENDENCY_OFFLINE=ON",
f"-DTSFILE_DEPENDENCY_CACHE={os.environ['TSFILE_TEST_DEPENDENCY_CACHE']}",
)
self.install()
self.consumer("InstalledConsumer", "tsfile_installed_consumer")
self.pkgconfig_consumers(static=True)
def pkgconfig_consumers(self, libdir="lib", static=False):
env = dict(
os.environ,
PKG_CONFIG_PATH=str(self.stage / libdir / "pkgconfig"),
)
prefix = self.run_command(
"pkg-config", "--variable=prefix", "libtsfile", env=env
)
self.assertEqual(Path(prefix).resolve(), self.stage.resolve())
self.consumer(
"PkgConfigConsumer",
"tsfile_pkgconfig_consumer",
env,
f"-DTSFILE_PKGCONFIG_STATIC={'ON' if static else 'OFF'}",
)
self.run_command(
self.directory / "PkgConfigConsumer/tsfile_pkgconfig_cpp_consumer", env=env
)
def test_multiarch_pkgconfig_install_and_relocated_consumers(self):
self.configure("-DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu")
self.install()
self.pkgconfig_consumers("lib/x86_64-linux-gnu")
def cli_rpath(self, libdir):
self.configure("-DBUILD_TOOLS=ON", f"-DCMAKE_INSTALL_LIBDIR={libdir}")
env = dict(os.environ)
for name in (
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
"DYLD_FALLBACK_LIBRARY_PATH",
):
env.pop(name, None)
self.run_command("cmake", "--build", self.build, "--parallel", "8")
self.run_command(self.build / "bin/tsfile-cli", "--version", env=env)
self.install()
# A build-directory RPATH must not mask a broken installed RPATH.
shutil.rmtree(self.build)
self.run_command(self.stage / "bin/tsfile-cli", "--version", env=env)
def test_lib64_cli_build_and_relocated_install(self):
self.cli_rpath("lib64")
def test_multiarch_cli_build_and_relocated_install(self):
self.cli_rpath("lib/x86_64-linux-gnu")
def test_installed_smoke_requires_a_library_symbol(self):
self.configure()
self.install()
result = subprocess.run(
[
*shlex.split(os.environ.get("CXX", "c++")),
"-std=c++11",
f"-I{self.stage / 'include'}",
f"-I{self.stage / 'include/tsfile'}",
str(FIXTURES / "InstalledConsumer/main.cc"),
"-o",
str(self.directory / "unlinked"),
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
self.assertNotEqual(
result.returncode, 0, "Smoke consumer linked successfully without libtsfile"
)
self.assertIn("get_global_time_encoding", result.stdout)
self.consumer("InstalledConsumer", "tsfile_installed_consumer")
for formula in ("tsfile.rb.in",):
source = re.search(
r"write <<~CPP\n(.*?)\n\s+CPP",
(ROOT / "packaging/homebrew" / formula).read_text(),
re.S,
).group(1)
source_path = self.directory / f"{formula}.cc"
source_path.write_text(textwrap.dedent(source))
command = [
*shlex.split(os.environ.get("CXX", "c++")),
"-std=c++11",
f"-I{self.stage / 'include'}",
str(source_path),
"-o",
str(self.directory / "brew-consumer"),
]
unlinked = subprocess.run(
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
self.assertNotEqual(unlinked.returncode, 0, formula)
self.assertIn("get_global_time_encoding", unlinked.stdout)
self.run_command(
*command,
f"-L{self.stage / 'lib'}",
f"-Wl,-rpath,{self.stage / 'lib'}",
"-ltsfile",
)
self.run_command(self.directory / "brew-consumer")
def test_static_msvc_runtime_reaches_dependency_and_project_targets(self):
self.configure(
"-DTSFILE_MSVC_STATIC_RUNTIME=ON",
"-DTSFILE_DEPENDENCY_SOURCE=BUNDLED",
"-DENABLE_ZLIB=ON",
"-DTSFILE_DEPENDENCY_OFFLINE=ON",
f"-DTSFILE_DEPENDENCY_CACHE={os.environ['TSFILE_TEST_DEPENDENCY_CACHE']}",
f"-DCMAKE_PROJECT_TsFile_CPP_INCLUDE={ROOT / 'cpp/cmake/tests/CheckStaticMSVCRuntime.cmake'}",
)
if __name__ == "__main__":
unittest.main()