diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake index ba6493370876..e59f5447c5a0 100644 --- a/libc/cmake/modules/LLVMLibCTestRules.cmake +++ b/libc/cmake/modules/LLVMLibCTestRules.cmake @@ -344,6 +344,10 @@ function(create_libc_unittest fq_target_name) ) endif() add_dependencies(libc-unit-tests ${fq_target_name}) + # Also add dependency to build-only target for lit + if(TARGET libc-unit-tests-build) + add_dependencies(libc-unit-tests-build ${fq_build_target_name}) + endif() endfunction(create_libc_unittest) function(add_libc_unittest target_name) @@ -883,6 +887,10 @@ function(add_libc_hermetic test_name) # If it is a benchmark, it will already have been added to the # gpu-benchmark target add_dependencies(libc-hermetic-tests ${fq_target_name}) + # Also add dependency to build-only target for lit + if(TARGET libc-hermetic-tests-build) + add_dependencies(libc-hermetic-tests-build ${fq_build_target_name}) + endif() endif() endfunction(add_libc_hermetic) diff --git a/libc/test/CMakeLists.txt b/libc/test/CMakeLists.txt index 011ad6aeb34b..88663367feb0 100644 --- a/libc/test/CMakeLists.txt +++ b/libc/test/CMakeLists.txt @@ -6,6 +6,31 @@ add_dependencies(check-libc libc-unit-tests libc-hermetic-tests) add_custom_target(exhaustive-check-libc) add_custom_target(libc-long-running-tests) +# Build-only targets for lit (don't run tests, just build executables) +add_custom_target(libc-unit-tests-build) +add_custom_target(libc-hermetic-tests-build) + +# Configure the site config file for lit +configure_lit_site_cfg( + ${LIBC_SOURCE_DIR}/test/lit.site.cfg.py.in + ${LIBC_BUILD_DIR}/test/lit.site.cfg.py + MAIN_CONFIG + ${LIBC_SOURCE_DIR}/test/lit.cfg.py + PATHS + "LLVM_SOURCE_DIR" + "LLVM_BINARY_DIR" + "LLVM_TOOLS_DIR" + "LLVM_LIBS_DIR" + "LIBC_SOURCE_DIR" + "LIBC_BUILD_DIR" +) + +add_lit_testsuite(check-libc-lit + "Running libc tests via lit" + ${LIBC_BUILD_DIR}/test + DEPENDS libc-unit-tests-build libc-hermetic-tests-build +) + add_subdirectory(UnitTest) if(LIBC_TARGET_OS_IS_GPU) diff --git a/libc/test/lit.cfg.py b/libc/test/lit.cfg.py new file mode 100644 index 000000000000..9791a24f9a56 --- /dev/null +++ b/libc/test/lit.cfg.py @@ -0,0 +1,17 @@ +# -*- Python -*- +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# All the Lit configuration is handled in the site config -- this file is only +# left as a canary to catch invocations of Lit that do not go through llvm-lit. +# +# Invocations that go through llvm-lit will automatically use the right Lit +# site configuration inside the build directory. + +lit_config.fatal( + "You seem to be running Lit directly -- you should be running Lit through " + "/bin/llvm-lit, which will ensure that the right Lit configuration " + "file is used." +) diff --git a/libc/test/lit.site.cfg.py.in b/libc/test/lit.site.cfg.py.in new file mode 100644 index 000000000000..87a5649e4ba6 --- /dev/null +++ b/libc/test/lit.site.cfg.py.in @@ -0,0 +1,37 @@ +@LIT_SITE_CFG_IN_HEADER@ + +import os +import site + +# Configuration values from CMake +config.llvm_tools_dir = lit_config.substitute(path(r"@LLVM_TOOLS_DIR@")) +config.libc_src_root = path(r"@LIBC_SOURCE_DIR@") +config.libc_obj_root = path(r"@LIBC_BUILD_DIR@") +config.libc_test_cmd = "@LIBC_TEST_CMD@" + +# Add libc's utils directory to the path so we can import the test format. +site.addsitedir(os.path.join(config.libc_src_root, "utils")) +import libctest + +# name: The name of this test suite. +config.name = "libc" + +# testFormat: Use libc's custom test format that discovers pre-built +# test executables (Libc*Tests) in the build directory. +config.test_format = libctest.LibcTest() + +# excludes: A list of directories to exclude from the testsuite. +config.excludes = ["Inputs", "CMakeLists.txt", "README.txt", "LICENSE.txt", "UnitTest"] + +# test_source_root: The root path where tests are located. +# test_exec_root: The root path where test executables are built. +# Set both to the build directory so ExecutableTest finds executables correctly. +config.test_exec_root = os.path.join(config.libc_obj_root, "test") +config.test_source_root = config.test_exec_root + +# Add tool directories to PATH (in case we add FileCheck tests later). +if hasattr(config, "llvm_tools_dir") and config.llvm_tools_dir: + config.environment["PATH"] = os.path.pathsep.join( + [config.llvm_tools_dir, config.environment.get("PATH", "")] + ) + diff --git a/libc/utils/libctest/__init__.py b/libc/utils/libctest/__init__.py new file mode 100644 index 000000000000..9472aa94e7f3 --- /dev/null +++ b/libc/utils/libctest/__init__.py @@ -0,0 +1,21 @@ +# ===----------------------------------------------------------------------===## +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===----------------------------------------------------------------------===## + +""" +Lit test format for LLVM libc unit tests. + +This format extends lit.formats.ExecutableTest to discover pre-built test +executables in the build directory. Test executables are expected to follow +the naming pattern used by add_libc_test(): + libc.test.src...__unit__.__build__ + libc.test.src...__hermetic__.__build__ +""" + +from .format import LibcTest + +__all__ = ["LibcTest"] diff --git a/libc/utils/libctest/format.py b/libc/utils/libctest/format.py new file mode 100644 index 000000000000..df7d40279976 --- /dev/null +++ b/libc/utils/libctest/format.py @@ -0,0 +1,106 @@ +# ===----------------------------------------------------------------------===## +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===----------------------------------------------------------------------===## + +""" +Lit test format for LLVM libc tests. + +This format discovers pre-built test executables in the build directory +and runs them. It extends lit's ExecutableTest format. + +The lit config sets test_source_root == test_exec_root (both to the build +directory), following the pattern used by llvm/test/Unit/lit.cfg.py. + +Test executables are discovered by looking for files matching: + libc.test.src...__unit__.__build__ + libc.test.src...__hermetic__.__build__ + +These are created by the add_libc_test() infrastructure. +""" + +import os +import shlex + +import lit.formats +import lit.Test +import lit.util + + +class LibcTest(lit.formats.ExecutableTest): + """ + Test format for libc unit tests. + + Extends ExecutableTest to discover tests from the build directory + rather than the source directory. Test executables are named like: + libc.test.src.ctype.isalnum_test.__unit__.__build__ + and return 0 on success. + """ + + def getTestsInDirectory(self, testSuite, path_in_suite, litConfig, localConfig): + """ + Discover test executables in the build directory. + + Since test_source_root == test_exec_root (both point to build dir), + we use getSourcePath() to find test executables. + """ + source_path = testSuite.getSourcePath(path_in_suite) + + # Look for test executables in the build directory + if not os.path.isdir(source_path): + return + + # Sort for deterministic test discovery/output ordering. + for filename in sorted(os.listdir(source_path)): + filepath = os.path.join(source_path, filename) + + # Match our test executable pattern + if self._isTestExecutable(filename, filepath): + # Create a test with the executable name + yield lit.Test.Test(testSuite, path_in_suite + (filename,), localConfig) + + def _isTestExecutable(self, filename, filepath): + """Check if a file is a test executable we should run.""" + # Pattern: libc.test.src.*.__unit__.__build__ or .__hermetic__.__build__ + if not filename.startswith("libc.test."): + return False + if not ( + filename.endswith(".__unit__.__build__") + or filename.endswith(".__hermetic__.__build__") + ): + return False + # Must be executable + if not os.path.isfile(filepath): + return False + if not os.access(filepath, os.X_OK): + return False + return True + + def execute(self, test, litConfig): + """ + Execute a test by running the test executable. + + Runs from the executable's directory so relative paths (like + testdata/test.txt) work correctly. + """ + + test_path = test.getSourcePath() + exec_dir = os.path.dirname(test_path) + + test_cmd_template = getattr(test.config, "libc_test_cmd", "") + if test_cmd_template: + test_cmd = test_cmd_template.replace("@BINARY@", test_path) + cmd_args = shlex.split(test_cmd) + if not cmd_args: + cmd_args = [test_path] + out, err, exit_code = lit.util.executeCommand(cmd_args, cwd=exec_dir) + else: + out, err, exit_code = lit.util.executeCommand([test_path], cwd=exec_dir) + + if not exit_code: + return lit.Test.PASS, "" + + return lit.Test.FAIL, out + err