Last Updated on 2023年7月13日
Lit And FileCheck
Lit
和 FileCheck
是 LLVM 测试中常用的工具, 尽管二者功能上是完全独立的,但是搭配起来使用会显得更加方便.
Lit
Lit
总的来说仅仅是一个 test-launcher, 它的主要功能就是发现测试,执行测试,收集结果.
"发现测试"主要依赖于lit.cfg
(或lit.site.cfg
)文件标记来实现. 当一个目录下包含这个文件时, 那么这个目录就可以被用作lit的测试根目录, lit会自动的递归进入子目录查找测试文件.
"执行测试"则相对简单, 每个"lit测试文件"都应该是文本文件,这些文本文件中 RUN:
将被视作标记,这个标记之后的内容就是执行测试的shell指令, 测试指令返回0则认为测试通过, 例如 RUN: echo jojo | grep jojo
.
具体来说,RUN:
对应的测试指令在执行之前还会被lit执行一次 substitute, 以更新待执行的指令, 例如%s
被替换为所在文件的路径.
从习惯上说, RUN:
一般被写在源文件中, 所以常常会被comment起来, 避免和源码内容冲突, 下面的python例子描述了典型的RUN:
指示用法.
# file.py
# RUN: python3 %s | grep Foo
print("Foo")
假如上面这个文件位于/foo/bar/a.py
, 且/foo/lit.cfg
已配置好,那么执行lit /foo
的过程中,就会自动发现这个测试,并且会在执行阶段执行python3 /foo/bar/a.py | grep Foo
, 如果这个指令返回0,则认为测试通过.
FileCheck
FileCheck
的功能像是增强的grep
, 从功能上说, 它的作用是检查stdin内的文本是否符合用户设定的Pattern
FileCheck
的Match Pattern是用文本文件来描述的, 基于诸如 CHECK:
, CHECK-NOT:
这样的Directive实现. Pattern可以相当复杂, 但是总的来说是按照先后顺序进行描述的. 例如,假如文件中有两行分别包含了CHECK: FOO
和CHECK: BAR
, 那么stdin中就必须先出现"FOO", 再出现"BAR", 则FileCheck才能通过.
FileCheck
的Directive也同样一般会写在源文件中. 此时, 这些Directive写在哪里并不重要, 重要的仅仅是先后顺序. 例如, 有的人习惯把所有的CHECK:
写在文件开头, 而有的人则习惯把CHECK:
写在刚好能打印这一内容的地方, 这只是风格的问题. 下面的python3 file1.py | FileCheck file1.py
和python3 file2.py | FileCheck file2.py
并没有本质区别
# file1.py
# CHECK: Foo
print("Foo")
# CHECK: Bar
print("Bar")
# file2.py
#### FileCheck Directives ####
# CHECK: Foo
# CHECK: Bar
#### FileCheck Directives ####
print("Foo")
print("Bar")
Combination
综上, Lit 和 FileCheck 搭配起来的简单例子如下, 该文件包含了三个lit测试.
- 标准的FileCheck测试, 检测stdin中按顺序出现了Foo和Bar
- 按自定的 FOO_ONLY_CHECK 进行检测, 只检测 stdin 出现了 Foo
- 按自定的 BAR_ONLY_CHECK 进行检测, 只检测stdin出现了 Bar
# sample.py
# RUN: python3 %s | FileCheck %s
# RUN: python3 %s | FileCheck %s --check-prefix=FOO_ONLY_CHECK
# RUN: python3 %s | FileCheck %s --check-prefix=BAR_ONLY_CHECK
# CHECK: Foo
# FOO_ONLY_CHECK: Foo
print("Foo")
# CHECK: Bar
# BAR_ONLY_CHECK: Bar
print("Bar")
A little more about LIT
Here is a minimal sample
总的来说, Lit的配置文件规则如下:
- 测试的根目录必须包含一个
lit.site.cfg
或lit.cfg
. lit.site.cfg
或lit.cfg
本质没有区别, 只是lit.site.cfg
会被优先加载.- 习惯上,
lit.site.cfg
主要用于定义一些编译相关的信息, 是通过CMAKE的configure_file
来动态生成的, 此时lit.site.cfg
通常是作为一个跳板, 往往会通过lit_config.load_config
来加载另一个lit.cfg
- 习惯上,
- 每个目录都会有一个逻辑上的
local_config
,这个local_config
要么是从父目录拷贝的, 要么是用新的lit.site.cfg
或lit.cfg
覆盖的.- 在从父目录拷贝config时,可以通过
lit.local.cfg
来更新local_config
- 常见的用法如, 根目录不设置
suffixes
,只存储通用的参数, 不同子目录则按自己的需求设置不同的suffixes
及environment
- 在从父目录拷贝config时,可以通过
关于config文件:
- config文件实际是一个回调的python文件, lit会预定义
config
,lit_config
等"builtin"变量config
的就是逻辑上的local_config
, 它内部已经存储了一些值, 我们直接修改它即可
config
这个变量有一些关键数据成员会被lit回调使用,比较重要的有- name:str
- test_format, 通常就是
lit.formats.ShTest(True)
, 它表示每个被发现到的测试文件都是独立的测试文件 - test_source_root, 表示扫描测试文件的起始目录, 通常就是
os.path.dirname(__file__)
- suffixes:list[str], 表示test_source_root下哪些文件会被识别为测试文件, 例如
['.py']
, 注意, 目前lit的实现有问题, 后缀名中只允许存在一个dot, 也就是说['.lit.py']
是不允许的 - test_exec_root, 表示测试执行的起始目录, 注意, 测试实际的执行目录(CWD)会额外concat一段
relative_path(test_file_path,test_source_root)
, 并不是直接在test_exec_root
下执行 - environment: 用于为测试设置新的环境变量.
- substitutions:list[str], 用于定义一系列额外的文本替换, 以简化测试文件中的描述.
- 除此之外, config常常还用于存储一些通用的变量, 作为传值的工具.
- 例如, 往往通过CMAKE的
configure_file
动态生成lit.site.cfg
, 通过config.my_src_dir="@CMAKE_SOURCE_DIR@"
这样的语法把CMAKE变量设置到lit.site.cfg
中, 然后通过lit_config.load_config(config,"@PATH_TO_REAL_LIT_DIR@/lit.cfg")
来将这些数据传递给真实的lit.cfg
- 例如, 常常会在
lit.local.cfg
中利用读取父目录config
定义的额外数据, 来进一步做一些其他操作
- 例如, 往往通过CMAKE的
lit.llvm
附带了一套llvm相关的更新config
变量的工具,位于lit.llvm
, 这些工具不是必须的, 但是如果项目和LLVM相关, 则可以更便利的设置一些llvm相关的lit配置.- Step1,更新
config.llvm_lib_dir
和config.llvm_tools_dir
到具体值 - Step2,调用
lit.llvm.initialize(lit_config, config)
初始化 - Step3,调用
lit.llvm.llvm_config.use_default_substitutions()
等来进一步更新config
变量
- Step1,更新
# 展示Lit的Config逻辑
def main(user_specified_root: str):
# config_map is a tree like structure organized by file system path
config_map = dict()
individual_config_files = ["lit.site.cfg", "lit.cfg"] # lit.site.cfg has higher priority
local_config_file = ["lit.local.cfg"]
# Make sure the root dir always has a config, or raise error.
root_cfg_file = dir_get_file_with_priority(user_specified_root, individual_config_files)
if not root_cfg_file:
raise Error("No config file found in root dir")
# create and load root config
config_map[user_specified_root] = Config()
root_cfg = config_map[user_specified_root]
update_config(root_cfg, root_cfg_file)
# Traverse the test_src_root
test_src_root = root_cfg.test_source_root
assert is_sub_dir(test_src_root, user_specified_root)
for dir in bfs_walk(test_src_root):
sub_dir_ind_cfg_file = dir_get_file_with_priority(dir, individual_config_files)
if sub_dir_ind_cfg_file:
config_map[dir] = Config()
update_config(config_map[dir], sub_dir_ind_cfg_file)
local_config = config_map[dir]
else:
local_config = copy_from_nearest_parent_dir(dir, config_map)
local_cfg_file = dir_get_file_with_priority(dir, local_config_file)
if local_cfg_file:
update_config(local_config, local_cfg_file)
# only run test in this dir, not sub dir
NoRecursiveFindAndRunTests(dir, local_config)
参考资料
- FileCheck的文档基本谈到了所有的用法.
- LIT的CLI Manual仅仅说了CLI的用法, 在CLI中调用lit时可以用上. LLVM Testing Infrastructure Guide则仅描述了LLVM内的测试方法,几乎没有什么用.
- 目前并没有公开的文档来说明
lit.cfg
的规则, 可以参考的主要是一篇博客using-llvm-lit-out-of-tree, 以及官方的测试用例lit.cfg和lit.site.cfg. - FileCheck 会随着 llvm 安装包分发,但也有第三方的Python实现. Lit则自身就是一个标准的Python项目,可以直接用pip安装