Lit and FileCheck

Last Updated on 2023年7月13日

Lit And FileCheck

LitFileCheck 是 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: FOOCHECK: BAR, 那么stdin中就必须先出现"FOO", 再出现"BAR", 则FileCheck才能通过.

FileCheck的Directive也同样一般会写在源文件中. 此时, 这些Directive写在哪里并不重要, 重要的仅仅是先后顺序. 例如, 有的人习惯把所有的CHECK:写在文件开头, 而有的人则习惯把CHECK:写在刚好能打印这一内容的地方, 这只是风格的问题. 下面的python3 file1.py | FileCheck file1.pypython3 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测试.

  1. 标准的FileCheck测试, 检测stdin中按顺序出现了Foo和Bar
  2. 按自定的 FOO_ONLY_CHECK 进行检测, 只检测 stdin 出现了 Foo
  3. 按自定的 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.cfglit.cfg.
  • lit.site.cfglit.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.cfglit.cfg覆盖的.
    • 在从父目录拷贝config时,可以通过lit.local.cfg来更新local_config
    • 常见的用法如, 根目录不设置suffixes,只存储通用的参数, 不同子目录则按自己的需求设置不同的suffixesenvironment

关于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定义的额外数据, 来进一步做一些其他操作
  • lit.llvm附带了一套llvm相关的更新config变量的工具,位于lit.llvm, 这些工具不是必须的, 但是如果项目和LLVM相关, 则可以更便利的设置一些llvm相关的lit配置.
    • Step1,更新config.llvm_lib_dirconfig.llvm_tools_dir到具体值
    • Step2,调用lit.llvm.initialize(lit_config, config)初始化
    • Step3,调用lit.llvm.llvm_config.use_default_substitutions()等来进一步更新config变量
# 展示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)

参考资料