Linux Test Project(LTP项目)

  • Post author:
  • Post category:linux

LTP测试写作指导/准则

本文档描述了LTP准则和LTP测试接口,并且适用于想要编写或修改LTP测试用例的任何人。这不是权威性指南,也不是常识的替代品。

 

1. 一般规则

1.1 简单性

对于所有值得保留的测试用例,都应使其尽可能简单或更好。内核和libc是棘手的野兽,它们的接口所带来的复杂性很高。如果您专注于要测试的界面并遵循UNIX哲学,最好使测试尽可能独立,这是一个好主意(不应依赖于尚未广泛使用的工具或库)。

不要重新发明轮子!

  • 使用LTP标准接口

  • 不要添加自定义通过/失败报告功能

  • 不要从头开始编写Makefile,请改用LTP构建系统,等等。

  • ……

1.2 代码重复

复制粘贴是一个很好的仆人,但主人却很差。如果要将大部分代码从一个测试用例复制到另一个用例,请考虑一下,如果您发现已在整个树上复制的代码中的错误,将会发生什么。那么将其移至库呢?

对于简短但复杂的部分也是如此,每当您要复制并粘贴系统调用包装程序时,该包装程序根据机器体系结构或类似复杂的代码打包参数,请将其放入header中。

1.3 编码风格

1.3.1 C编码风格

LTP采用Linux内核编码风格。如果您不熟悉它的规则,请在内核源代码 中找到linux / Documentation / CodingStyle并阅读,这是一篇写得很好的介绍。

还有一个checkpatch(请参阅linux / scripts / checkpatch.pl)脚本,可用于在提交之前检查补丁。

注意
如果checkpatch没有报告任何问题,则该代码可能仍然是错误的,因为该工具仅查找常见错误。

 

1.3.2 Shell编码风格

当在shell 中编写测试用例时,仅在 portable shell 中编写,最好也尝试使用 alternative shell(bash的替代,例如破折号)来运行测试。

Portable shell 程序是指POSIX定义的shell 程序命令语言,除了少数广泛使用的扩展名,即在函数内部使用的局部关键字以及-o和-a测试参数(在POSIX中标记为 obsolete)。

您可以尝试在Debian上运行测试用例,默认情况下/ bin / sh指向破折号,或者在您喜欢的发行版上安装dash 并使用它来运行测试。如果您的发行版缺少dash 包,您可以随时从源代码进行编译。

Debian也有不错的devscript checkbashism.pl ,可用于检查非便携式shell代码。

这是一些关于shell 的常识样式规则

  • 线数保持在80个字符以下

  • 使用标签进行缩进

  • 保持简单,避免不必要的子shell

  • 不要搞乱事情(即不要像普通的shell命令那样命名您的函数等)

  • 引用变量

  • 始终如一

1.4 注释代码

注释有时可以节省您的时间,但它们带来的弊大于利。在很多情况下,注释和实际实现之间的分离缓慢,导致API滥用和难以发现错误。记住,只有一件事比没有文档更糟糕,错误的文档。

通常,每个人都应该编写显而易见的代码(不幸的是,这并不总是可能的)。如果有需要注释的代码,请保持简短。永远不要评论显而易见的事情。

在使用LTP测试用例的情况下,习惯上在文件开头的某个位置(通常在GPL标头下方)添加带有高级测试描述的段落。这有助于其他人在深入了解技术细节之前了解测试的总体目标。

1.5 向后兼容

LTP测试应尽可能向后兼容。考虑具有长期支持的企业发行版(自初始发行以来已超过五年),或需要使用制造商提供的使用多年的工具链的嵌入式平台。

因此,针对更多当前功能的LTP测试应该能够应对较旧的系统。它至少应该可以正常编译,如果不适合该配置,则应返回TCONF(请参见下面的测试接口说明)。

我们使用几种检查类型:

配置脚本configure script 通常用来检测 system headers 中 一个函数声明的可用性,它被用于在编译时禁用测试。

我们还具有运行时检测内核版本功能,它被用于在compile 运行时 disable tests 禁用测试。

检查errno值是另一种runtime 运行时检查。当未实现 或 在内核编译时禁用系统调用时,大多数系统调用将返回EINVAL或ENOSYS。

有时定义一些宏而不是创建配置测试也很有意义。一个示例是include / lapi / posix_clocks.h中特定于Linux的POSIX时钟ID 。

1.6 处理混乱的旧代码

LTP包含许多旧的且凌乱的代码,我们正在尽最大努力对其进行清理,但是尽管有很多努力,但它仍然很多。如果您开始修改旧的或混乱的测试用例,并且更改比简单的错字修复更复杂,则应首先进行清理(在单独的补丁程序中)。如果将格式修订与影响测试行为的更改分开,则更容易查看更改。

移动文件也是如此。如果您需要重命名或移动文件,请在单独的补丁程序中进行。

1.7 许可证

贡献给LTP的代码应获得GPLv2 +(GNU GPL版本2或任何更高版本)的许可。使用SPDX-License-Identifier: GPL-2.0-or-later

 

2. 编写一个测试用例

2.1 LTP结构

LTP的结构非常简单。每个测试都是用 portable shell 或 C语言编写的二进制文件。该测试通过 环境变量和 / 或 命令行参数获取配置,它将附加信息打印到标准输出中,并通过退出值报告总体 成功/失败。

测试通常放在testcases /目录下。所有属于syscall 或(slightly confusingly)libc syscall包装器的东西都在 testcases / kernel / syscalls /下。然后是testcases / open_posix_testsuite ,它是上游项目的维护良好的分支,该分支自2005年以来就已终止,并且还有许多具有更多特定功能测试的目录。

2.1.1运行测试文件

要执行的测试列表存储在runtest /目录下的 runtest 文件中。要执行的默认运行测试文件集存储在scenario_groups / default中。添加测试时,还应将相应的条目添加到一些运行测试文件中。

对于syscall测试(这些放置在testcases / kernel / syscalls /下),请使用 runtest / syscalls文件;对于内核相关的内存管理测试,我们使用runtest / mm等。

每个测试的运行测试文件应具有一个条目。强烈建议不要创建运行所有测试的包装,并将其作为单个测试添加到runtest文件中。

2.1.2 数据文件

如果您的测试需要数据文件才能工作,则应将它们放入名为datafiles的子目录中,并安装到 testcases / data / $ TCID目录中(为此,您必须将 INSTALL_DIR:= testcases / data / TCID 添加 到 datafiles / Makefile中) 。

您可以通过由 test.sh 提供 $TST_DATAROOT 获得路径数据文件 $ ST_DATAROOT / … 或通过C函数 tst_dataroot()由libltp提供:

const  char * dataroot = tst_dataroot();

数据文件也可以作为访问 $LTPROOT/testcases/data/$TCID/…​, 但 $TST_DATAROOT 和 tst_dataroot()是首选,因为这些可以直接在git tree中 或者 安装位置处 运行测试用例时使用。

路径是根据以下规则构造的:

  1. 如果设置了$LTPROOT,则返回 $LTPROOT/testcases/data/$TCID

  2. 否则,如果调用 tst_tmpdir(),则返回 $STARTWD/datafiles(其中 $STARTWD 是由 tst_tmpdir()记录的初始工作目录)

  3. 否则返回 $CWD / datafiles

示例请参见 testcases/commands/file/ 目录

2.1.3 子可执行文件(Subexecutables)

如果测试需要执行二进制文件,请将其与测试用例放在同一目录中,并以$ {test_binary_name} _开头的文件命名。一旦框架执行了测试,带有所有LTP二进制文件的目录路径将添加到 $PATH中,您可以仅按其名称执行它。

如果需要从LTP树执行此类测试,则可以使用以下命令将当前目录的路径手动添加到 $PATH :PATH=”$PATH:$PWD” ./foo01.

2.2 用C编写测试

2.2.1基本测试结构

让我们从一个示例开始,以下代码是对getenv() 的简单测试。

/*
 * This is test for basic functionality of getenv().
 *
 *  - create an env variable and verify that getenv() can get get it
 *  - call getenv() with nonexisting variable name, check that it returns NULL
 */

#include "tst_test.h"

#define ENV1 "LTP_TEST_ENV"
#define ENV2 "LTP_TEST_THIS_DOES_NOT_EXIST"
#define ENV_VAL "val"

static void setup(void)
{
	if (setenv(ENV1, ENV_VAL, 1))
		tst_brk(TBROK | TERRNO, "setenv() failed");
}

static void test(void)
{
	char *ret;

	ret = getenv(ENV1);

	if (!ret) {
		tst_res(TFAIL, "getenv(" ENV1 ") = NULL");
		goto next;
	}

	if (!strcmp(ret, ENV_VAL)) {
		tst_res(TPASS, "getenv(" ENV1 ") = '"ENV_VAL "'");
	} else {
		tst_res(TFAIL, "getenv(" ENV1 ") = '%s', expected '"
		               ENV_VAL "'", ret);
	}

next:
	ret = getenv(ENV2);

	if (ret)
		tst_res(TFAIL, "getenv(" ENV2 ") = '%s'", ret);
	else
		tst_res(TPASS, "getenv(" ENV2 ") = NULL");
}

static struct tst_test test = {
	.test_all = test,
	.setup = setup,
};

