* [LTP] [PATCH v3 1/2] metadata: add tests grouping support
2026-06-22 8:53 [LTP] [PATCH v3 0/2] Support metadata groups Andrea Cervesato
@ 2026-06-22 8:53 ` Andrea Cervesato
2026-06-22 9:57 ` [LTP] " linuxtestproject.agent
2026-06-22 9:59 ` [LTP] [PATCH v3 1/2] " Cyril Hrubis
2026-06-22 8:53 ` [LTP] [PATCH v3 2/2] doc: conf.py: Show groups in test catalog Andrea Cervesato
1 sibling, 2 replies; 9+ messages in thread
From: Andrea Cervesato @ 2026-06-22 8:53 UTC (permalink / raw)
To: Linux Test Project
From: Andrea Cervesato <andrea.cervesato@suse.com>
Add groups field to metaparse JSON output, so we can filter out tests
in kirk. Groups are derived from:
1. Source file path - the two nearest parent directories (immediate
parent first), skipping 'kernel' as too generic. For example:
- testcases/kernel/syscalls/clone/clone01.c -> clone, syscalls
- testcases/kernel/kvm/kvm_pagefault01.c -> kvm
- testcases/cve/cve-2017-16939.c -> cve
2. @groups tags in the doc comment block, e.g.:
/*\
* Test description.
*
* @groups stress syscalls
* @groups memory
*/
Add test case for @groups tag parsing.
Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
---
metadata/metaparse.c | 163 +++++++++++++++++++++++++++-------
metadata/tests/array_size01.c.json | 2 +
metadata/tests/array_size02.c.json | 2 +
metadata/tests/array_size03.c.json | 2 +
metadata/tests/array_size04.c.json | 2 +
metadata/tests/empty_struct.c.json | 2 +
metadata/tests/expand_flags.c.json | 2 +
metadata/tests/groups.c | 11 +++
metadata/tests/groups.c.json | 12 +++
metadata/tests/include.c.json | 2 +
metadata/tests/macro.c.json | 2 +
metadata/tests/macro_str.c.json | 2 +
metadata/tests/multiline_macro.c.json | 2 +
metadata/tests/tags.c.json | 2 +
14 files changed, 177 insertions(+), 31 deletions(-)
diff --git a/metadata/metaparse.c b/metadata/metaparse.c
index 561cbb9d2d54689988c9aa49d591628696bcf847..f83acc5f057719ca1a48e24db7dec22b9f04e789 100644
--- a/metadata/metaparse.c
+++ b/metadata/metaparse.c
@@ -17,6 +17,7 @@
#include "data_storage.h"
#define INCLUDE_PATH_MAX 5
+#define GROUPS_TAG "@groups"
static int verbose;
static char *cmdline_includepath[INCLUDE_PATH_MAX];
@@ -34,7 +35,7 @@ static void remove_to_newline(FILE *f)
} while (c != '\n');
}
-static const char *eat_asterisk_space(const char *c)
+static char *eat_asterisk_space(char *c)
{
unsigned int i = 0;
@@ -50,7 +51,51 @@ static const char *eat_asterisk_space(const char *c)
return c;
}
-static void multiline_comment(FILE *f, struct data_node *doc)
+/*
+ * Add a group to the groups array, skipping 'kernel' as it's too generic.
+ * Returns 0 if no group was added, 1 otherwise.
+ */
+static int add_group(struct data_node *groups, const char *name)
+{
+ if (name && strcmp(name, "kernel")) {
+ data_node_array_add(groups, data_node_string(name));
+ return 1;
+ }
+
+ return 0;
+}
+
+/*
+ * Parse a '@groups foo bar baz' doc comment line, adding each
+ * whitespace-separated name to the groups array. Returns 1 if the line
+ * was a @groups tag (and should be consumed), 0 otherwise.
+ */
+static int parse_groups(struct data_node *groups, char *line)
+{
+ char *s;
+ char *name;
+
+ if (!groups)
+ return 0;
+
+ s = eat_asterisk_space(line);
+ if (strncmp(s, GROUPS_TAG, sizeof(GROUPS_TAG) - 1))
+ return 0;
+
+ s += sizeof(GROUPS_TAG) - 1;
+ if (*s && *s != ' ' && *s != '\t') {
+ WARN("Empty @group");
+ return 1;
+ }
+
+ for (name = strtok(s, " \t"); name; name = strtok(NULL, " \t"))
+ add_group(groups, name);
+
+ return 1;
+}
+
+static void multiline_comment(FILE *f, struct data_node *doc,
+ struct data_node *groups)
{
int c;
int state = 0;
@@ -62,12 +107,14 @@ static void multiline_comment(FILE *f, struct data_node *doc)
if (doc) {
if (c == '\n') {
- struct data_node *line;
+ char *str;
buf[bufp] = 0;
- line = data_node_string(eat_asterisk_space(buf));
- if (data_node_array_add(doc, line))
- WARN("doc string comment truncated");
+ str = eat_asterisk_space(buf);
bufp = 0;
+ if (parse_groups(groups, str))
+ continue;
+ if (data_node_array_add(doc, data_node_string(str)))
+ WARN("doc string comment truncated");
continue;
}
@@ -100,7 +147,8 @@ static void multiline_comment(FILE *f, struct data_node *doc)
static const char doc_prefix[] = "\\\n";
-static void maybe_doc_comment(FILE *f, struct data_node *doc)
+static void maybe_doc_comment(FILE *f, struct data_node *doc,
+ struct data_node *groups)
{
int c, i;
@@ -113,14 +161,15 @@ static void maybe_doc_comment(FILE *f, struct data_node *doc)
if (c == '*')
ungetc(c, f);
- multiline_comment(f, NULL);
+ multiline_comment(f, NULL, NULL);
return;
}
- multiline_comment(f, doc);
+ multiline_comment(f, doc, groups);
}
-static void maybe_comment(FILE *f, struct data_node *doc)
+static void maybe_comment(FILE *f, struct data_node *doc,
+ struct data_node *groups)
{
int c = getc(f);
@@ -129,7 +178,7 @@ static void maybe_comment(FILE *f, struct data_node *doc)
remove_to_newline(f);
break;
case '*':
- maybe_doc_comment(f, doc);
+ maybe_doc_comment(f, doc, groups);
break;
default:
ungetc(c, f);
@@ -137,7 +186,8 @@ static void maybe_comment(FILE *f, struct data_node *doc)
}
}
-static char *next_token2(FILE *f, char *buf, size_t buf_len, struct data_node *doc)
+static char *next_token2(FILE *f, char *buf, size_t buf_len,
+ struct data_node *doc, struct data_node *groups)
{
size_t i = 0;
int c;
@@ -194,7 +244,7 @@ static char *next_token2(FILE *f, char *buf, size_t buf_len, struct data_node *d
buf[i++] = c;
break;
case '/':
- maybe_comment(f, doc);
+ maybe_comment(f, doc, groups);
break;
case '"':
in_str = 1;
@@ -216,11 +266,11 @@ exit:
return buf;
}
-static char *next_token(FILE *f, struct data_node *doc)
+static char *next_token(FILE *f, struct data_node *doc, struct data_node *groups)
{
static char buf[4096];
- return next_token2(f, buf, sizeof(buf), doc);
+ return next_token2(f, buf, sizeof(buf), doc, groups);
}
static FILE *open_file(const char *dir, const char *fname)
@@ -383,7 +433,7 @@ static int array_is_hash(FILE *f)
int in_id = 1;
char *token;
- while ((token = next_token(f, NULL))) {
+ while ((token = next_token(f, NULL, NULL))) {
if (!strcmp(token, "}")) {
if (in_id && !comma_last)
@@ -402,7 +452,7 @@ static int array_is_hash(FILE *f)
int level = 1;
for (;;) {
- token = next_token(f, NULL);
+ token = next_token(f, NULL, NULL);
if (!token)
goto ret;
@@ -453,7 +503,7 @@ static int parse_array(FILE *f, const char *arr_id, struct data_node **ret)
*ret = data_node_array();
for (;;) {
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
return 1;
if (!strcmp(token, "{")) {
@@ -529,14 +579,14 @@ static int parse_get_array_len(FILE *f)
const char *token;
int cnt = 0, depth = 0, prev_comma = 0;
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
return 0;
if (strcmp(token, "{"))
return 0;
for (;;) {
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
return 0;
if (!strcmp(token, "{"))
@@ -565,7 +615,7 @@ static void look_for_array_size(FILE *f, const char *arr_id, struct data_node **
int prev_buf = 1;
for (;;) {
- if (!(token = next_token2(f, buf[cur_buf], sizeof(buf[cur_buf]), NULL)))
+ if (!(token = next_token2(f, buf[cur_buf], sizeof(buf[cur_buf]), NULL, NULL)))
break;
if (!strcmp(token, "=") && !strcmp(buf[prev_buf], arr_id)) {
@@ -595,13 +645,13 @@ static int parse_array_size(FILE *f, struct data_node **res)
*res = NULL;
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
return 1;
if (strcmp(token, "("))
return 1;
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
return 1;
arr_id = strdup(token);
@@ -621,7 +671,7 @@ static int parse_array_size(FILE *f, struct data_node **res)
rewind(f);
for (;;) {
- if (!(token = next_token(f, NULL)))
+ if (!(token = next_token(f, NULL, NULL)))
break;
if (token[0] == '#') {
@@ -654,7 +704,8 @@ static int parse_array_size(FILE *f, struct data_node **res)
return 0;
}
-static int parse_test_struct(FILE *f, struct data_node *doc, struct data_node *node)
+static int parse_test_struct(FILE *f, struct data_node *doc,
+ struct data_node *groups, struct data_node *node)
{
char *token;
char *id = NULL;
@@ -662,7 +713,7 @@ static int parse_test_struct(FILE *f, struct data_node *doc, struct data_node *n
struct data_node *ret;
for (;;) {
- if (!(token = next_token(f, doc)))
+ if (!(token = next_token(f, doc, groups)))
return 1;
if (!strcmp(token, "}"))
@@ -842,7 +893,7 @@ static void parse_include_macros(FILE *f, int level)
if (!inc)
return;
- while ((token = next_token(inc, NULL))) {
+ while ((token = next_token(inc, NULL, NULL))) {
if (token[0] == '#') {
hash = 1;
continue;
@@ -907,6 +958,49 @@ static void load_internal_macros(void)
fprintf(stderr, "END PREDEFINED MACROS\n");
}
+/*
+ * Add groups derived from the source file path.
+ *
+ * Groups are the two nearest parent directories (immediate parent
+ * first), skipping 'kernel' as it's too generic:
+ *
+ * testcases/kernel/syscalls/clone/clone01.c -> clone, syscalls
+ * testcases/kernel/kvm/kvm_pagefault01.c -> kvm
+ * testcases/cve/cve-2017-16939.c -> cve
+ */
+static void add_path_groups(struct data_node *groups, const char *fname)
+{
+ char *buf;
+ char *dirs[8];
+ int ndirs = 0;
+ char *p;
+
+ if (strncmp(fname, "testcases/", 10))
+ return;
+
+ buf = strdup(fname + 10);
+ if (!buf) {
+ fprintf(stderr, "Allocation failed!\n");
+ exit(1);
+ }
+
+ p = strtok(buf, "/");
+ while (p && ndirs < 8) {
+ dirs[ndirs++] = p;
+ p = strtok(NULL, "/");
+ }
+
+ /* Last element is the filename, skip it */
+ ndirs--;
+
+ if (ndirs >= 1)
+ add_group(groups, dirs[ndirs - 1]);
+ if (ndirs >= 2)
+ add_group(groups, dirs[ndirs - 2]);
+
+ free(buf);
+}
+
static struct data_node *parse_file(const char *fname)
{
int state = 0, found = 0;
@@ -923,15 +1017,18 @@ static struct data_node *parse_file(const char *fname)
struct data_node *res = data_node_hash();
struct data_node *doc = data_node_array();
+ struct data_node *groups = data_node_array();
+
+ add_path_groups(groups, fname);
load_internal_macros();
- while ((token = next_token(f, doc))) {
+ while ((token = next_token(f, doc, groups))) {
if (state < 6 && !strcmp(tokens[state], token)) {
state++;
} else {
if (token[0] == '#') {
- token = next_token(f, doc);
+ token = next_token(f, doc, groups);
if (token) {
if (!strcmp(token, "define"))
parse_macro(f);
@@ -948,7 +1045,7 @@ static struct data_node *parse_file(const char *fname)
continue;
found = 1;
- parse_test_struct(f, doc, res);
+ parse_test_struct(f, doc, groups, res);
}
if (data_node_array_len(doc)) {
@@ -958,6 +1055,9 @@ static struct data_node *parse_file(const char *fname)
data_node_free(doc);
}
+ /* inside the testcases/ folder we will always have at least one group */
+ data_node_hash_add(res, "groups", groups);
+
fclose(f);
if (!found) {
@@ -985,7 +1085,7 @@ static void parse_must_files(void)
if (!f)
continue;
- while ((token = next_token(f, NULL))) {
+ while ((token = next_token(f, NULL, NULL))) {
if (!strcmp(token, "define"))
parse_macro(f);
}
@@ -1238,6 +1338,7 @@ int main(int argc, char *argv[])
}
data_node_hash_add(res, "fname", data_node_string(argv[optind]));
+
printf(" \"%s\": ", strip_name(argv[optind]));
data_to_json(res, stdout, 2);
data_node_free(res);
diff --git a/metadata/tests/array_size01.c.json b/metadata/tests/array_size01.c.json
index ec364be1207673c7c2efe8a3b6284dc70d0d5e68..98ad9f6b0ba483b19a7b7253606e0ccf92060f29 100644
--- a/metadata/tests/array_size01.c.json
+++ b/metadata/tests/array_size01.c.json
@@ -1,4 +1,6 @@
"array_size01": {
"test_variants": 1,
+ "groups": [
+ ],
"fname": "array_size01.c"
}
\ No newline at end of file
diff --git a/metadata/tests/array_size02.c.json b/metadata/tests/array_size02.c.json
index 1226869525dfc5cc298f2916150e491f9a1f0db6..10e08c6af8a2d57e463431c3b350bb0d66739554 100644
--- a/metadata/tests/array_size02.c.json
+++ b/metadata/tests/array_size02.c.json
@@ -1,4 +1,6 @@
"array_size02": {
"test_variants": 3,
+ "groups": [
+ ],
"fname": "array_size02.c"
}
\ No newline at end of file
diff --git a/metadata/tests/array_size03.c.json b/metadata/tests/array_size03.c.json
index bb690c9f5abc332c2c55d2af7aaeec9e315bcaa8..44727933187cf465aaf91e43cc47d67cbcdae5a0 100644
--- a/metadata/tests/array_size03.c.json
+++ b/metadata/tests/array_size03.c.json
@@ -1,4 +1,6 @@
"array_size03": {
"test_variants": 2,
+ "groups": [
+ ],
"fname": "array_size03.c"
}
\ No newline at end of file
diff --git a/metadata/tests/array_size04.c.json b/metadata/tests/array_size04.c.json
index 6b8d41792035bbb04e8e69b2ae0f0e2d89af3073..13a987e1c0852d8520f0e37f38b467a8652469de 100644
--- a/metadata/tests/array_size04.c.json
+++ b/metadata/tests/array_size04.c.json
@@ -1,4 +1,6 @@
"array_size04": {
"test_variants": 3,
+ "groups": [
+ ],
"fname": "array_size04.c"
}
\ No newline at end of file
diff --git a/metadata/tests/empty_struct.c.json b/metadata/tests/empty_struct.c.json
index 9f49f53320040441c46f7ba314ec20912e0d94d7..3fbd9a38a664acf9ec893bf29ab6d0ab1a387ef8 100644
--- a/metadata/tests/empty_struct.c.json
+++ b/metadata/tests/empty_struct.c.json
@@ -1,3 +1,5 @@
"empty_struct": {
+ "groups": [
+ ],
"fname": "empty_struct.c"
}
\ No newline at end of file
diff --git a/metadata/tests/expand_flags.c.json b/metadata/tests/expand_flags.c.json
index 079972e4a7cf65a3717c179f7ddeb30c49964820..3941ae7f6ccb1d3c98176f75d1f10e6a99c2d18d 100644
--- a/metadata/tests/expand_flags.c.json
+++ b/metadata/tests/expand_flags.c.json
@@ -1,4 +1,6 @@
"expand_flags": {
+ "groups": [
+ ],
"all_filesystems": true,
"needs_device": true,
"needs_tmpdir": true,
diff --git a/metadata/tests/groups.c b/metadata/tests/groups.c
new file mode 100644
index 0000000000000000000000000000000000000000..40032a7ef60e398c50690995fc68025575d7f684
--- /dev/null
+++ b/metadata/tests/groups.c
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*\
+ * Test for @groups tag parsing.
+ *
+ * @groups stress regression
+ * @groups smoke
+ */
+
+static struct tst_test test = {
+};
diff --git a/metadata/tests/groups.c.json b/metadata/tests/groups.c.json
new file mode 100644
index 0000000000000000000000000000000000000000..2ca8b5d21d980cdffae4bdaeea20b659a52bad8a
--- /dev/null
+++ b/metadata/tests/groups.c.json
@@ -0,0 +1,12 @@
+ "groups": {
+ "doc": [
+ "Test for @groups tag parsing.",
+ ""
+ ],
+ "groups": [
+ "stress",
+ "regression",
+ "smoke"
+ ],
+ "fname": "groups.c"
+ }
\ No newline at end of file
diff --git a/metadata/tests/include.c.json b/metadata/tests/include.c.json
index b7c636e82e1ec2d90e680cc8b79891f147848cea..df0e5814bdac5c12a0c62b3a02b14718179449a1 100644
--- a/metadata/tests/include.c.json
+++ b/metadata/tests/include.c.json
@@ -1,4 +1,6 @@
"include": {
+ "groups": [
+ ],
"test_variants": 10,
"fname": "include.c"
}
\ No newline at end of file
diff --git a/metadata/tests/macro.c.json b/metadata/tests/macro.c.json
index c3f53ae432de6a4db851b02fcb0d384530d0c8a6..85907d3aba42330c0b7b5da5d7a86470097ae5d9 100644
--- a/metadata/tests/macro.c.json
+++ b/metadata/tests/macro.c.json
@@ -1,4 +1,6 @@
"macro": {
+ "groups": [
+ ],
"test_variants": 10,
"fname": "macro.c"
}
\ No newline at end of file
diff --git a/metadata/tests/macro_str.c.json b/metadata/tests/macro_str.c.json
index b162283166dfa13c9023c018206c0e7617d8c702..23a5dbd0775e303cfdc6e996daa13f53d4982aa6 100644
--- a/metadata/tests/macro_str.c.json
+++ b/metadata/tests/macro_str.c.json
@@ -1,4 +1,6 @@
"macro_str": {
"syscall": "syscall(\"foo\")",
+ "groups": [
+ ],
"fname": "macro_str.c"
}
\ No newline at end of file
diff --git a/metadata/tests/multiline_macro.c.json b/metadata/tests/multiline_macro.c.json
index 63451624256d9845cd4f1552f03b23b033c9a55b..4ae49bc0df1f89c24e3fc70cac3aa2c798b2cc88 100644
--- a/metadata/tests/multiline_macro.c.json
+++ b/metadata/tests/multiline_macro.c.json
@@ -1,4 +1,6 @@
"multiline_macro": {
+ "groups": [
+ ],
"test_variants": 10,
"fname": "multiline_macro.c"
}
\ No newline at end of file
diff --git a/metadata/tests/tags.c.json b/metadata/tests/tags.c.json
index d45c9f447f1337c9bed86edb7fd71a134db5d25e..4d176f5448640f87c9ea189a300e167b0030e97b 100644
--- a/metadata/tests/tags.c.json
+++ b/metadata/tests/tags.c.json
@@ -9,5 +9,7 @@
"tag-value-2"
]
],
+ "groups": [
+ ],
"fname": "tags.c"
}
\ No newline at end of file
--
2.51.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 9+ messages in thread