| # ===----------------------------------------------------------------------===## | |
| # | |
| # 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 <executable>.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.<category>.<test_name>.__build__ | |
| libc.test.src.<category>.<test_name>[.<opts>...].__build__ | |
| libc.test.include.<test_name>[.<opts>...].__build__ | |
| libc.test.integration.<category>.<test_name>.__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."): | |
| pass # Accept all include tests ending in .__build__ | |
| 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 <executable>.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 |