# ===----------------------------------------------------------------------===## # # 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 _isTestExecutable() and run by execute(). Integration tests that require command-line arguments or environment variables have a sidecar .params file generated by CMake. The format is one arg per line, a "---" separator, then one KEY=VALUE env entry per line. """ import os import shlex import sys import lit.formats import lit.Test import lit.util kIsWindows = sys.platform in ["win32", "cygwin"] class LibcTest(lit.formats.ExecutableTest): """ Test format for libc unit tests. Extends ExecutableTest to discover pre-built test executables in the build directory rather than the source directory. """ 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, localConfig): # Create a test with the executable name yield lit.Test.Test(testSuite, path_in_suite + (filename,), localConfig) def _isTestExecutable(self, filename, filepath, localConfig): """ Check if a file is a libc test executable we should run. Recognized patterns (all must end with .__build__, optionally followed by .exe on Windows): libc.test.src...__build__ libc.test.src...__unit__[....].__build__ libc.test.src...__hermetic__[....].__build__ libc.test.include..__unit__[....].__build__ libc.test.include..__hermetic__[....].__build__ libc.test.integration...__build__ """ test_name = filename if kIsWindows and filename.endswith(".exe"): test_name = filename[: -len(".exe")] if not test_name.endswith(".__build__"): return False if test_name.startswith("libc.test.src."): pass # Accept all src tests ending in .__build__ elif test_name.startswith("libc.test.include."): if ".__unit__." not in test_name and ".__hermetic__." not in test_name: return False elif test_name.startswith("libc.test.integration."): pass # Accept all integration tests ending in .__build__ elif test_name.startswith("libc.test.shared."): pass # Accept all shared tests ending in .__build__ elif test_name.startswith("libc.test.utils."): pass # Accept all utils tests ending in .__build__ else: return False if not os.path.isfile(filepath): return False # GPU binaries are not host-executable but run via an emulator, so ignore X_OK if emulator is set. if ( not kIsWindows and not os.access(filepath, os.X_OK) and not getattr(localConfig, "libc_crosscompiling_emulator", None) ): return False return True def _getParamsPath(self, test_path): params_path = test_path + ".params" if os.path.isfile(params_path): return params_path root, ext = os.path.splitext(test_path) if ext.lower() == ".exe": params_path = root + ".params" if os.path.isfile(params_path): return params_path return None 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. If a sidecar .params file exists, it supplies the command-line arguments and environment variables for the test. Honors test.config.maxIndividualTestTime (set via --timeout) to kill tests that exceed the per-test time limit. """ test_path = test.getSourcePath() exec_dir = os.path.dirname(test_path) # Read optional sidecar .params file generated by CMake for tests that # need specific args/env (e.g. integration tests with ARGS/ENV). # Format: one arg per line, "---" separator, then KEY=VALUE env lines. loader_args = [] test_args = [] extra_env = {} params_path = self._getParamsPath(test_path) if params_path: with open(params_path) as f: content = f.read() sections = content.split("---\n") if len(sections) >= 3: loader_args = [l for l in sections[0].splitlines() if l] test_args = [l for l in sections[1].splitlines() if l] env_section = sections[2] else: loader_args = [] test_args = [l for l in sections[0].splitlines() if l] env_section = sections[1] if len(sections) > 1 else "" for line in env_section.splitlines(): if "=" in line: k, _, v = line.partition("=") extra_env[k] = v # Build the environment: inherit the current process environment, then # set PWD to exec_dir so getenv("PWD") matches getcwd(), then overlay # any test-specific variables from the .params file. env = dict(os.environ) env["PWD"] = exec_dir env.update(extra_env) timeout = test.config.maxIndividualTestTime test_cmd_template = getattr(test.config, "libc_test_cmd", "") if test_cmd_template: if "@BINARY@" in test_cmd_template: # Insert loader_args before the binary, and test_args after. prefix, _, suffix = test_cmd_template.partition("@BINARY@") cmd_args = ( shlex.split(prefix) + loader_args + [test_path] + shlex.split(suffix) + test_args ) else: # Fallback to appending the binary path if @BINARY@ placeholder is missing. cmd_args = ( shlex.split(test_cmd_template) + loader_args + [test_path] + test_args ) if not cmd_args: cmd_args = [test_path] else: cmd_args = [test_path] + test_args try: out, err, exit_code = lit.util.executeCommand( cmd_args, cwd=exec_dir, env=env, timeout=timeout ) except lit.util.ExecuteCommandTimeoutException as e: return ( lit.Test.TIMEOUT, f"{e.out}\n--\n" f"Reached timeout of {timeout} seconds", ) if not exit_code: return lit.Test.PASS, "" return lit.Test.FAIL, out + err