atf, kyua: Implement require.kmods.

This adds a metadata variable, require.kmods, and corresponding functions
or methods in C, C++, and shell, which allow a test to specify that it
requires particular kernel modules to run.  If the kernel modules are not
present, the test is skipped.  One might want to consider a kyua option
which makes it attempt to load the modules instead.

Differential Revision:	https://reviews.freebsd.org/D47470

(cherry picked from commit 83a1ee578c9d1ab7013e997289c7cd470c0e6902)
This commit is contained in:
Dag-Erling Smørgrav 2025-05-31 14:27:30 +02:00 committed by Franco Fichtner
parent f3f36d3fc1
commit 84a5cebd36
18 changed files with 214 additions and 17 deletions

View file

@ -22,7 +22,7 @@
.\" IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
.\" OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
.\" IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.Dd March 6, 2017
.Dd May 11, 2025
.Dt ATF-C++ 3
.Os
.Sh NAME
@ -277,6 +277,14 @@ It is possible to get the path to the test case's source directory from any
of its three components by querying the
.Sq srcdir
configuration variable.
.Ss Requiring kernel modules
Aside from the
.Va require.kmods
meta-data variable available in the header only, one can also check for
additional kernel modules in the test case's body by using the
.Fn require_kmod
function, which takes the name of a single module.
If it is not found, the test case will be automatically skipped.
.Ss Requiring programs
Aside from the
.Va require.progs

View file

@ -318,6 +318,13 @@ impl::tc::cleanup(void)
{
}
void
impl::tc::require_kmod(const std::string& kmod)
const
{
atf_tc_require_kmod(kmod.c_str());
}
void
impl::tc::require_prog(const std::string& prog)
const

View file

@ -80,6 +80,7 @@ protected:
virtual void body(void) const = 0;
virtual void cleanup(void) const;
void require_kmod(const std::string&) const;
void require_prog(const std::string&) const;
friend struct tc_impl;

View file

@ -22,7 +22,7 @@
.\" IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
.\" OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
.\" IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.Dd February 23, 2021
.Dd May 11, 2025
.Dt ATF-C 3
.Os
.Sh NAME
@ -77,6 +77,8 @@
.Nm atf_tc_fail ,
.Nm atf_tc_fail_nonfatal ,
.Nm atf_tc_pass ,
.Nm atf_tc_require_kmod ,
.Nm atf_tc_require_prog ,
.Nm atf_tc_skip ,
.Nm atf_utils_cat_file ,
.Nm atf_utils_compare_file ,
@ -145,6 +147,8 @@
.Fn atf_tc_fail "reason"
.Fn atf_tc_fail_nonfatal "reason"
.Fn atf_tc_pass
.Fn atf_tc_require_kmod "kmod"
.Fn atf_tc_require_prog "prog"
.Fn atf_tc_skip "reason"
.Ft void
.Fo atf_utils_cat_file
@ -362,6 +366,14 @@ It is possible to get the path to the test case's source directory from any
of its three components by querying the
.Sq srcdir
configuration variable.
.Ss Requiring kernel modules
Aside from the
.Va require.kmods
meta-data variable available in the header only, one can also check for
additional kernel modules in the test case's body by using the
.Fn atf_tc_require_kmod
function, which takes the name of a single kernel module.
If it is not found, the test case will be automatically skipped.
.Ss Requiring programs
Aside from the
.Va require.progs

View file