每个测试都包含tst_test.h标头,并且必须定义struct tst_test测试结构。

整个测试初始化​​在setup() 函数中完成。

整体清理在cleanup() 函数中完成。这里没有cleanup(),因为测试没有什么要清理的。如果在测试结构中设置了清理,则在测试库清理之前的测试退出时调用它。这尤其意味着可以在测试执行的任何时候调用清除。例如,即使测试设置步骤失败了,因此cleanup() 函数也必须能够处理未完成的初始化,依此类推。

测试本身在test() 函数中完成。如果在循环中调用,则测试功能必须工作正常。

测试结构中有两种类型的测试功能指针。第一个是 .test_all 指针,当将测试实现为单个函数时使用。然后有一个 .test 函数以及测试 .tcnt 的数量,可以提供更详细的结果报告。如果设置了.test指针,则会使用整数参数 [0,.tcnt -1] 调用该函数.tcnt次。

一次只能设置 .test 和 .test_all 中的一个。

每个测试的默认超时设置为300秒。通过在测试结构中设置 .timeout 或在测试setup() 中调用 tst_set_timeout() 可以覆盖默认超时。有一些测试用例的运行时间可能会任意变化,因为可以通过将其设置为-1来禁用这些超时。

测试可以通过调用tst_timeout_remaining() 来找出剩余的超时时间(以秒为单位)。

为了编写正确的cleanup() 回调,需要遵循一些规则

  1. 仅释放已初始化的资源。请记住,可以在测试运行中的任何时候执行回调。

  2. 确保以相反的顺序释放资源。(某些步骤可能不依赖于其他步骤,如果交换了所有步骤,一切都会正常进行,但让我们保持秩序。)

首先,第一个规则可能看起来很复杂,但是相反,这很容易。您要做的就是跟踪已初始化的内容。例如,仅当文件描述符被分配了有效的文件描述符时才需要关闭它们。对于大多数事情,您需要创建额外的标志,但是该标志在成功初始化之后立即设置。考虑下面的测试设置。

我们还更喜欢清理本应在程序出口释放的资源。做出此决定的主要原因有两个。

  • 在测试库为测试临时目录安装了文件系统的情况下,文件描述符和mmaped内存之类的资源可能会阻止卸载块设备。
  • 如果不释放分配的内存,则会在检查libc和其他低级库中的泄漏时,使静态分析和工具(如valgrind)产生false-positives(误报)
static int fd0, fd1, mount_flag;

#define MNTPOINT "mntpoint"
#define FILE1 "mntpoint/file1"
#define FILE2 "mntpoint/file2"

static void setup(void)
{
	SAFE_MKDIR(MNTPOINT, 0777);
	SAFE_MKFS(tst_device->dev, tst_device->fs_type, NULL, NULL);
	SAFE_MOUNT(tst_device->dev, MNTPOINT, tst_device->fs_type, 0, 0);
	mount_flag = 1;

	fd0 = SAFE_OPEN(cleanup, FILE1, O_CREAT | O_RDWR, 0666);
	fd1 = SAFE_OPEN(cleanup, FILE2, O_CREAT | O_RDWR, 0666);
}

在这种情况下,当SAFE_ * 宏中的任何一个失败时,都可以调用cleanup() 函数,因此也必须能够完成未完成的初始化。由于全局变量被初始化为零,我们可以在尝试将其关闭之前先检查fd> 0。成功安装设备后,挂载功能需要设置额外的标志。

static void cleanup(void)
{
	if (fd1 > 0)
		SAFE_CLOSE(fd1);

	if (fd0 > 0)
		SAFE_CLOSE(fd0);

	if (mount_flag && tst_umouont(MNTPOINT))
		tst_res(TWARN | TERRNO, "umount(%s)", MNTPOINT);
}
Important 清理中使用的 SAFE_MACROS() 不会退出测试。失败只会产生警告,并且 cleanup() 继续进行。这是有意的,因为我们希望执行尽可能多的 cleanup() 
Warning 在测试 cleanup() 中调用 tst_brk() 也不会退出测试,并且TBROK会转换为TWARN。
Note 测试临时目录的创建和删除在测试库中处理,并且递归删除该目录。因此,我们不必在测试清理中删除文件和目录。

2.2.2基本测试界面

void tst_res(int ttype, char *arg_fmt, ...);

类似Printf的功能可报告测试结果,通常用于以下类型:

TPASS

Test has passed.

TFAIL

Test has failed.

TINFO

General message.

可以将ttype与TERRNO或TTERRNO按位组合以分别打印errno,TST_ERR。

void tst_brk(int ttype, char *arg_fmt, ...);

类似Printf的函数报告错误并退出测试,可以与ttype一起使用:

TBROK

在测试准备阶段失败了。

TCONF

测试不适用于当前配置(未实现syscall,不支持的arch等)

可以将ttype与TERRNO或TTERRNO按位组合以分别打印errno,TST_ERR。

const char *tst_strsig(int sig);

返回给定信号编号的相应字符串。

const char *tst_strerrno(int err);

返回给定的errno号的对应字符串。 最好使用此函数将errno值转换为字符串。 您不应在测试用例中使用strerror()函数。

const char * tst_strstatus(int status);

Warning

此函数不是线程安全的。
void tst_set_timeout(unsigned int timeout);

允许在test setup()中动态设置每次测试迭代的超时,超时以秒为单位指定。 有一些测试用例的运行时可以任意变化,这些测试用例可以通过将其设置为-1来禁用超时。

void tst_flush(void);

刷新输出流,适当处理错误。

当您必须在调用fork()或clone()之前刷新输出流时,几乎不需要此函数。 请注意,SAFE_FORK()自动调用此函数。 请参阅3.4 FILE缓冲区和fork()以了解为什么需要这样做。

2.2.3测试临时目录

如果在struct tst_test 中将 .needs_tmpdir 设置为1,则会创建唯一的测试临时目录,并将其设置为测试工作目录。测试不得在该目录之外创建临时文件。使用以下标志时,无需设置该标志:.all_filesystems,.format_device,.mntpoint, .mount_device .needs_checkpoints,.needs_device和.resource_file (这些标志表示创建临时目录)。

Important

在test()函数或test cleanup()中关闭所有文件描述符(指向测试临时目录中的文件,甚至是未链接的文件),否则测试可能会中断NFS上的临时目录删除(查找“ NFS silly rename” ”)。

2.2.4安全宏

安全宏旨在简化测试准备中的错误检查。您只需使用相应的安全宏,而不是调用系统API函数,检查它们的返回值并中止操作是否失败即可进行测试。

尽可能使用它们。

而不是写:

fd = open("/dev/null", O_RDONLY);
if (fd < 0)
	tst_brk(TBROK | TERRNO, "opening /dev/null failed");

你只需写:

fd = SAFE_OPEN("/dev/null", O_RDONLY);

Important

成功关闭后,SAFE_CLOSE()函数还会将传递的文件描述符设置为-1。

它们还可以简化sysfs文件的读取和写入,例如,您可以执行以下操作:

SAFE_FILE_SCANF("/proc/sys/kernel/pid_max", "%lu", &pid_max);

有关完整列表,请参见include / tst_safe_macros.h,include / tst_safe_stdio.h和include / tst_safe_file_ops.h和include / tst_safe_net.h。

2.2.5 Test specific command line options

struct tst_option {
        char *optstr;
        char **arg;
        char *help;
};

测试特定的命令行参数可以与  struct tst_option 中的 NULL-terminated 数组一起传递。

optstr是命令行选项,即“ o”或“ o:”(如果该选项具有参数)。 仅支持短选项。 arg是匹配时optarg的存储位置。 如果option没有参数,则将其设置为非NULL值(如果存在)。 help是简短的帮助字符串。

Note

测试参数不得与库中定义的通用测试参数冲突,当前使用的参数是-i,-I,-C和-h。
int tst_parse_int(const char *str, int *val, int min, int max);
int tst_parse_float(const char *str, float *val, float min, float max);

解析 tst_option结构 中返回的字符串的 帮助文献。

两者都在成功时返回零,而在失败时返回errno,主要是EINVAL或ERANGE。

如果str为NULL,则这两个函数均为无操作。

结果的有效范围包括最小和最大。

用法示例:

#include <limits.h>
#include "tst_test.h"

static char *str_threads;
static int threads = 10;

static struct tst_option options[] = {
	{"t:", &str_threads, "Number of threads (default 10)"},
	...
	{NULL, NULL, NULL}
};

static void setup(void)
{
	if (tst_parse_int(str_threads, &threads, 1, INT_MAX))
		tst_brk(TBROK, "Invalid number of threads '%s'", str_threads);

	...
}

static void test_threads(void)
{
	...

	for (i = 0; i < threads; i++) {
		...
	}

	...
}

static struct tst_test test = {
	...
	.options = options,
	...
};

2.2.6 运行时内核版本检测

对于新添加的内核功能的测试用例,需要比特定版本更新的内核才能运行。 您需要跳过对旧内核的测试,只需将struct tst_test中的.min_kver字符串设置为所需的最低内核版本,例如 .min_kver =“ 2.6.30”。

对于更复杂的操作,例如跳过对特定范围内核版本的测试,可以使用以下功能:

