如何在功能测试中模拟INotify失败?

时间:2018-03-27 03:23:33

标签: linux testing filesystems functional-testing inotify

我有一个Linux应用程序,它使用inotify跟踪文件系统更改。我想为它编写一个功能测试套件,从最终用户角度测试应用程序,作为其中的一部分,我想测试文件系统失败的情况,特别是我想测试inotify失败。
我需要进行inotify_init()inotify_add_watch()inotify_rm_watch()调用以及read()调用inotify文件描述符在测试中需要时返回错误。< / p>

但问题是我找不到如何模拟inotify失败的方法。我想知道是否有人遇到过这样的问题,并且知道一些解决方案。

3 个答案:

答案 0 :(得分:2)

如果你想避免任何嘲弄,你最好的选择就是直接触及操作系统限制来引发错误。例如,如果调用进程已达到对打开文件描述符数量的限制,inotify_init可能会失败EMFILE errno。要达到100%精度的条件,您可以使用两个技巧:

  1. changing values in procfs
  2. 动态操作运行流程的限制
  3. 将您的应用程序进程分配给专用cgroup,并通过cgroups API为其提供~0%CPU时间来“暂停”它(这就是Android限制后台应用程序并实现其节能“打盹”模式的方式)。
  4. inotify的所有可能错误条件都记录在inotifyinotify_initinotify_add_watch的手册页中(我不认为inotify_rm_watch可能会失败,除了纯编程代码中的错误。)

    除了普通的错误(例如转过/proc/sys/fs/inotify/max_user_watches)之外,inotify有几种错误模式(队列空间耗尽,监视ID重用),但严格意义上说这些不是“失败”。

    当某人执行文件系统更改的速度超过您的反应速度时,就会发生队列耗尽。它很容易重现:使用cgroups暂停程序,同时打开inotify描述符(因此事件队列不会被耗尽)并通过修改观察到的文件/目录快速生成批次通知。一旦您有/proc/sys/fs/inotify/max_queued_events未处理的事件,并取消暂停您的程序,它将收到IN_Q_OVERFLOW(可能会遗漏一些不适合队列的事件)。

    监视ID重用是繁琐的重现,因为现代内核从类似文件描述符的行为切换到监视ID的类似PID的行为。您应该使用与测试PID重用时相同的方法 - 创建并销毁inotify监视的批次,直到整数监视ID换行。

    Inotify还有一些棘手的角落案例,在正常操作期间很少发生(例如,我所知道的所有Java绑定,包括Android和OpenJDK,都无法正确处理所有这些):同样的inode问题和处理IN_UNMOUNT

    inotify文档中详细解释了相同的inode问题:

      

    对inotify_add_watch()的成功调用会为此inotify实例返回与路径名对应的文件系统对象(inode)的唯一监视描述符。如果此inotify实例先前未监视文件系统对象,则新分配监视描述符。如果文件系统对象已被监视(可能通过指向同一对象的不同链接),则返回现有监视的描述符。

    简单来说:如果你看到同一个文件的两个硬链接,他们的数字手表ID将是相同的。如果您将手表存储在类似hashmap的内容中,则此行为很容易导致丢失第二个inotify手表的跟踪,并使用整数手表ID。

    第二个问题更难以观察,因此尽管甚至不是错误模式,但很少得到适当的支持:卸载当前通过inotify观察到的分区。棘手的部分是:Linux文件系统不允许您在打开文件描述符时卸载它们,但通过inotify 观察文件并不会阻止文件系统卸载。如果您的应用程序在单独的文件系统上观察文件,并且用户卸载该文件系统,则必须准备好处理生成的IN_UNMOUNT事件。

    上述所有测试都应该可以在tmpfs文件系统上执行。

答案 1 :(得分:1)

经过一番思考后,我想出了另一个解决方案。您可以使用Linux“seccomp”工具来“模拟”与单个inotify相关的系统调用的结果。这种方法的优点是简单,强大和完全非侵入性。您可以有条件地调整系统调用的行为,同时在其他情况下仍使用原始操作系统行为。从技术上讲,这仍然算作模拟,但是模拟层在内核代码和用户空间系统调用接口之间非常深入。

您不需要修改程序代码,只需编写一个包装器,在exec之前安装适合的seccomp过滤器(下面的代码使用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()系统调用与kernel bpf() facility的直接使用结合起来编写复杂的过滤程序在eBPF方言中。

编写系统调用过滤器可能很繁琐,而且受seccomp影响的程序行为实际上并不依赖于内核实现(在将控件传递给内核系统调用处理程序之前,内核会调用seccomp过滤器)。因此,您可能希望将seccomp的稀疏使用与更有机的方法相结合,在my other answer中进行了概述。

答案 2 :(得分:1)

可能不是您想要的非侵入性,但来自INotify的{​​{1}}类很小。您可以完全包装它,委托所有方法,并注入错误。

代码看起来像这样:

inotify_simple

使用此代码,您可以在其他地方执行此操作:

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)

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