@ -26,6 +26,10 @@
#include "atf-c/tc.h"
#include <sys/types.h>
#ifdef __FreeBSD__
#include <sys/linker.h>
#include <sys/module.h>
#endif
#include <sys/stat.h>
#include <sys/uio.h>
@ -103,6 +107,9 @@ static void format_reason_fmt(atf_dynstr_t *, const char *, const size_t,
static void errno_test(struct context *, const char *, const size_t,
const int, const char *, const bool,
void (*)(struct context *, atf_dynstr_t *));
#ifdef __FreeBSD__
static atf_error_t check_kmod(struct context *, const char *);
#endif
static atf_error_t check_prog_in_dir(const char *, void *);
static atf_error_t check_prog(struct context *, const char *);
@ -460,6 +467,39 @@ errno_test(struct context *ctx, const char *file, const size_t line,
}
}
#ifdef __FreeBSD__
static atf_error_t
check_kmod(struct context *ctx, const char *kmod)
{
struct kld_file_stat fstat = { .version = sizeof(fstat) };
struct module_stat mstat = { .version = sizeof(mstat) };
atf_dynstr_t reason;
size_t len = strlen(kmod);
int fid, mid;
for (fid = kldnext(0); fid > 0; fid = kldnext(fid)) {
if (kldstat(fid, &fstat) != 0)
continue;
if (strcmp(fstat.name, kmod) == 0)
goto done;
if (strncmp(fstat.name, kmod, len) == 0 &&
strcmp(fstat.name + len, ".ko") == 0)
goto done;
for (mid = kldfirstmod(fid); mid > 0; mid = modfnext(mid)) {
if (modstat(mid, &mstat) != 0)
continue;
if (strcmp(mstat.name, kmod) == 0)
goto done;
}
}
format_reason_fmt(&reason, NULL, 0, "The required kmod %s "
"is not loaded", kmod);
fail_requirement(ctx, &reason);
done:
return atf_no_error();
}
#endif
struct prog_found_pair {
const char *prog;
bool found;
@ -829,6 +869,9 @@ static void _atf_tc_fail_check(struct context *, const char *, const size_t,
static void _atf_tc_fail_requirement(struct context *, const char *,
const size_t, const char *, va_list) ATF_DEFS_ATTRIBUTE_NORETURN;
static void _atf_tc_pass(struct context *) ATF_DEFS_ATTRIBUTE_NORETURN;
#ifdef __FreeBSD__
static void _atf_tc_require_kmod(struct context *, const char *);
#endif
static void _atf_tc_require_prog(struct context *, const char *);
static void _atf_tc_skip(struct context *, const char *, va_list)
ATF_DEFS_ATTRIBUTE_NORETURN;
@ -908,6 +951,14 @@ _atf_tc_pass(struct context *ctx)
UNREACHABLE;
}
#ifdef __FreeBSD__
static void
_atf_tc_require_kmod(struct context *ctx, const char *kmod)
{
check_fatal_error(check_kmod(ctx, kmod));
}
#endif
static void
_atf_tc_require_prog(struct context *ctx, const char *prog)
{
@ -1154,6 +1205,16 @@ atf_tc_pass(void)
_atf_tc_pass(&Current);
}
#ifdef __FreeBSD__
void
atf_tc_require_kmod(const char *kmod)
{
PRE(Current.tc != NULL);
_atf_tc_require_kmod(&Current, kmod);
}
#endif
void
atf_tc_require_prog(const char *prog)
{

View file

@ -106,6 +106,9 @@ void atf_tc_fail_nonfatal(const char *, ...)
ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(1, 2);
void atf_tc_pass(void)
ATF_DEFS_ATTRIBUTE_NORETURN;
#ifdef __FreeBSD__
void atf_tc_require_kmod(const char *);
#endif
void atf_tc_require_prog(const char *);
void atf_tc_skip(const char *, ...)
ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(1, 2)

View file

@ -22,7 +22,7 @@
.\" IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
.\" OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
.\" IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.Dd January 27, 2021
.Dd May 11, 2025
.Dt ATF-SH 3
.Os
.Sh NAME
@ -43,6 +43,7 @@
.Nm atf_get_srcdir ,
.Nm atf_init_test_cases ,
.Nm atf_pass ,
.Nm atf_require_kmod ,
.Nm atf_require_prog ,
.Nm atf_set ,
.Nm atf_skip ,
@ -90,6 +91,8 @@
.Nm atf_init_test_cases
.Qq name
.Nm atf_pass
.Nm atf_require_kmod
.Qq kmod_name
.Nm atf_require_prog
.Qq prog_name
.Nm atf_set
@ -205,6 +208,14 @@ function.
It is interesting to note that this can be used inside
.Nm atf_init_test_cases
to silently include additional helper files from the source directory.
.Ss Requiring kernel modules
Aside from the
.Va require.kmods
meta-data variable available in the header only, one can also check for
additional kernel modules in the test case's body by using the
.Nm atf_require_kmod
function, which takes the name of a single kernel module.
If it is not found, the test case will be automatically skipped.
.Ss Requiring programs
Aside from the
.Va require.progs

View file

@ -353,6 +353,18 @@ atf_require_prog()
esac
}
#
# atf_require_kmod kmod
#
# Checks that the given kmod is loaded. If it is not, automatically
# skips the test case with an appropriate message.
#
atf_require_kmod()
{
kldstat -q "${1}" || \
atf_skip "The required kmod ${1} is not loaded"
}
#
# atf_set varname val1 [.. valN]
#

View file

@ -22,7 +22,7 @@
.\" IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
.\" OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
.\" IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.Dd March 6, 2017
.Dd May 11, 2025
.Dt ATF-TEST-CASE 4
.Os
.Sh NAME
@ -223,6 +223,14 @@ The value can have a size suffix such as
or
.Sq T
to make the amount of bytes easier to type and read.
.It require.kmods
Type: textual.
Optional.
.Pp
A whitespace separated list of kernel modules that must be present to
execute the test case.
If any of the required kernel modules is not found, the test case is
.Em skipped .
.It require.progs
Type: textual.
Optional.

View file

@ -133,6 +133,10 @@ engine::parse_atf_metadata(const model::properties_map& props)
mdbuilder.set_string("required_disk_space", value);
} else if (name == "require.files") {
mdbuilder.set_string("required_files", value);
#ifdef __FreeBSD__
} else if (name == "require.kmods") {
mdbuilder.set_string("required_kmods", value);
#endif
} else if (name == "require.machine") {
mdbuilder.set_string("allowed_platforms", value);
} else if (name == "require.memory") {

View file

@ -41,6 +41,10 @@
#include "utils/sanity.hpp"
#include "utils/units.hpp"
#ifdef __FreeBSD__
#include <libutil.h>
#endif
namespace config = utils::config;
namespace fs = utils::fs;
namespace passwd = utils::passwd;
@ -220,6 +224,26 @@ check_required_programs(const model::paths_set& required_programs)
}
#ifdef __FreeBSD__
/// Checks if all required kmods are loaded.
///
/// \param required_programs Set of kmods.
///
/// \return Empty if the required kmods are all loaded or an error
/// message otherwise.
static std::string
check_required_kmods(const model::strings_set& required_kmods)
{
for (model::strings_set::const_iterator iter = required_kmods.begin();
iter != required_kmods.end(); iter++) {
if (!kld_isloaded((*iter).c_str()))
return F("Required kmod '%s' not loaded") % *iter;
}
return "";
}
#endif
/// Checks if the current system has the specified amount of memory.
///
/// \param required_memory Amount of required physical memory, or zero if not
@ -312,6 +336,12 @@ engine::check_reqs(const model::metadata& md, const config::tree& cfg,
if (!reason.empty())
return reason;
#ifdef __FreeBSD__
reason = check_required_kmods(md.required_kmods());
if (!reason.empty())
return reason;
#endif
reason = check_required_memory(md.required_memory());
if (!reason.empty())
return reason;

View file

@ -256,6 +256,9 @@ init_tree(config::tree& tree)
tree.define< bytes_node >("required_disk_space");
tree.define< paths_set_node >("required_files");
tree.define< bytes_node >("required_memory");
#ifdef __FreeBSD__
tree.define< config::strings_set_node >("required_kmods");
#endif
tree.define< paths_set_node >("required_programs");
tree.define< user_node >("required_user");
tree.define< delta_node >("timeout");
@ -282,6 +285,9 @@ set_defaults(config::tree& tree)
tree.set< bytes_node >("required_disk_space", units::bytes(0));
tree.set< paths_set_node >("required_files", model::paths_set());
tree.set< bytes_node >("required_memory", units::bytes(0));
#ifdef __FreeBSD__
tree.set< config::strings_set_node >("required_kmods", model::strings_set());
#endif
tree.set< paths_set_node >("required_programs", model::paths_set());
tree.set< user_node >("required_user", "");
// TODO(jmmv): We shouldn't be setting a default timeout like this. See
@ -597,6 +603,22 @@ model::metadata::required_memory(void) const
}
#ifdef __FreeBSD__
/// Returns the list of kmods needed by the test.
///
/// \return Set of strings.
const model::strings_set&
model::metadata::required_kmods(void) const
{
if (_pimpl->props.is_set("required_kmods")) {
return _pimpl->props.lookup< config::strings_set_node >("required_kmods");
} else {
return get_defaults().lookup< config::strings_set_node >("required_kmods");
}
}
#endif
/// Returns the list of programs needed by the test.
///
/// \return Set of paths.