int tst_kvercmp(int r1, int r2, int r3);

struct tst_kern_exv {
        char *dist_name;
        char *extra_ver;
};

int tst_kvercmp2(int r1, int r2, int r3, struct tst_kern_exv *vers);

这两个函数用于运行时内核版本检测。 他们解析uname()的输出,并将其与传递的值进行比较。

返回值类似于strcmp()函数,即零表示相等,负值表示内核比预期值旧,正值表示内核较新。

第二个函数tst_kvercmp2()允许指定每个供应商的内核版本表,因为供应商通常向其内核回传修补程序,并且即使内核版本不建议这样做,测试也可能是相关的。 有关用法示例,请参见 testcases / kernel / syscalls / inotify / inotify04.c。

Warning

shell程序 tst_kvercmp 将结果映射为无符号整数 – 进程退出值。

2.2.7 Fork()-ing

请注意,如果测试forks 并且 tst _ *()接口打印了消息 ,则数据可能仍位于libc / kernel缓冲区中,并且不会自动刷新这些缓冲区 。

当stdout 重定向到文件时,会发生这种情况。在这种情况下, 标准输出不是行缓冲的,而是块缓冲的。因此,在派生之后,缓冲区的内容将由 parent 和 每个 children.打印。

为了避免这种情况,您应该使用SAFE_FORK()。

重要
如果您的测试用例分叉,则必须在测试结构中设置.forks_child 标志。

2.2.8在子进程中进行测试

tst_res()返回的结果通过共享内存块传播到父进程。

调用tst_brk()会使子进程以非零退出值退出。这意味着在子进程中也可以使用SAFE _ *()宏。

在测试库中等待超过test()函数执行时间的子进程 。不干净的子进程退出(被信号杀死,退出值非零等)将导致主测试进程使用tst_brk()退出,这尤其意味着从子进程传播的TBROK 将导致整个测试通过TBROK 退出。

如果测试需要一个有段错误 segfaults    子进程,或者执行任何其他导致该子进程异常退出的操作,则您需要等待test()函数中的此类子进程 , 以便在主测试退出test() 函数之前获得该子进程。

#include "tst_test.h"

void tst_reap_children(void);

tst_reap_children()函数使进程等待所有子进程,如果其中任何一个返回非零退出代码,则以tst_brk(TBROK,…)退出。

从exec()启动的二进制文件中使用tst_res()

/* test.c */
#define _GNU_SOURCE
#include <unistd.h>
#include "tst_test.h"

static void do_test(void)
{
	char *const argv[] = {"test_exec_child", NULL};
	char path[4096];

	if (tst_get_path("test_exec_child", path, sizeof(path)))
		tst_brk(TCONF, "Couldn't find test_exec_child in $PATH");

	execve(path, argv, environ);

	tst_res(TBROK | TERRNO, "EXEC!");
}

static struct tst_test test = {
	.test_all = do_test,
	.child_needs_reinit = 1,
};

/* test_exec_child.c */
#define TST_NO_DEFAULT_MAIN
#include "tst_test.h"

int main(void)
{
	tst_reinit();
	tst_res(TPASS, "Child passed!");
	return 0;
}

该tst_res()函数也可以从开始的二进制文件使用EXEC() ,父测试过程必须设置.child_needs_reinit使图书馆准备它的标志并且有以确保LTP_IPC_PATH环境变量传递下去,那么程序必须在main()中调用的第一件事 是设置IPC的tst_reinit()。

2.2.9 Fork()和父子同步

由于LTP测试是针对Linux编写的,因此大多数测试都涉及fork()-ing和父子进程同步。LTP包括一个检查点库,该库提供了基于等待/唤醒 futex 的功能。

为了使用检查点,必须将struct tst_test中的.needs_checkpoints标志设置为1,这将导致测试库在调用test()函数之前初始化检查点。

#include "tst_test.h"

TST_CHECKPOINT_WAIT(id)

TST_CHECKPOINT_WAIT2(id, msec_timeout)

TST_CHECKPOINT_WAKE(id)

TST_CHECKPOINT_WAKE2(id, nr_wake)

TST_CHECKPOINT_WAKE_AND_WAIT(id)

检查点接口提供了一对唤醒和等待功能。该ID是无符号整数,指定检查点 checkpoint 唤醒/等待。事实上,它是存储在共享内存中的数组的索引,因此它从0开始, 并且至少要有数百个空间。

TST_CHECKPOINT_WAIT()和TST_CHECKPOINT_WAIT2() ,直到它唤醒,或者直到达到超时挂起的流程执行。

TST_CHECKPOINT_WAKE()唤醒在检查点的一种过程的等待。如果没有进程在等待,则该函数将重试,直到成功或达到超时为止。

如果已达到超时,则进程退出并显示相应的错误消息(使用 tst_brk())。

TST_CHECKPOINT_WAKE2()和TST_CHECKPOINT_WAKE() 不同在于其可以用于唤醒精确nr_wake过程。

TST_CHECKPOINT_WAKE_AND_WAIT()是执行唤醒,然后立即在同一检查点上等待的简写。

通过SAFE_FORK()创建的子进程可以使用检查点同步功能,因为它们会自动继承映射的页面。

通过exec()启动的子进程 或 未从测试进程派生的任何其他进程 必须通过调用tst_reinit()来初始化检查点。

有关接口的详细信息,请查看include / tst_checkpoint.h。

#include "tst_test.h"

/*
 * Waits for process state change.
 *
 * The state is one of the following:
 *
 * R - process is running
 * S - process is sleeping
 * D - process sleeping uninterruptibly
 * Z - zombie process
 * T - process is traced
 */
TST_PROCESS_STATE_WAIT(pid, state)

所述TST_PROCESS_STATE_WAIT()等待直到过程PID是在请求的 状态。呼叫轮询/proc/pid/stat以获取此信息。

它通常与状态S一起使用,这意味着进程正在内核中休眠,例如在pause()或任何其他阻塞的系统调用中。

2.2.10 信号和信号处理程序

如果需要使用信号处理程序,请使代码简短明了。不要忘记信号处理程序是异步调用的,并且可以在任何地方中断代码执行。

这意味着,从测试代码和信号处理程序更改全局状态时,会出现问题,这有时会导致:

  • 数据损坏(数据进入不一致状态),例如,对FILE对象的任何操作都可能发生这种情况。

  • 例如,如果同时从测试代码和信号处理程序中调用malloc(2),free(2)等,则会发生死锁,因为 malloc对其内部数据结构具有全局锁定。(请注意, libc函数也在内部使用malloc(2)。)

  • 任何其他不可复制和意外的行为。

常见的错误是从信号处理程序调用exit(3)。请注意,该函数不是信号异步安全的,因为它会刷新缓冲区等。如果您需要立即从信号处理程序中退出测试,请使用_exit(2)。

Tip

有关信号异步安全功能的列表,请参见man 7 signal。

如果信号处理程序设置了变量,则其声明必须是volatile,否则编译器可能会错误地优化代码。这是因为在编译器代码流分析中可能不会更改该变量。sig_atomic_t 类型被定义在C99中,但这个意味着 volatile(它只是一个 typedef to int )。因此,信号处理程序更改的标志,其正确类型是 volatile int 或 volatile sig_atomic_t。

如果在测试中预计会发生崩溃(例如,由信号SIGSEGV触发),则可以通过调用 tst_no_corefile()函数来避免创建核心文件。这对调用它的进程(及其子进程)生效,除非它们随后修改了RLIMIT_CORE。

请注意,LTP库将获取所有未测试的进程,并且将任何非零的退出代码报告为失败。

2.2.11 内核模块

在某些情况下,测试需要内核部分和用户空间部分,但很高兴,LTP可以构建内核模块,然后在测试开始时将其插入内核。有关详细信息,请参见/kernel/device-drivers/block 

2.2.12 有用的宏

ARRAY_SIZE(arr)

返回静态定义数组的大小,即 (sizeof(arr)/ sizeof(* arr))

LTP_ALIGN(x,a)

将x对齐为a的下一个倍数。a必须是2的幂。

2.2.13 文件系统类型检测

已知某些测试在某些文件系统上失败(您不能在TMPFS上交换,还有未实现的fcntl()等)。

如果需要在某些文件系统上跳过测试,请使用以下界面:

#include "tst_test.h"

	/*
	 * Unsupported only on NFS.
	 */
	if (tst_fs_type(".") == TST_NFS_MAGIC)
		tst_brk(TCONF, "Test not supported on NFS filesystem");


	/*
	 * Unsupported on NFS, TMPFS and RAMFS
	 */
	long type;

	switch ((type = tst_fs_type("."))) {
	case TST_NFS_MAGIC:
	case TST_TMPFS_MAGIC:
	case TST_RAMFS_MAGIC:
		tst_brk(TCONF, "Test not supported on %s filesystem",
		        tst_fs_type_name(type));
	break;
	}

2.2.14 LTP库中的线程安全

在多线程测试中使用库 tst_res()函数是安全的。

只有主线程必须从test()函数返回到测试库,并且必须在终止可能调用任何库函数的所有线程之后才能执行此操作。这尤其意味着,可能会调用tst_brk()的线程 必须在test()函数的执行返回到库之前终止。

