如何模拟功能测试中的局部故障



我有一个Linux应用程序,该应用程序使用inotify跟踪文件系统更改。我想为其编写一个功能性测试套件,该套件从最终用户的角度来测试应用程序,作为其中的一部分,我想测试文件系统失败,尤其是我要测试Intify Faility失败的情况。

我想对Inotify file-descriptors进行inotify_init()inotify_add_watch()inotify_rm_watch()read()的呼叫和呼叫,以便在测试中需要返回错误。

,但问题是我找不到如何模拟挖掘失败的方式。我想知道有人是否已经遇到了这样的问题并知道一些解决方案。

如果要避免任何模拟,最好的选择就是直接通过直接引起错误达到操作系统限制。例如,如果调用过程达到了打开文件描述符数量的限制,则inotify_init可以使用EMFILE ERRNO失败。要以100%的精度达到这样的条件,您可以使用两个技巧:

  1. 通过更改procfs中的值动态操纵运行过程的限制
  2. 将您的应用程序流程分配给专用CGroup,并通过CGROUPS API给出〜0%CPU时间来"暂停"它(这是Android油门的背景应用程序的方式并实现其能量节省的"润夹"模式(。
  3. (。

inotifyinotify_initinotify_add_watch的MAN页面中记录了所有可能的错误条件(我认为inotify_rm_watch不会失败,除了您的代码中的纯编程错误(。

除了普通错误(例如越过/proc/sys/fs/inotify/max_user_watches(Inotify具有多种故障模式(队列空间耗尽,观察ID重复使用(,但这些并不是严格的言语。

当某人执行文件系统变化速度比您反应快的速度时,

排队精疲力尽。它很容易复制:使用cGroups在程序上打开插图时暂停您的程序(因此事件队列不会排干(,并通过修改观察到的文件/目录来迅速生成通知的 lots 。一旦您拥有无与伦比的事件的/proc/sys/fs/inotify/max_queued_events并解开程序,它将收到IN_Q_OVERFLOW(并且可能错过一些事件,而这些事件不适合队列(。

手表ID重复使用非常繁殖,因为现代内核从类似文件描述符的行为转变为手表ID的类似PID的行为。您应该使用与对PID重复使用测试时相同的方法 - 创建和破坏inotify手表的直到整数手表ID包裹为止。

Intotify还具有几个棘手的角式案例,在正常操作过程中很少发生(例如,我知道的所有Java绑定,包括Android和OpenJDK,都不会正确处理所有绑定(IN_UNMOUNT

相同的输入问题在Inotify文档中得到了很好的解释:

成功呼叫inotify_add_watch((返回了此Inotify实例的唯一手表描述符,用于对应于PathName的文件系统对象(Inode(。如果此Inotify实例以前未观看文件系统对象,则新分配了手表描述符。如果已经观看了文件系统对象(也许是通过指向同一对象的其他链接(,则返回现有手表的描述符。

用简单的话说:如果您观看两个硬链接到同一文件,则其数字手表ID将是相同的。如果您将手表存储在Hashmap之类的东西,则可以通过整数手表ID键入

,因此这种行为很容易导致失去第二个Inotify手表的跟踪。

第二期更难观察,因此尽管没有错误模式,但很少得到适当的支持:卸下当前通过Inotify观察到的分区。棘手的部分是:Linux Filesystems在您打开文件描述符时不允许您卸载自己,但是通过Inotify 观察文件并不能阻止文件系统删除。如果您的应用在单独的文件系统上观察文件,并且用户删除了文件系统,则必须准备好处理结果IN_UNMOUNT事件。

上面的所有测试都应在TMPFS文件系统上执行。

思考后,我提出了另一个解决方案。您可以使用Linux" seccomp"功能来对单个与局体相关的系统调用的"模拟"结果。这种方法的优势是简单,坚固且完全不受欢迎的。您可以在其他情况下仍在使用原始OS行为的同时,有条件地调整SYSCALL的行为。从技术上讲,这仍然算作模拟,但是模拟层在内核代码和用户空间syscall接口之间非常深入。

您不需要修改程序代码,只需编写包装器即可在exec -ing您的应用程序之前安装拟合Seccomp filter(下面的代码使用libseccomp(:

 // pass control to kernel syscall code by default
 scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
 if (!ctx) exit(1);
 // modify behavior of specific system call to return `EMFILE` error
 seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EMFILE), __NR_inotify_init, 0));
 execve(...

seccomp本质上是有限的解释器,运行BPF字节码的扩展版本,因此它的功能非常广泛。LIBSECCOMP允许您安装有限的条件过滤器(例如,将系统调用的整数参数与常数值进行比较(。如果您想实现更令人印象深刻的条件行为(例如,将传递给inotify_add_watch的文件路径与预定义值进行比较(,则可以将Seccomp((Syscall的直接使用与内核BPF((功能结合在一起,以在EBPF方言中编写复杂的过滤程序。

编写syscall滤波器可能很乏味,而在seccomp效果下的程序行为实际上并不依赖于内核实现(seccomp滤波器在将控件传递给内核syscall handler之前由内核调用。因此,您可能需要将Seccomp的稀疏使用与更有机的方法相结合,在我的其他答案中概述。

可能不像您那样不受欢迎喜欢,但是inotify_simpleINotify类很小。您可以将其完全包装,委派所有方法并注入错误。

代码看起来像这样:

from inotify_simple.inotify_simple import INotify
class WrapINotify(object):
    init_error_list      = []
    add_watch_error_list = []
    rm_watch_error_list  = []
    read_error_list      = []
    def raise_if_error(self, error_list):
        if not error_list:
            return
        # Simulate INotify raising an exception
        exception = error_list.pop(0)
        raise exception
    def __init__(self):
        self.raise_if_error(WrapINotify.init_error_list)
        self.inotify = INotify()
    def add_watch(self, path, mask):
        self.raise_if_error(WrapINotify.add_watch_error_list)
        self.inotify.add_watch(path, mask)
    def rm_watch(self, wd):
        self.raise_if_error(WrapINotify.rm_watch_error_list)
        return self.inotify.rm_watch(wd)
    def read(self, timeout=None, read_delay=None):
        self.raise_if_error(WrapINotify.read_error_list)
        return self.inotify.read(timeout, read_delay)
    def close(self):
        self.inotify.close()
    def __enter__(self):
        return self.inotify.__enter__()
    def __exit__(self, exc_type, exc_value, traceback):
        self.inotify.__exit__(exc_type, exc_value, traceback)

使用此代码,您会在其他地方做:

WrapINotify.add_watch_error_list.append(OSError(28, 'No space left on disk'))

注入错误。当然,您可以在包装器类中添加更多代码以实现不同的错误注入方案。

最新更新