View file

@ -76,6 +76,9 @@ public:
const utils::units::bytes& required_disk_space(void) const;
const paths_set& required_files(void) const;
const utils::units::bytes& required_memory(void) const;
#ifdef __FreeBSD__
const strings_set& required_kmods(void) const;
#endif
const paths_set& required_programs(void) const;
const std::string& required_user(void) const;
const utils::datetime::delta& timeout(void) const;
@ -121,6 +124,9 @@ public:
metadata_builder& set_required_disk_space(const utils::units::bytes&);
metadata_builder& set_required_files(const paths_set&);
metadata_builder& set_required_memory(const utils::units::bytes&);
#ifdef __FreeBSD__
metadata_builder& set_required_kmods(const strings_set&);
#endif
metadata_builder& set_required_programs(const paths_set&);
metadata_builder& set_required_user(const std::string&);
metadata_builder& set_string(const std::string&, const std::string&);

View file

@ -119,6 +119,8 @@ MLINKS+= atf-c.3 ATF_CHECK.3 \
atf-c.3 atf_tc_fail.3 \
atf-c.3 atf_tc_fail_nonfatal.3 \
atf-c.3 atf_tc_pass.3 \
atf-c.3 atf_tc_require_kmod.3 \
atf-c.3 atf_tc_require_prog.3 \
atf-c.3 atf_tc_skip.3 \
atf-c.3 atf_utils_cat_file.3 \
atf-c.3 atf_utils_compare_file.3 \