通常,这是通过在test()函数末尾连接所有工作线程的主线程来完成的。请注意,在从其中一个线程调用tst_brk()的情况下,主线程将永远不会进入库代码,因为它将至少在调用tst_brk()的线程上的pthread_join()中休眠,直到exit()由tst_brk()调用。

如果从tst_brk()输入了 cleanup,则测试提供的cleanup函数将与其余线程并行运行。在用户提供的清除功能开始时,必须暂停或终止进入tst_brk()的后续线程。在测试清除之前,可能有必要停止或退出其余线程。例如,在删除临时目录之前,应停止创建新文件的线程。

以下代码示例显示了使用原子增量作为防护的线程安全清除函数示例。执行从用户提供的清除返回后,库将调用其清除,并期望只有一个线程从用户提供的清除返回到测试库。

#include "tst_test.h"

static void cleanup(void)
{
	static int flag;

	if (tst_atomic_inc(&flag) != 1)
		pthread_exit(NULL);

	/* if needed stop the rest of the threads here */

	...

	/* then do cleanup work */

	...

	/* only one thread returns to the library */
}

2.2.15 使用块设备进行测试

某些测试需要块设备(进行测试化,系统调用EROFS故障等)。LTP库包含用于准备测试设备的代码。

如果在结构tst_test 中设置了.needs_device 标志,则将 使用测试设备和要使用的默认文件系统的路径来初始化tst_device结构。

您还可以通过设置.dev_min_size来请求最小设备大小(以兆字节为 单位),然后确保设备至少具有请求的大小。

  • 如果设置了.format_device标志,则设备也将使用文件系统进行格式化。如果需要,可以使用.dev_fs_type 覆盖默认文件系统类型,并通过 .dev_fs_opts 和 .dev_extra_opts 指针将其他选项传递给mkfs 。请注意,.format_device 暗含 .needs_device ,无需同时设置两者。
  • 如果设置了.mount_device,则将设备安装在.mntpoint 上,该设备用于传递将创建的目录名称并将其用作安装目的地。您可以通过 .mnt_flags 和 .mnt_data 指针将其他标志和数据传递给mount命令。
  • 请注意,.mount_device表示.needs_device 和.format_device,因此无需设置后面的两个。
  • 如果设置了.needs_rofs,则在.mntpoint挂载只读文件系统,该文件系统应用于EROFS测试。
  • 如果设置了.all_filesystems,则对所有受支持的文件系统执行测试功能。根据mkfs的存在来检测支持的文件系统,
    $ fs helper并在内核支持下进行安装。对于每个受支持的文件系统,将tst_device.fs_type设置为当前测试的fs类型,如果设置了.format_device,则设备也进行了格式化,如果设置了.mount_device,则将其安装在.mntpoint。 此外,每次执行测试功能都会重置测试超时。 预期此标志将用于与文件系统相关的系统调用,这些调用至少部分以文件系统特定的代码(例如, fallocate()。
#include "tst_test.h"

struct tst_device {
	const char *dev;
	const char *fs_type;
};

extern struct tst_device *tst_device;

int tst_umount(const char *path);

如果在环境中将LTP_DEV传递给测试,则库会检查该文件是否存在以及它是否是块设备,如果设置了.device_min_size,还将检查设备大小。 如果未设置LTP_DEV或未满足大小要求,则会创建一个临时文件并将其附加到空闲循环设备。

如果没有可用的设备并且无法初始化循环设备,则使用TCONF退出测试。

tst_umount()函数与umount(2)完全一样,但是在EBUSY上重试了几次。 这是因为各种桌面守护程序(为此而闻名的gvfsd-trash)可能足够愚蠢,无法探测所有新安装的文件系统,从而导致umount(2)失败并导致EBUSY失败。

Important

所有测试用例都应使用tst_umount()而不是umount(2)来卸载文件系统。
#include "tst_test.h"

int tst_find_free_loopdev(const char *path, size_t path_len);

此函数查找一个免费的 loopdev 并返回免费的loopdev次要对象(如果没有免费的loopdev,则返回-1)。 如果path为非NULL,它将使用可用的 loopdev 路径填充。 如果要使用自定义的循环设备,我们可以在测试中调用tst_find_free_loopdev(NULL,0)以获取免费的次要编号,然后获取mknod。

#include "tst_test.h"

unsigned long tst_dev_bytes_written(const char *dev);

此函数读取测试块设备状态文件(/ sys / block / <device> / stat),并返回自上次调用此函数以来写入的字节。

2.2.16 使用文件系统格式化设备

#include "tst_test.h"

static void setup(void)
{
	...
	SAFE_MKFS(tst_device->dev, tst_device->fs_type, NULL, NULL);
	...
}

此函数采用设备路径,文件系统类型以及传递给mkfs的一系列附加选项。

fs选项fs_opts应该为NULL(如果不存在),或者以NULL终止的字符串数组,例如:

const char * const opts [] = {“ -b”,“ 1024”,NULL}。

额外的选项extra_opts应该为NULL(如果没有),或者为以NULL结尾的字符串数组,例如{“ 102400”,NULL}; 设备名称后将传递extra_opts。 例如:在这种情况下,mkfs -t ext4 -b 1024 / dev / sda1 102400 。

2.2.17 验证文件系统的可用空间

某些测试对文件系统的可用空间有大小要求。 如果不满足这些要求,则应跳过测试。

#include "tst_test.h"

int tst_fs_has_free(const char *path, unsigned int size, unsigned int mult);

如果有足够的空间,则tst_fs_has_free()函数将返回1,否则将返回0。

该路径是文件系统中任何目录/文件的路径名。

 mult 参数 是一个乘法器 , TST_BYTES, TST_KB, TST_MB or TST_GB.之一 。

所需的可用空间由size * mult 计算得出,例如 如果“ / tmp / testfile”所在的文件系统至少具有64MB可用空间,则tst_fs_has_free(“ / tmp / testfile”,64,TST_MB)将返回1,否则返回0。

2.2.18 文件,目录和fs限制

某些测试需要知道到常规文件或目录的链接的最大数量(例如,rename(2)或linkat(2))才能测试EMLINK错误。

#include "tst_test.h"

int tst_fs_fill_hardlinks(const char *dir);

尝试在目录中获取到常规文件的最大硬链接数。

Note

此数字取决于文件系统目录

此函数使用link(2)创建指向单个文件的硬链接,直到获得EMLINK或创建65535链接为止。 如果达到限制,则返回最大数量的硬链接,并且在目录中填充了格式为“ testfile%i”的硬链接,其中i属于[0,limit)间隔。 如果没有达到限制,或者如果ENOSPC或EDQUOT链接(2)失败,则返回零,并删除先前创建的文件。

#include "tst_test.h"

int tst_fs_fill_subdirs(const char *dir);

尝试获取目录中最大子目录数。

Note

此数字取决于打开的文件系统目录。 对于当前内核,subdir限制不适用于所有文件系统(可用于ext2,ext3,minix,sysv等)。 如果测试在其他文件系统(例如ramfs,tmpfs)上运行,它甚至不会尝试达到限制并返回0。

此函数使用 mkdir(2)在dir中创建目录,直到获得EMLINK或创建65535目录为止。 如果达到限制,则返回最大子目录数,并且该目录以“ testdir%i”格式填充子目录,其中i属于[0,限制-2]间隔(因为每个新创建的目录已经有两个链接) -。和来自父目录的链接)。 如果没有达到限制,或者mkdir(2)因ENOSPC或EDQUOT而失败,则返回零,并删除先前创建的目录。

#include "tst_test.h"

int tst_dir_is_empty(const char *dir, int verbose);

如果目录为空,则返回非零值,否则返回零。

如果目录仅包含,则认为目录为空。 还有…

#include "tst_test.h"

int tst_fill_fd(int fd, char pattern, size_t bs, size_t bcount);

使用文件描述符以指定的模式填充文件。

#include "tst_test.h"

int tst_fill_file(const char *path, char pattern, size_t bs, size_t bcount);

使用文件路径以指定的模式创建/覆盖文件。

2.2.19 获取未使用的PID号

某些测试需要PID,OS不会使用该PID(不属于OS中的任何进程)。 例如,kill(2)如果传递了此类PID,则应将errno设置为ESRCH。

#include "tst_test.h"

pid_t tst_get_unused_pid(void);

返回操作系统或操作系统中未使用的PID值。

#include "tst_test.h"

int tst_get_free_pids(void);

返回系统中未使用的pid的数量。 请注意,调用返回后,该数字可能会有所不同,并且仅应用于粗略估算。

2.2.20 Running executables

#include "tst_test.h"

int tst_run_cmd(const char *const argv[],
	        const char *stdout_path,
	        const char *stderr_path,
	        int pass_exit_val);

tst_run_cmd是vfork()+ execvp()的包装,它提供了一种执行外部程序的方法。

argv []是一个以NULL终止的字符串数组,其开头是程序名称,后跟可选参数。

pass_exit_val为非零值,tst_run_cmd会将程序退出代码返回给调用方。 如果pass_exit_val为零,则tst_run_cmd会在失败时退出测试。

如果execvp()失败并且设置了pass_exit_val标志,返回值为255,否则 execvp()失败,返回ENOENT 和 254。

