关注了就能看到更多这么棒的文章哦~

Driver regression testing with roadtest

By Jonathan Corbet
March 18, 2022
DeepL assisted translation
https://lwn.net/Articles/887974/

内核社区对于此项目中偏少的回归测试覆盖率(regression-test coverage)的时候总能提出若干理由,其中一些理由格外有说服力。其中一个就是大量的内核代码是针对硬件的,没有人能够做到把某个测试系统来针对内核的所有硬件进行测试,哪怕一小部分硬件也做不到。由 Vincent Whitchurch 发布的一个名为 roadtest 的最新驱动测试框架可能会让这个理由今后不再成立了,至少对于某些类型的硬件来说是这样的。

硬件的问题是种类太多。以一个概念上非常简单的设备为例,如 GPIO 端口,这个硬件功能是用来驱动一根引脚来让它变 0 或者变 1 (也就是逻辑上的 true 或 false)。GPIO 驱动通常都是很简单的功能了,但 vendor 总喜欢在每个新版本中加入他们自己的一些特殊功能。结果,在内核源代码中有超过 150 个 GPIO 驱动,其中许多都可以驱动一个设备的多个变种。没有办法建立一个系统来包含所有这些设备;大多数的 GPIO 驱动都是一个更大的外设或 SoC 系统的一部分,而且其中许多设备已经多年没有商业产品了。

当然,这些驱动程序中的每一个都曾经在它所驱动的硬件上进行过测试。人们通常期望这些设备仍然能继续正常工作。但是,内核在不断有变动,而这些变动往往也会影响到驱动。做出这些改动的开发者虽然会尽力避免破坏任何功能,但是他们没有办法测试那些会影响许多驱动的改动;即使是子系统维护者,通常也只有所支持的所有设备的一个子集在手边用来测试。因此,总是有可能会出现 regression 而不被注意到,直到有人的设备停止工作才被发现。

Roadtest 就是希望能规避这个问题,它消除了测试驱动程序是否仍在工作时对实际存在的硬件的需求。这是通过将驱动测试与可以在别的地方运行的模拟设备(mock device)配合来实现的;当开发者为一个特定的驱动做了一套 regression test 的时候,这项工作也会包括目标设备的模拟(mock)。然后,测试就会在一个专门建立的 User-Mode Linux 内核下运行,这些模拟的硬件(mocked hardware)就代替了真实的硬件。

这里需要强迫 test 的作者也要去实现被测设备的模拟版,听起来是一个很高的门槛,很难克服。但是好消息是,这个 mocked device 不需要封装支持真实硬件的全部复杂功能;它们只需要能够做出回应就可以验证驱动程序的行为是否符合预期。模拟这个设备的编程接口(而不是实际去做真实设备会实现的功能)可能就足够了。

以下面这个来自 patch set 的 test 为例,它验证了 opt3001 light-sensor 的驱动程序。测试和模拟的设备都是用 Python 写的,所模拟的 opt3001 device 的核心部分代码是这样的:

class OPT3001(SMBusModel):
    def __init__(self, **kwargs: Any) -> None:
        super().__init__(regbytes=2, byteorder="big", **kwargs)
        # Reset values from datasheet
        self.regs = {
            REG_RESULT: 0x0000,
            REG_CONFIGURATION: 0xC810,
            REG_LOW_LIMIT: 0xC000,
            REG_HIGH_LIMIT: 0xBFFF,
            REG_MANUFACTURER_ID: 0x5449,
            REG_DEVICE_ID: 0x3001,
        }

    def reg_read(self, addr: int) -> int:
        val = self.regs[addr]

        if addr == REG_CONFIGURATION:
            # Always indicate that the conversion is ready.  This is good
            # enough for our current purposes.
            val |= REG_CONFIGURATION_CRF

        return val

    def reg_write(self, addr: int, val: int) -> None:
        assert addr in self.regs
        self.regs[addr] = val

opt3001 是一个 SMBus 设备,通过对一组寄存器进行写入(和读取)来进行编程。使用 roadtest 提供的 SMBus emulation 功能,这个模拟设备只需要实现少数几个寄存器。很难想象会有更简单的实现方式了。read 的实现甚至懒得检查所请求的寄存器地址是否有效,大概是基于这样的假设:一个错误的读取请求所触发的 crash 就应该足够表明 "测试失败" 了。

roadtest 框架将实现模拟设备(mocked device),并将其跟驱动程序关联起来(是现在 User-mode Linux 实例中),就像它是一个真实的设备一样。测试本身就作为该实例中的一个用户空间进程来运行;它会调整其中的一些寄存器从而模拟有数据过来了,然后使用 IIO API 读取这些数据:

def test_illuminance(self) -> None:
    data = [
        # Some values from datasheet, and 0
        (0b_0000_0000_0000_0000, 0),
        (0b_0000_0000_0000_0001, 0.01),
        (0b_0011_0100_0101_0110, 88.80),
        (0b_0111_1000_1001_1010, 2818.56),
    ]
    with self.driver.bind(self.dts["addr"]) as dev:
        luxfile = dev.path / "iio:device0/in_illuminance_input"

        for regval, lux in data:
            self.hw.reg_write(REG_RESULT, regval)
            self.assertEqual(read_float(luxfile), lux)

对寄存器进行的写入(上面的 self.hw.reg_write()调用就是了)会直接进入模拟设备。对它的读取内容则会交给驱动程序,它将会与模拟设备交互来获得所需的数据。如果驱动程序工作正常,它就会从模拟设备上读取到模拟数据(simulated data)并返回所期望的测试结果。

这是一个很简单的测试。可以有更复杂的测试来验证驱动程序是否正确配置了硬件、是否处理了错误情况等等。即便如此,这样的机制似乎也还是有局限性,很难用它来验证,比如说,很难检查 Video4Linux 驱动程序是否正确地管理了放置那些 user-mapped buffer 的 buffer queue。但是对于更简单的设备(非常多),像 roadtest 这样的系统就可以提供内核开发者目前无法实现的一些验证了。

关于 roadtest 的更多信息可以在相关的文档 patch 中找到,其中包括一个为新设备添加 test 的教程。patch set 包含了一些设备类型的测试,如果这个框架被合入 mainline,这个 list 可能会大大增加。

到目前为止,对该系统的评论还不是很多,所以很难确定 roadtest 是否真的能合入 mainline。不过 Brendan Higgins 对 roadtest 的看法是非常清晰的:"我喜欢这个框架;看起来非常容易使用"。像 roadtest 这样的测试框架应该不会让那些不需要使用的人碰到什么干扰,而且,如果测试足够全面,就可以在 regression 进入发布版本内核之前大大增加找出这种问题的机会。因此,很难找到什么反对 roadtest 进入 mainline kernel 的理由——当然,除非内核开发者真的不想失去一个解释为什么很少进行驱动的回归测试的很好理由。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

326509e587b9a476370e03e1acb17947.png

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