View file

@ -54,6 +54,7 @@ MLINKS+= \
atf-sh.3 atf_get_srcdir.3 \
atf-sh.3 atf_init_test_cases.3 \
atf-sh.3 atf_pass.3 \
atf-sh.3 atf_require_kmod.3 \
atf-sh.3 atf_require_prog.3 \
atf-sh.3 atf_set.3 \
atf-sh.3 atf_skip.3 \

View file

@ -48,7 +48,6 @@ tarsum() {
}
tarfs_setup() {
kldload -n tarfs || atf_skip "This test requires tarfs and could not load it"
mkdir "${mnt}"
}
@ -60,6 +59,7 @@ atf_test_case tarfs_basic cleanup
tarfs_basic_head() {
atf_set "descr" "Basic function test"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_basic_body() {
tarfs_setup
@ -87,6 +87,7 @@ atf_test_case tarfs_basic_gnu cleanup
tarfs_basic_gnu_head() {
atf_set "descr" "Basic function test using GNU tar"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_basic_gnu_body() {
@ -101,6 +102,7 @@ atf_test_case tarfs_notdir_device cleanup
tarfs_notdir_device_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_notdir_device_body() {
tarfs_setup
@ -121,6 +123,7 @@ atf_test_case tarfs_notdir_device_gnu cleanup
tarfs_notdir_device_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_device_gnu_body() {
@ -135,6 +138,7 @@ atf_test_case tarfs_notdir_dot cleanup
tarfs_notdir_dot_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_notdir_dot_body() {
tarfs_setup
@ -155,6 +159,7 @@ atf_test_case tarfs_notdir_dot_gnu cleanup
tarfs_notdir_dot_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_dot_gnu_body() {
@ -169,6 +174,7 @@ atf_test_case tarfs_notdir_dotdot cleanup
tarfs_notdir_dotdot_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_notdir_dotdot_body() {
tarfs_setup
@ -189,6 +195,7 @@ atf_test_case tarfs_notdir_dotdot_gnu cleanup
tarfs_notdir_dotdot_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_dotdot_gnu_body() {
@ -203,6 +210,7 @@ atf_test_case tarfs_notdir_file cleanup
tarfs_notdir_file_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_notdir_file_body() {
tarfs_setup
@ -223,6 +231,7 @@ atf_test_case tarfs_notdir_file_gnu cleanup
tarfs_notdir_file_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_file_gnu_body() {
@ -237,6 +246,7 @@ atf_test_case tarfs_emptylink cleanup
tarfs_emptylink_head() {
atf_set "descr" "Regression test for PR 277360: empty link target"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_emptylink_body() {
tarfs_setup
@ -256,6 +266,7 @@ atf_test_case tarfs_linktodir cleanup
tarfs_linktodir_head() {
atf_set "descr" "Regression test for PR 277360: link to directory"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_linktodir_body() {
tarfs_setup
@ -276,6 +287,7 @@ atf_test_case tarfs_linktononexistent cleanup
tarfs_linktononexistent_head() {
atf_set "descr" "Regression test for PR 277360: link to nonexistent target"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_linktononexistent_body() {
tarfs_setup
@ -293,6 +305,7 @@ atf_test_case tarfs_checksum cleanup
tarfs_checksum_head() {
atf_set "descr" "Verify that the checksum covers header padding"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_checksum_body() {
tarfs_setup
@ -313,6 +326,7 @@ atf_test_case tarfs_long_names cleanup
tarfs_long_names_head() {
atf_set "descr" "Verify that tarfs supports long file names"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_long_names_body() {
tarfs_setup
@ -337,6 +351,7 @@ atf_test_case tarfs_long_paths cleanup
tarfs_long_paths_head() {
atf_set "descr" "Verify that tarfs supports long paths"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
}
tarfs_long_paths_body() {
tarfs_setup
@ -361,6 +376,7 @@ atf_test_case tarfs_git_archive cleanup
tarfs_git_archive_head() {
atf_set "descr" "Verify that tarfs supports archives created by git"
atf_set "require.user" "root"
atf_set "require.kmods" "tarfs"
atf_set "require.progs" "git"
}
tarfs_git_archive_body() {

View file

@ -34,6 +34,7 @@ wg_basic_head()
{
atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails'
atf_set require.user root
atf_set require.kmods if_wg
}
wg_basic_body()
@ -41,8 +42,6 @@ wg_basic_body()
local epair pri1 pri2 pub1 pub2 wg1 wg2
local endpoint1 endpoint2 tunnel1 tunnel2
kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
pri1=$(wg genkey)
pri2=$(wg genkey)
@ -175,6 +174,7 @@ wg_basic_netmap_head()
{
atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails with netmap'
atf_set require.user root
atf_set require.kmods if_wg netmap
}
wg_basic_netmap_body()
@ -183,9 +183,6 @@ wg_basic_netmap_body()
local endpoint1 endpoint2 tunnel1 tunnel2 tunnel3 tunnel4
local pid status
kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
kldload -n netmap || atf_skip "This test requires netmap and could not load it"
pri1=$(wg genkey)
pri2=$(wg genkey)
@ -268,6 +265,7 @@ wg_key_peerdev_shared_head()
{
atf_set descr 'Create a wg(4) interface with a shared pubkey between device and a peer'
atf_set require.user root
atf_set require.kmods if_wg
}
wg_key_peerdev_shared_body()
@ -275,8 +273,6 @@ wg_key_peerdev_shared_body()
local epair pri1 pub1 wg1
local endpoint1 tunnel1
kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
pri1=$(wg genkey)
endpoint1=192.168.2.1
@ -316,8 +312,6 @@ wg_key_peerdev_makeshared_body()
local epair pri1 pub1 pri2 wg1 wg2
local endpoint1 tunnel1
kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
pri1=$(wg genkey)
pri2=$(wg genkey)
@ -361,6 +355,7 @@ wg_vnet_parent_routing_head()
{
atf_set descr 'Create a wg(4) tunnel without epairs and pass traffic between jails'
atf_set require.user root
atf_set require.kmods if_wg
}
wg_vnet_parent_routing_body()
@ -368,8 +363,6 @@ wg_vnet_parent_routing_body()
local pri1 pri2 pub1 pub2 wg1 wg2
local tunnel1 tunnel2
kldload -n if_wg
pri1=$(wg genkey)
pri2=$(wg genkey)

View file

@ -14,7 +14,7 @@ KYUA_SRCDIR= ${SRCTOP}/contrib/kyua
PACKAGE= tests
PROG_CXX= kyua
SRCS= main.cpp
LIBADD= lutok sqlite3
LIBADD= lutok sqlite3 util
MAN= kyua-about.1 \
kyua-config.1 \