stdout_path 和 stderr_path 确保将程序 stdout 和 stderr I / O流重定向到的位置。

例:

#include "tst_test.h"

const char *const cmd[] = { "ls", "-l", NULL };

...
	/* Store output of 'ls -l' into log.txt */
	tst_run_cmd(cmd, "log.txt", NULL, 0);
...

2.2.21 测量 elapsed time 经过时间 和 helper  functions 助手函数

#include "tst_timer.h"

void tst_timer_check(clockid_t clk_id);

void tst_timer_start(clockid_t clk_id);

void tst_timer_stop(void);

struct timespec tst_timer_elapsed(void);

long long tst_timer_elapsed_ms(void);

long long tst_timer_elapsed_us(void);

int tst_timer_expired_ms(long long ms);
  • tst_timer_check()函数检查是否支持指定的 clk_id ,否则使用TCONF退出测试。 预期将在初始化任何需要清除的资源之前在测试setup()中使用它,因此它不包含清除函数参数。
  • tst_timer_start()标记开始时间并存储clk_id以供进一步使用。
  • tst_timer_stop()使用与最后一次调用tst_timer_start()相同的clk_id标记停止时间。
  • tst_timer_elapsed *()以几种格式和单位返回计时器开始时间和最后计时器停止时间之间的时差。
  • tst_timer_expired_ms()函数检查由tst_timer_start()启动的计时器是否运行的时间超过ms毫秒。 如果计时器到期,则函数返回非零值,否则返回零

Important

计时器函数内部使用clock_gettime(),需要与旧版glibc上的-lrt链接。 请不要忘记在Makefile中添加LDLIBS + =-lrt。
#include "tst_test.h"
#include "tst_timer.h"

static void setup(void)
{
	...
	tst_timer_check(CLOCK_MONOTONIC);
	...
}

static void run(void)
{
	...
	tst_timer_start(CLOCK_MONOTONIC);
	...
	while (!tst_timer_expired_ms(5000)) {
		...
	}
	...
}

struct tst_test test = {
	...
	.setup = setup,
	.test_all = run,
	...
};

Expiration timer 到期计时器示例用法。

long long tst_timespec_to_us(struct timespec t);
long long tst_timespec_to_ms(struct timespec t);

struct timeval tst_us_to_timeval(long long us);
struct timeval tst_ms_to_timeval(long long ms);

int tst_timespec_lt(struct timespec t1, struct timespec t2);

struct timespec tst_timespec_add_us(struct timespec t, long long us);

struct timespec tst_timespec_diff(struct timespec t1, struct timespec t2);
long long tst_timespec_diff_us(struct timespec t1, struct timespec t2);
long long tst_timespec_diff_ms(struct timespec t1, struct timespec t2);

struct timespec tst_timespec_abs_diff(struct timespec t1, struct timespec t2);
long long tst_timespec_abs_diff_us(struct timespec t1, struct timespec t2);
long long tst_timespec_abs_diff_ms(struct timespec t1, struct timespec t2);

前四个功能是简单的内联转换功能。

  • 如果t1早于t2,则tst_timespec_lt()函数将返回非零值。
  • tst_timespec_add_us()函数将us微秒添加到 结构体 the timespec t。 us预期为正。
  • tst_timespec_diff *()函数返回两次之间的差,预期t1会晚于t2。
  • tst_timespec_abs_diff *()函数返回两次之间的差的绝对值。

Note

所有转化为ms和us的之都会四舍五入

2.2.22 数据文件

#include "tst_test.h"

static const char *const res_files[] = {
	"foo",
	"bar",
	NULL
};

static struct tst_test test = {
	...
	.resource_files = res_files,
	...
}

如果测试需要将其他文件复制到测试临时目录,则只需在 tst_test结构 的 NULL-terminated array.resource_files 中列出其文件名。

设置资源文件后,会自动创建测试临时目录,还需要设置.needs_tmpdir。

测试库首先查找数据文件,这些文件要么在测试开始时存储在$ PWD中的数据文件目录中,要么存储在$ LTPROOT / testcases / data / $ {test_binary_name}中。 如果找不到该文件,则在测试开始时,库将查找$ LTPROOT / testcases / bin /并查找$ PWD。 这样可以确保测试用例可以在从编译目录开始测试以及安装LTP时轻松复制文件。

将文件复制到新创建的测试临时目录,该目录将在执行test()函数时设置为测试工作目录。

2.2.23 代码路径跟踪

tst_res是一个宏,因此在一个文件中定义函数时依此类推:

int do_action(int arg)
{
	...

	if (ok) {
		tst_res(TPASS, "check passed");
		return 0;
	} else {
		tst_res(TFAIL, "check failed");
		return -1;
	}
}

使用另一个文件中调用它时,the file and line 将被指出通过先前的文件中 函数的 tst_res。

TST_TRACE可以使这种情况的分析更加容易。 这是一个宏,它会插入对tst_res(TINFO,…)的调用,以防其参数的计算结果为非零。 在对tst_res(TINFO,…)的调用中,将使用TST_TRACE的实际位置来扩展文件和行。

例如,如果另一个文件包含:

#include "tst_test.h"

if (TST_TRACE(do_action(arg))) {
	...
}

生成的输出可能类似于:

common.h:9: FAIL: check failed
test.c:8: INFO: do_action(arg) failed

2.2.24 Tainted kernels / 污染的内核

如果需要检测,如果测试用例触发了内核警告,错误或哎呀,则可以使用以下命令检测 TAINT_W 或 TAINT_D:

#include "tst_test.h"
#include "tst_taint.h"

void setup(void)
{
	...
	tst_taint_init(TST_TAINT_W | TST_TAINT_D);
	...
}
...
void run(void)
{
	...
	if (tst_taint_check() == 0)
		tst_res(TPASS, "kernel is not tainted");
	else
		tst_res(TFAIL, "kernel is tainted");
}

您必须首先使用非零标志调用tst_taint_init(),最好在setup()期间进行。 如果正在运行的内核不完全支持所请求的标志,则该函数将生成TCONF;如果提供了零掩码或在执行测试之前内核已被污染,则该函数将生成TBROK。

然后,您可以在run()期间调用 tst_taint_check(),它返回0或如先前指定的在/proc/sys/kernel/tainted 中设置的污染标志。

根据您的内核版本,并非所有污点标志都将受支持。

有关污染内核的参考,请参阅内核文档:

Documentation / admin-guide / tainted-kernels.rst 或 https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html

2.2.25 Checksums / 校验和

LTP支持CRC32c校验和生成。为了使用它,测试应包括“ tst_checksum.h”标头,然后可以调用tst_crc32c()。

2.2.26 检查内核的驱动程序支持

某些测试可能需要特定的内核驱动程序,这些内核驱动程序可以编译或作为模块构建。如果.need_drivers指向内核模块名称的以NULL结尾的数组,则将全部检查所有这些,并在第一个缺少的驱动程序上使用TCONF退出测试。

由于它依赖于modprobe命令,因此如果命令本身在系统上不可用,则将跳过该检查。

2.2.27 保存和还原 /proc | sys 值

可以指示LTP库保存和恢复指定(/ proc | sys)文件的值。这是通过初始化tst_test struct字段save_restore来实现的。它是一个以NULL终止的字符串数组,其中每个字符串代表一个文件,其值在测试开始时保存并在测试结束时恢复。仅保存和还原指定文件的第一行。

路径名可以选择前缀,以指定(在store期间)错误处理的严格程度 :

  • (无前缀)-如果文件不存在,则测试以TCONF结尾

  • ? -如果文件不存在或打开/读取失败,测试将打印信息消息并继续

  • ! -如果文件不存在,则测试以TBROK结尾

restore 恢复总是严格的,如果遇到任何错误,将返回TWARN。

static const char *save_restore[] = {
	"/proc/sys/kernel/core_pattern",
	NULL,
};

static void setup(void)
{
	FILE_PRINTF("/proc/sys/kernel/core_pattern", "/mypath");
}

static struct tst_test test = {
	...
	.setup = setup,
	.save_restore = save_restore,
};

2.2.28 解析内核.config

通常,测试用例应尝试根据当前正在运行的内核来自动检测尽可能多的内核功能。 我们确实有tst_check_driver()来检查系统上是否存在可以作为内核模块编译的功能,可以通过检查ENOSYS errno等来检测禁用的系统调用。

但是,在极少数情况下,无法基于内核用户空间API检测到核心内核功能,因此我们不得不求助于.config解析。

对于这种情况,测试应设置测试所需的NULL终止的内核配置选项的 needs_kconfig 数组。 config选项可以指定为简单的              “CONFIG_FOO”,在这种情况下,只要将其设置为任何值(通常为= y或= m),就足以继续进行测试。

或者使用“ CONFIG_FOO = bar”作为值,在这种情况下,该值也必须匹配。 如果未设置任何必需的选项,则使用TCONF中止测试。

#include "tst_test.h"

static const char *kconfigs[] = {
	"CONFIG_X86_INTEL_UMIP",
	NULL
};

static struct tst_test test = {
	...
	.needs_kconfigs = kconfigs,
	...
};

2.2.29 在测试执行期间更改 Wall Clock 时间

由于不同的原因,有些测试可能需要更改系统范围的时钟时间。 无论何时发生这种情况,都必须在测试执行结束时恢复时钟,并考虑到该测试过程中经过的时间。

为了实现这一点,struct tst_test有一个名为“ restore_wallclock”的变量,应将其设置为“ 1”,以便LTP知道它应该:(1)在测试设置阶段,初始化 monotonic clock 单调时钟(2)在测试清理阶段,使用该 monotonic clock 单调时钟 修复 system-wide clock系统范围的时钟时间。

#include "tst_test.h"

static void setup(void)
{
	...
}

static void run(void)
{
	...
}

struct tst_test test = {
	...
	.setup = setup,
	.test_all = run,
	.restore_wallclock = 1,
	...
};

2.2.30 Testing similar syscalls in one test / 在一项测试中测试类似的系统调用

在某些情况下,内核有几个非常相似的系统调用,它们可以执行相同或非常相似的工作。 在我们通常有两个或三个syscall版本的i386上,这最为明显。 这是因为i386是Linux开发的第一个平台,并且因为API的大多数错误也发生在那里。 但是,这根本不限于i386,在第二版系统调用添加了丢失标志参数也很常见。

在这种情况下,一遍又一遍地复制并粘贴测试代码没有多大意义,而不是说测试库提供了对测试变体的支持。 测试变量背后的想法很简单,我们每次使用不同的syscall变量运行几次测试。

该实现由test_variant整数组成,如果设置了该整数,则表示测试变量的数量。 然后对测试进行fork,并使用全局tst_variant变量中的不同值。

#include "tst_test.h"

static int do_foo(void)
{
	switch (tst_variant) {
	case 0:
		return foo();
	case 1:
		return syscall(__NR_foo);
	}

	return -1;
}

static void run(void)
{
	...

	TEST(do_foo);

	...
}

static void setup(void)
{
	switch (tst_variant) {
	case 0:
		tst_res(TINFO, "Testing foo variant 1");
	break;
	case 1:
		tst_res(TINFO, "Testing foo variant 2");
	break;
	}
}

struct tst_test test = {
	...
	.setup = setup,
	.test_all = run,
	.test_variants = 2,
	...
};

2.2.31 受保护的缓冲区

测试库支持受保护的缓冲区,它们是分配的缓冲区,以便:

  • 缓冲区的末尾是PROT_NONE页
  • 在缓冲区中填充 random canary data 之前的页面的其余部分

这意味着缓冲区之后的任何访问都将产生 Segmentation fault 分段错误 或 EFAULT,这取决于访问分别发生在用户空间还是内核中。 缓冲区前面的 canary (栈保护)还将捕获缓冲区外部的任何写访问。

修补程序的目的是捕获将缓冲区和结构传递给syscall时发生的单一错误。 新的测试应该为所有通过指针传递给测试的系统调用的数据分配受保护的缓冲区。

#include "tst_test.h"

static struct foo *foo_ptr;
static struct iovec *iov;
static void *buf_ptr;
static char *id;
...

static void run(void)
{
	...

	foo_ptr->bar = 1;
	foo_ptr->buf = buf_ptr;

	...
}

static void setup(void)
{
	...

	id = tst_strdup(string);

	...
}

static struct tst_test test = {
	...
	.bufs = (struct tst_buffers []) {
		{&foo_ptr, .size = sizeof(*foo_ptr)},
		{&buf_ptr, .size = BUF_SIZE},
		{&iov, .iov_sizes = (int[]){128, 32, -1},
		{}
	}
};

可以在运行时通过tst_alloc()或tst_strdup()以及在tst_test结构中填充.bufs数组,在测试setup()中分配受保护的缓冲区。

到目前为止,tst_test结构支持通过设置大小或struct iovec来分配普通缓冲区,而struct iovec则以-1终止的缓冲区大小数组来递归分配,包括各个缓冲区。

2.2.32 添加和删除功能

一些测试可能需要存在或不存在特定功能。 使用tst_capability.h提供的API,测试作者可以尝试确保在测试过程中某些功能存在或不存在。

例如; 下面我们尝试创建一个原始套接字,它需要CAP_NET_ADMIN。 在设置过程中,我们应该能够做到,而在运行过程中,这应该是不可能的。 LTP功能库将在安装前检查我们是否具有此功能,然后在安装后将其删除。

#include "tst_test.h"
#include "tst_capability.h"
#include "tst_safe_net.h"

#include "lapi/socket.h"

static void run(void)
{
	TEST(socket(AF_INET, SOCK_RAW, 1));
	if (TST_RET > -1) {
		tst_res(TFAIL, "Created raw socket");
	} else if (TST_ERR != EPERM) {
		tst_res(TBROK | TTERRNO,
			"Failed to create socket for wrong reason");
	} else {
		tst_res(TPASS | TTERRNO, "Didn't create raw socket");
	}
}

static void setup(void)
{
	TEST(socket(AF_INET, SOCK_RAW, 1));
	if (TST_RET < 0)
		tst_brk(TCONF | TTERRNO, "We don't have CAP_NET_RAW to begin with");

	SAFE_CLOSE(TST_RET);
}

static struct tst_test test = {
	.setup = setup,
	.test_all = run,
	.caps = (struct tst_cap []) {
		TST_CAP(TST_CAP_REQ, CAP_NET_RAW),
		TST_CAP(TST_CAP_DROP, CAP_NET_RAW),
		{}
	},
};

查看底部的测试结构。 我们在caps字段中填充了一个包含两个tst_cap结构的NULL终止数组。 在设置之前执行TST_CAP_REQ操作,在设置之后执行TST_CAP_DROP。 这意味着可以请求和删除功能。

static struct tst_test test = {
	.test_all = run,
	.caps = (struct tst_cap []) {
		TST_CAP(TST_CAP_REQ, CAP_NET_RAW),
		TST_CAP(TST_CAP_DROP, CAP_SYS_ADMIN),
		{}
	},
};

在这里,我们请求CAP_NET_RAW,但删除CAP_SYS_ADMIN。 如果功能在允许的集合中,但不在有效的集合中,则库将尝试允许它。 如果不在允许的范围内,则它将失败并显示TCONF。

该API不需要安装libcap。 但是,相对于libcap,它的功能有限。 它仅尝试从有效集中添加或删除功能。 这意味着需要产生子进程的测试可能很难确保子级可以使用正确的功能(请参阅功能(7)手册页)。

但是,直接使用tst_cap_action(struct tst_cap * cap)可以解决很多问题,可以随时调用。 如果您希望在安装开始时放弃功能,这也将有所帮助。

2.2.33 Reproducing race-conditions / 重现竞赛条件

如果错误是由 kernel racing 内核竞赛 中的 两个tasks任务引起的,并且您希望创建回归测试(或错误修复验证测试),则应使用tst_fuzzy_sync.h库。

它允许您在代码中指定两个race竞赛窗口。 每个线程循环中只有一个窗口(触发竞赛通常需要多次迭代)。 这些窗口显示race竞赛可能发生的地方的模糊同步。 它们不需要很精确,因此是模糊的部分。 如果没有立即触发竞赛条件,则库将开始尝试不同的计时。

#include "tst_fuzzy_sync.h"

static struct tst_fzsync_pair fzsync_pair;

static void setup(void)
{
        tst_fzsync_pair_init(&fzsync_pair);
}

static void cleanup(void)
{
	tst_fzsync_pair_cleanup(&fzsync_pair);
}

static void *thread_b(void *arg)
{
	while (tst_fzsync_run_b(&fzsync_pair)) {

		tst_fzsync_start_race_b(&fzsync_pair);

                /* This is the race window for thread B */

                tst_fzsync_end_race_b(&fzsync_pair);
	}

	return arg;
}

static void thread_a(void)
{
	tst_fzsync_pair_reset(&fzsync_pair, thread_b);

        while (tst_fzsync_run_a(&fzsync_pair)) {

		tst_fzsync_start_race_a(&fzsync_pair);

		/* This is the race window for thread A */

                tst_fzsync_end_race_a(&fzsync_pair);
	}
}

static struct tst_test test = {
	.test_all = thread_a,
	.setup = setup,
	.cleanup = cleanup,
};

上面是使用模糊同步进行测试的最小模板。 在简单的情况下,您只需要将要race的位放在start_race和end_race之间即可。 同时,您需要进行迭代的任何设置都超出了窗口范围。

模糊同步使充当障碍的run_a和run_b同步,因此任何一个线程都无法继续进行,直到另一个线程追上了它。 还有pair_wait函数,可用于在其他位置添加障碍。 当然 start/end_race_a/b 也是一个障碍。

该库根据用户指定的超时时间以及一些其他启发式方法来决定测试应运行多长时间。

有关完整的文档,请参见include / tst_fuzzy_sync.h中的注释。

2.3 在shell中编写测试用例

LTP也支持将测试用例编写在portable shell中。

在testcases/lib/tst_test.sh中有一个与C接口紧密建模的shell库。

Warning

所有以TST_或tst_开头的标识符都被保留为测试库。

2.3.1 基本测试接口

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# This is a basic test for true shell builtin

TST_TESTFUNC=do_test
. tst_test.sh

do_test()
{
	true
	ret=$?

	if [ $ret -eq 0 ]; then
		tst_res TPASS "true returned 0"
	else
		tst_res TFAIL "true returned $ret"
	fi
}

tst_run

Tip

要执行此测试,tst_test.sh库必须位于$ PATH中。 如果您是从git checkout执行测试,则可以将其作为

PATH =“ $ PATH:../../ lib” ./foo01.sh运行

shell程序库需要 $TST_SETUP,$TST_CLEANUP 和 $TST_TESTFUNC 变量中的测试安装程序,清理程序和执行测试的测试函数。

$TST_SETUP 和 $TST_CLEANUP 都是可选的。

如果通过将正确的命令行选项传递给测试来请求多次测试迭代,则可能多次调用 $TST_TESTFUNC。

$TST_CLEANUP 甚至可以在设置过程中被调用,并且即使在这种情况下也必须能够正确清理。 最简单的解决方案是跟踪已初始化的内容,并在清理中采取相应的措施。

Warning

与C库类似,在 $TST_CLEANUP中调用 tst_brk()不会退出测试,并且TBROK 被转换为 TWARN。

还要注意在测试结束时调用的tst_run函数实际上开始了测试。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Example test with tests in separate functions

TST_TESTFUNC=test
TST_CNT=2
. tst_test.sh

test1()
{
	tst_res TPASS "Test $1 passed"
}

test2()
{
	tst_res TPASS "Test $1 passed"
}

tst_run


# output:
# foo 1 TPASS: Test 1 passed
# foo 2 TPASS: Test 2 passed

如果设置了$ TST_CNT,则测试库将检查是否存在名为$ {TST_TESTFUNC} 1,…,$ {TST_TESTFUNC} $ {TST_CNT}的函数,如果找到这些函数,它们将被逐一执行。 测试编号在$ 1中传递给它。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Example test with tests in a single function

TST_TESTFUNC=do_test
TST_CNT=2
. tst_test.sh

do_test()
{
	case $1 in
	1) tst_res TPASS "Test $1 passed";;
	2) tst_res TPASS "Test $1 passed";;
	esac
}

tst_run


# output:
# foo 1 TPASS: Test 1 passed
# foo 2 TPASS: Test 2 passed

否则,如果设置了 $TST_CNT 但没有$ {TST_TESTFUNC} 1,$TST_TESTFUNC 则将被执行 $TST_CNT次,并且测试编号在$1中传递给它。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Example test with tests in a single function, using $TST_TEST_DATA and
# $TST_TEST_DATA_IFS

TST_TESTFUNC=do_test
TST_TEST_DATA="foo:bar:d dd"
TST_TEST_DATA_IFS=":"
. tst_test.sh

do_test()
{
	tst_res TPASS "Test $1 passed with data '$2'"
}

tst_run


# output:
# foo 1 TPASS: Test 1 passed with data 'foo'
# foo 2 TPASS: Test 1 passed with data 'bar'
# foo 3 TPASS: Test 1 passed with data 'd dd'

可以通过 $TST_TEST_DATA 传递功能数据。 可以设置 $TST_TEST_DATA_IFS 的值 用于拆分传递的数据,默认值为空格。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Example test with tests in a single function, using $TST_TEST_DATA and $TST_CNT

TST_TESTFUNC=do_test
TST_CNT=2
TST_TEST_DATA="foo bar"
. tst_test.sh

do_test()
{
	case $1 in
	1) tst_res TPASS "Test $1 passed with data '$2'";;
	2) tst_res TPASS "Test $1 passed with data '$2'";;
	esac
}

tst_run


# output:
# foo 1 TPASS: Test 1 passed with data 'foo'
# foo 2 TPASS: Test 2 passed with data 'foo'
# foo 3 TPASS: Test 1 passed with data 'bar'
# foo 4 TPASS: Test 2 passed with data 'bar'

$TST_TEST_DATA 可与 $TST_CNT 一起使用。 如果未指定 $TST_TEST_DATA_IFS,则使用空格作为默认值。 当然,可以使用分离的功能。

2.3.2 Shell的库环境变量

与C库类似,可以简单的准备,只需通过设置正确的 $TST_NEEDS_FOO。

Variable name Action done

TST_NEEDS_ROOT

除非以root身份执行,否则以TCONF退出测试

TST_NEEDS_TMPDIR

创建测试临时目录并cd进入该目录。

TST_NEEDS_DEVICE

准备测试临时设备,测试设备的路径存储在$ TST_DEVICE变量中。 该选项暗含TST_NEEDS_TMPDIR。

TST_NEEDS_CMDS

带有命令名称的字符串,该名称必须用于测试(请参见下文)。

TST_NEEDS_MODULE

测试所需的测试模块名称(请参见下文)。

TST_NEEDS_DRIVERS

检查内核驱动程序对测试的支持。

TST_TIMEOUT

为测试设置的最大超时时间(以秒为单位)。 必须为int> = 1或-1(禁用超时的特殊值),默认值为300。变量应在测试中设置,而不是由用户设置。 相当于C中的tst_test.timeout。

检查命令是否存在

#!/bin/sh

...

TST_NEEDS_CMDS="modinfo modprobe"
. tst_test.sh

...

将 $TST_NEEDS_CMDS 设置为监听所需命令的字符串,其将检查每个命令是否存在,并在第一个缺少命令时使用TCONF退出测试。

或者,可以使用tst_require_cmds()函数在运行时执行相同的操作,因为有时我们也需要在运行时进行检查。

tst_check_cmds()只能用于特定测试,因为它不会退出(它发出tst_res TCONF)。 预期的用法是:…

TST_TESTFUNC=do_test . tst_test.sh

do_test() { tst_check_cmds cmd || return cmd --foo …​}

tst_run …​

定位内核模块

LTP构建系统也可以构建内核模块,将 $TST_NEEDS_MODULE 设置为模块名称将导致库在一些可能的路径中查找模块。

如果找到模块,则将其路径存储在$ TST_MODPATH变量中;如果未找到模块,则测试将以TCONF退出。

2.3.3 可选命令行参数

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Optional test command line parameters

TST_OPTS="af:"
TST_USAGE=usage
TST_PARSE_ARGS=parse_args
TST_TESTFUNC=do_test

. tst_test.sh

ALTERNATIVE=0
MODE="foo"

usage()
{
	cat << EOF
usage: $0 [-a] [-f <foo|bar>]

OPTIONS
-a     Enable support for alternative foo
-f     Specify foo or bar mode
EOF
}

parse_args()
{
	case $1 in
	a) ALTERNATIVE=1;;
	f) MODE="$2";;
	esac
}

do_test()
{
	...
}

tst_run

可选参数的getopts字符串在 $TST_OPTS 变量中传递。 有一些默认参数不能被测试使用,可以通过将help -h选项传递给任何测试来列出这些默认参数。

打印用法的函数在 $TST_USAGE 中传递,打印用法时将附加库中实现的选项的帮助。

最后,调用 $PARSE_ARGS 函数,其选项名称位于 $1 中,如果option具有参数,则其值位于 $2 中。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Optional test positional parameters

TST_POS_ARGS=3
TST_USAGE=usage
TST_TESTFUNC=do_test

. tst_test.sh

usage()
{
	cat << EOF
usage: $0 [min] [max] [size]

EOF
}

min="$1"
max="$2"
size="$3"

do_test()
{
	...
}

tst_run

您还可以通过设置$ TST_POS_ARGS变量来请求多个位置参数。 如果这样做的话,这些将直接在$ 1,$ 2,…,$ n中传递给脚本。

2.3.4 有用的库功能

检索配置变量

您可能需要检索诸如 PAGESIZE之类的配置值,这里有 getconf,但是由于某些系统可能没有,因此建议您改用 tst_getconf。 注意,它实现了仅由测试用例使用的 getconf系统变量的子集。

# retrieve PAGESIZE
pagesize=`tst_getconf PAGESIZE`

睡眠间隔 – subsecond 亚秒级

尽管基本上到处都有一个sleep命令,但并非所有实现都可以支持少于一秒钟的睡眠。 而且大多数时间睡一秒钟都太多了。 因此,LTP包含tst_sleep,它可以休眠规定的秒,毫秒或微秒量。

# sleep for 100 milliseconds
tst_sleep 100ms

多次重运行函数在有限的时间内

有时LTP测试需要多次重试功能才能获得成功。 如果函数的返回值不符合我们的预期,则可以通过保持重试来实现此目的。 超过限定时间后,测试将立即从重试中中断。 时间限制乘以LTP_TIMEOUT_MUL。

# retry function in 1 second
TST_RETRY_FUNC(FUNC, EXPECTED_RET)

# retry function in N second
TST_RETRY_FN_EXP_BACKOFF(FUNC, EXPECTED_RET, N)
# retry function in 1 second
TST_RETRY_FUNC "FUNC arg1 arg2 ..." "EXPECTED_RET"

# retry function in N second
TST_RETRY_FN_EXP_BACKOFF "FUNC arg1 arg2 ..." "EXPECTED_RET" "N"

检查整数

# returns zero if passed an integer parameter, non-zero otherwise
tst_is_int "$FOO"

检查整数和浮点数

# returns zero if passed an integer or floating point number parameter,
# non-zero otherwise
tst_is_num "$FOO"

获取随机数

portable shell 中没有$ RANDOM,请改用tst_random。

# get random integer between 0 and 1000 (including 0 and 1000)
tst_random 0 1000

使用文件系统格式化设备

tst_mkfs帮助程序将使用文件系统格式化设备。

# format test device with ext2
tst_mkfs ext2 $TST_DEVICE
# default params are $TST_FS_TYPE $TST_DEVICE
tst_mkfs
# optional parameters
tst_mkfs ext4 /dev/device -T largefile

挂载和卸载文件系统

tst_moun t和 tst_umount 帮助器是 安装/卸载 文件系统的安全方法。

tst_mount 将$TST_FS_TYPE 的 $TST_DEVICE(可选)安装到 $TST_MNTPOINT(默认为mntpoint),可以选择使用            $TST_MNT_PARAMS。 如果$TST_MNTPOINT目录在函数调用之前不存在,则会创建该目录。

如果未安装传递到tst_umount 的路径(可选,默认为 $TST_DEVICE),则该路径没有安装(在/proc/mounts中)。 否则,它会在发生故障时重试几次挂载文件系统,这是一种解决方法,因为有一个守护程序愚蠢到足以探测所有新挂载的文件系统,从而阻止了它们在挂载后不久挂载。

ROD和ROD_SILENT

这些函数提供了C中使用的SAFE_MACROS,尽管它们可以工作并且名称不同。

ROD_SILENT command arg1 arg2 ...

# is shorthand for:

command arg1 arg2 ... > /dev/null 2>&1
if [ $? -ne 0 ]; then
        tst_brkm TBROK "..."
fi


ROD command arg1 arg2 ...

# is shorthand for:

ROD arg1 arg2 ...
if [ $? -ne 0 ]; then
        tst_brkm TBROK "..."
fi

Warning

请记住,输出重定向(到文件)是在调用方而不是ROD函数中发生的,并且ROD函数无法检查是否存在写入错误。

事实上,执行ROD echo可以使> / proc / cpuinfo正常工作,因为ROD函数只会使echo正常运行。

# Redirect output to a file with ROD
ROD echo foo \> bar

请注意,> 会用 \ 进行转义,这会导致 > 和 文件名 作为参数传递到ROD函数,并且ROD函数包含用于拆分$ @ on>并将其重定向到文件的代码。

EXPECT_PASS{,_BRK} and EXPECT_FAIL{,_BRK}

EXPECT_PASS command arg1 arg2 ... [ \> file ]
EXPECT_FAIL command arg1 arg2 ... [ \> file ]

如果命令以0退出码退出,则EXPECT_PASS 调用 tst_resm TPASS,否则调用 tst_resm TFAIL。 EXPECT_FAIL反之亦然。

输出重定向规则与ROD功能相同。 除此之外,EXPECT_FAIL 始终将命令的 stderr 重定向到 /dev/null。

还有EXPECT_PASS_BRK 和 EXPECT_FAIL_BRK,它们的工作方式相同,只是在发生意外动作时中断测试。

可以检测是否发生了预期值:

if ! EXPECT_PASS command arg1 2\> /dev/null; then
	continue
fi

tst_kvcmp

该命令将给定条件下当前正在运行的内核版本与类似于shell test命令的语法进行比较。

# Exit the test if kernel version is older or equal to 2.6.8
if tst_kvcmp -le 2.6.8; then
	tst_brk TCONF "Kernel newer than 2.6.8 is needed"
fi

# Exit the test if kernel is newer than 3.8 and older than 4.0.1
if tst_kvcmp -gt 3.8 -a -lt 4.0.1; then
	tst_brk TCONF "Kernel must be older than 3.8 or newer than 4.0.1"
fi
expression description

-eq kver

Returns true if kernel version is equal

-ne kver

Returns true if kernel version is not equal

-gt kver

Returns true if kernel version is greater

-ge kver

Returns true if kernel version is greater or equal

-lt kver

Returns true if kernel version is lesser

-le kver

Returns true if kernel version is lesser or equal

-a

Does logical and between two expressions

-o

Does logical or between two expressions

内核版本的格式必须是一个点,例如 2.6 或带有两个点 4.8.1。

tst_fs_has_free

#!/bin/sh

...

# whether current directory has 100MB free space at least.
if ! tst_fs_has_free . 100MB; then
	tst_brkm TCONF "Not enough free space"
fi

...

如果满足指定的可用空间,则tst_fs_has_free Shell接口将返回0,否则将返回1,并且在出错时将返回2。

第二个参数支持后缀kB,MB和GB,默认单位为Byte。

tst_retry

#!/bin/sh

...

# Retry ping command three times
tst_retry "ping -c 1 127.0.0.1"

if [ $? -ne 0 ]; then
	tst_resm TFAIL "Failed to ping 127.0.0.1"
else
	tst_resm TPASS "Successfully pinged 127.0.0.1"
fi

...

使用tst_retry函数,您可以在等待一小段时间后重试命令,直到成功或达到指定的重试次数为止(默认为三次尝试)。

2.3.5重新启动守护程序

由于两个原因,重新启动系统守护程序是一项复杂的任务。

有不同的初始化系统(SysV初始化,systemd等)

守护程序名称在发行版之间不统一(apache与httpd,cron与crond,各种syslog版本)

为了解决这些问题,LTP提供了testcases / lib / daemonlib.sh库,该库提供了启动/停止/查询守护程序的功能以及存储正确守护程序名称的变量。

Table 1. Supported operations / 支持的操作

start_daemon()

启动守护程序,将名称作为第一个参数传递

stop_daemon()

停止守护程序,将名称作为第一个参数传递。

restart_daemon()

重新启动守护程序,将名称作为第一个参数传递。

status_daemon()

检测守护程序状态(退出代码:0:正在运行,1:未运行)。

Table 2. Variables with detected names / 具有检测到的名称的变量

CROND_DAEMON

Cron守护程序名称(cron,crond)

SYSLOG_DAEMON

Syslog守护程序名称(syslog,syslog-ng,rsyslog)。

Cron守护程序重新启动示例

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Cron daemon restart example

TCID=cron01
TST_COUNT=1
. test.sh
. daemonlib.sh

...

restart_daemon $CROND_DAEMON

...

tst_exit

2.3.6访问检查点界面

shell 程序库提供了与C版本兼容的检查点接口的实现。 所有TST_CHECKPOINT_ *函数均可用。

为了初始化检查点,必须在包含test.sh之前将$ TST_NEEDS_CHECKPOINTS设置为1:

#!/bin/sh

TST_NEEDS_CHECKPOINTS=1
. test.sh

由于两种实现都兼容,因此还可以从Shell测试中启动子二进制进程并与之同步。 此过程必须具有通过调用tst_reinit()初始化的检查点。

 

3. 常见问题

本章将介绍UNIX接口中的常见问题/误用和不太明显的设计模式(quirks),请仔细阅读。

3.1 umask()

我已经提及这一个好几遍了…当你创建一个文件open() 或 creat() 等,指定为最后一个参数的模式不是创建文件的模式。该模式取决于当前的umask() 设置,该设置可能会清除某些位。如果测试取决于特定的文件权限,则需要将umask更改为0或之后将文件chmod()更改,或使用为您执行chmod()的 SAFE_TOUCH()。

3.2 access()

如果由root执行access(some_file,W_OK),即使该文件未设置写许可权位,它也将返回成功(R_OK也是如此)。对于sysfs文件,您可以使用open()作为检查文件读写权限的解决方法。它可能不适用于其他文件系统,因为这些文件系统必须使用stat(),lstat()或fstat()。

3.3 umount()繁忙

各种桌面守护程序(为此而闻名的gvfsd-trash)可能足够愚蠢,无法探测所有新安装的文件系统,从而导致umount(2)失败并导致EBUSY;使用2.2.19中描述的tst_umount()在这种情况下重试,而不是普通的umount(2)。

3.4 文件缓冲区和fork()

有所不同的是,如果某个进程调用fork(2),则子进程会继承打开描述符以及父内存的副本。特别强调的是,如果有任何打开的FILE缓冲区中有数据,则它们的父级和子级都可能写入它们,从而导致结果文件中的数据损坏/重复。

另外,打开的FILE流也将在exit(3)处刷新并关闭,因此,如果您的程序可以使用FILE流,则fork(2)也可以,并且子级可能最终调用exit(3),那么您可能最终会损坏文件。

解决此问题的方法是简单地调用fflush(NULL),在执行fork(2)之前刷新所有打开的输出FILE流。您也可以在子进程中使用 _exit(2),该子进程不会刷新FILE缓冲区,并且还会跳过atexit(3)回调。

 

4. 测试贡献清单

  1. 测试编译并运行正常(也可以使用 -i 10 进行检查)

  2. Checkpatch不报告任何错误

  3. 完整的运行测试

  4. 测试文件被添加到相应的.gitignore文件中

  5. 补丁适用于最新的git

4.1 关于.gitignore文件

LTP树中有许多.gitignore文件。通常,每组测试都有一个 .gitignore文件。进行此设置的原因很简单。在每个目录中测试使得维护.gitignore文件要容易得多,而不是在项目根目录中只有一个文件。这样,我们不必在移动目录时更新所有gitignore文件,并且在删除带有测试的目录时,它们会自动删除。