On Fri, 2026-03-06 at 17:10 +0100, Juri Lelli wrote:
> Add the foundational infrastructure for SCHED_DEADLINE scheduler tests
> following the pattern established by sched_ext selftests. This provides
> a clean, extensible framework for testing SCHED_DEADLINE functionality.
>
> The framework uses ELF constructors for automatic test registration,
> allowing new tests to be added simply by defining a struct dl_test and
> calling REGISTER_DL_TEST(). The runner discovers all registered tests
> at runtime and executes them serially, providing a simple and consistent
> way to expand test coverage without modifying the test runner itself.
>
> The framework provides a complete test lifecycle with setup, run, and
> cleanup phases for each test. Tests can be filtered by name, listed for
> inspection, or run in quiet mode for automated testing environments. The
> framework includes assertion macros such as DL_EQ and DL_FAIL_IF to
> simplify test authoring and provide consistent error reporting. Signal
> handling ensures graceful interruption and cleanup when tests are
> terminated early.
>
> This commit establishes the framework without any actual tests. Tests
> will be added in subsequent patches. The framework design is inspired by
> tools/testing/selftests/sched_ext/ but tailored for SCHED_DEADLINE
> testing needs.
>
> Assisted-by: Claude Code: claude-sonnet-4-5@20250929
> Signed-off-by: Juri Lelli <[email protected]>
> ---
> tools/testing/selftests/sched/deadline/.gitignore | 3 +
> tools/testing/selftests/sched/deadline/Makefile | 34 ++++
> tools/testing/selftests/sched/deadline/dl_test.h | 238
> ++++++++++++++++++++++
> tools/testing/selftests/sched/deadline/runner.c | 219 ++++++++++++++++++++
> 4 files changed, 494 insertions(+)
>
I wonder if this runner couldn't be made generic, something like not mentioning
DL in any of the helpers, keep runner.c in tools/testing/selftests/sched/ and
have a tools/testing/selftests/sched/deadline/Makefile glue all required objects
together for the deadline tests (or anything else).
That would probably make adding new tests much easier without code duplication.
Just a thought, haven't tested it.
Thanks,
Gabriele
> diff --git a/tools/testing/selftests/sched/deadline/.gitignore
> b/tools/testing/selftests/sched/deadline/.gitignore
> new file mode 100644
> index 0000000000000..503a1a968f952
> --- /dev/null
> +++ b/tools/testing/selftests/sched/deadline/.gitignore
> @@ -0,0 +1,3 @@
> +runner
> +cpuhog
> +*.o
> diff --git a/tools/testing/selftests/sched/deadline/Makefile
> b/tools/testing/selftests/sched/deadline/Makefile
> new file mode 100644
> index 0000000000000..fd57794f1a543
> --- /dev/null
> +++ b/tools/testing/selftests/sched/deadline/Makefile
> @@ -0,0 +1,34 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +TEST_GEN_PROGS := runner
> +
> +# override lib.mk's default rules
> +OVERRIDE_TARGETS := 1
> +include ../../lib.mk
> +
> +CFLAGS += -Wall -O2 -g -pthread
> +
> +OUTPUT_DIR := $(OUTPUT)
> +
> +# Utility object files
> +UTIL_OBJS :=
> +
> +# Test object files (all .c files except runner.c, dl_util.c, cpuhog.c)
> +# Will be populated as we add tests
> +TEST_OBJS :=
> +
> +# Runner binary links utility and test objects
> +$(OUTPUT)/runner: runner.c $(UTIL_OBJS) $(TEST_OBJS) dl_test.h |
> $(OUTPUT_DIR)
> + $(CC) $(CFLAGS) -o $@ runner.c $(UTIL_OBJS) $(TEST_OBJS) $(LDFLAGS)
> +
> +$(OUTPUT_DIR):
> + mkdir -p $@
> +
> +.PHONY: all clean
> +
> +all: $(TEST_GEN_PROGS)
> +
> +clean:
> + rm -f $(OUTPUT)/runner
> + rm -f $(OUTPUT)/*.o
> + rm -f *.o
> diff --git a/tools/testing/selftests/sched/deadline/dl_test.h
> b/tools/testing/selftests/sched/deadline/dl_test.h
> new file mode 100644
> index 0000000000000..545fae1af6631
> --- /dev/null
> +++ b/tools/testing/selftests/sched/deadline/dl_test.h
> @@ -0,0 +1,238 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * SCHED_DEADLINE Test Framework
> + *
> + * Provides infrastructure for testing SCHED_DEADLINE scheduler
> functionality.
> + * Tests register themselves using REGISTER_DL_TEST() macro and are
> + * automatically discovered by the runner at runtime.
> + */
> +
> +#ifndef __DL_TEST_H__
> +#define __DL_TEST_H__
> +
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <stdlib.h>
> +
> +/* Test status return codes */
> +enum dl_test_status {
> + DL_TEST_PASS = 0, /* Test passed successfully */
> + DL_TEST_SKIP, /* Test skipped (feature unavailable, etc.)
> */
> + DL_TEST_FAIL, /* Test failed */
> +};
> +
> +/**
> + * struct dl_test - Deadline scheduler test definition
> + * @name: Short test identifier (e.g., "basic_scheduling")
> + * @description: Human-readable description of what the test validates
> + * @setup: Optional callback to prepare test environment
> + * @run: Required callback to execute test logic
> + * @cleanup: Optional callback to cleanup test resources
> + *
> + * Tests are defined by filling in this structure and registering with
> + * REGISTER_DL_TEST(). The runner will discover and execute all registered
> + * tests automatically.
> + */
> +struct dl_test {
> + /**
> + * name - The name of the test
> + *
> + * Short identifier used for filtering and reporting. Should be
> + * lowercase with underscores (e.g., "bandwidth_admission").
> + */
> + const char *name;
> +
> + /**
> + * description - Human-readable test description
> + *
> + * Explains what the test validates and why it's important.
> + * Displayed when test runs unless quiet mode is enabled.
> + */
> + const char *description;
> +
> + /**
> + * setup - Optional setup callback
> + * @ctx: Pointer to context pointer, can be set to pass data to run()
> + *
> + * Called before run() to prepare test environment. Can allocate
> + * resources, check prerequisites, etc.
> + *
> + * Return:
> + * - DL_TEST_PASS: Continue to run()
> + * - DL_TEST_SKIP: Skip this test (feature not available, etc.)
> + * - DL_TEST_FAIL: Abort test (setup failed)
> + *
> + * If setup() returns SKIP or FAIL, run() and cleanup() are not
> called.
> + */
> + enum dl_test_status (*setup)(void **ctx);
> +
> + /**
> + * run - Required test execution callback
> + * @ctx: Context pointer set by setup(), or NULL if no setup
> + *
> + * Executes the actual test logic. This is the main test function.
> + *
> + * Return:
> + * - DL_TEST_PASS: Test passed
> + * - DL_TEST_SKIP: Test skipped (unlikely here, prefer setup())
> + * - DL_TEST_FAIL: Test failed
> + */
> + enum dl_test_status (*run)(void *ctx);
> +
> + /**
> + * cleanup - Optional cleanup callback
> + * @ctx: Context pointer set by setup(), or NULL if no setup
> + *
> + * Called after run() to cleanup resources. Always runs if setup()
> + * succeeded, regardless of run() result. Cannot fail the test.
> + */
> + void (*cleanup)(void *ctx);
> +};
> +
> +/**
> + * dl_test_register() - Register a test with the framework
> + * @test: Pointer to test structure
> + *
> + * Called by REGISTER_DL_TEST() macro. Don't call directly.
> + */
> +void dl_test_register(struct dl_test *test);
> +
> +/**
> + * REGISTER_DL_TEST() - Register a test for auto-discovery
> + * @__test: Pointer to struct dl_test
> + *
> + * Uses ELF constructor attribute to automatically register the test
> + * when the binary loads. The runner will discover and execute all
> + * registered tests.
> + *
> + * Example:
> + * static struct dl_test my_test = {
> + * .name = "my_test",
> + * .description = "Tests something important",
> + * .run = my_test_run,
> + * };
> + * REGISTER_DL_TEST(&my_test);
> + */
> +#define __DL_CONCAT(a, b) a##b
> +#define _DL_CONCAT(a, b) __DL_CONCAT(a, b)
> +
> +#define REGISTER_DL_TEST(__test) \
> + __attribute__((constructor)) \
> + static void _DL_CONCAT(___dlregister_, __LINE__)(void) \
> + { \
> + dl_test_register(__test); \
> + }
> +
> +/* Error reporting macros */
> +
> +/**
> + * DL_ERR() - Print error message with file/line info
> + */
> +#define DL_ERR(__fmt, ...) \
> + do { \
> + fprintf(stderr, "ERR: %s:%d\n", __FILE__, __LINE__); \
> + fprintf(stderr, __fmt"\n", ##__VA_ARGS__); \
> + } while (0)
> +
> +/**
> + * DL_FAIL() - Fail the test with a message
> + *
> + * Prints error message and returns DL_TEST_FAIL. Use in test run() or
> + * setup() functions.
> + */
> +#define DL_FAIL(__fmt, ...) \
> + do { \
> + DL_ERR(__fmt, ##__VA_ARGS__); \
> + return DL_TEST_FAIL; \
> + } while (0)
> +
> +/**
> + * DL_FAIL_IF() - Conditionally fail the test
> + * @__cond: Condition to check
> + * @__fmt: printf-style format string
> + *
> + * If condition is true, fail the test with the given message.
> + */
> +#define DL_FAIL_IF(__cond, __fmt, ...)
> \
> + do { \
> + if (__cond) \
> + DL_FAIL(__fmt, ##__VA_ARGS__); \
> + } while (0)
> +
> +/* Comparison assertion macros */
> +
> +/**
> + * DL_EQ() - Assert two values are equal
> + */
> +#define DL_EQ(_x, _y) \
> + DL_FAIL_IF((_x) != (_y), \
> + "Expected %s == %s (%lld == %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_NE() - Assert two values are not equal
> + */
> +#define DL_NE(_x, _y) \
> + DL_FAIL_IF((_x) == (_y), \
> + "Expected %s != %s (%lld != %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_LT() - Assert x < y
> + */
> +#define DL_LT(_x, _y) \
> + DL_FAIL_IF((_x) >= (_y), \
> + "Expected %s < %s (%lld < %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_LE() - Assert x <= y
> + */
> +#define DL_LE(_x, _y) \
> + DL_FAIL_IF((_x) > (_y), \
> + "Expected %s <= %s (%lld <= %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_GT() - Assert x > y
> + */
> +#define DL_GT(_x, _y) \
> + DL_FAIL_IF((_x) <= (_y), \
> + "Expected %s > %s (%lld > %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_GE() - Assert x >= y
> + */
> +#define DL_GE(_x, _y) \
> + DL_FAIL_IF((_x) < (_y), \
> + "Expected %s >= %s (%lld >= %lld)", \
> + #_x, #_y, (long long)(_x), (long long)(_y))
> +
> +/**
> + * DL_ASSERT() - Assert condition is true
> + */
> +#define DL_ASSERT(_x) \
> + DL_FAIL_IF(!(_x), "Expected %s to be true", #_x)
> +
> +/**
> + * DL_BUG_ON() - Fatal assertion (for framework bugs, not test failures)
> + * @__cond: Condition to check
> + * @__fmt: Error message
> + *
> + * For internal framework consistency checks. If condition is true,
> + * prints error and aborts. Use for "should never happen" cases.
> + */
> +#define DL_BUG_ON(__cond, __fmt, ...)
> \
> + do { \
> + if (__cond) { \
> + fprintf(stderr, "BUG: %s:%d: " __fmt "\n", \
> + __FILE__, __LINE__, ##__VA_ARGS__); \
> + abort(); \
> + } \
> + } while (0)
> +
> +#endif /* __DL_TEST_H__ */
> diff --git a/tools/testing/selftests/sched/deadline/runner.c
> b/tools/testing/selftests/sched/deadline/runner.c
> new file mode 100644
> index 0000000000000..358f695423ef5
> --- /dev/null
> +++ b/tools/testing/selftests/sched/deadline/runner.c
> @@ -0,0 +1,219 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * SCHED_DEADLINE Test Runner
> + *
> + * Discovers and executes all registered SCHED_DEADLINE tests.
> + * Tests are statically linked and register themselves via ELF constructors.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <signal.h>
> +#include <string.h>
> +#include <stdbool.h>
> +#include "dl_test.h"
> +
> +const char help_fmt[] =
> +"Runner for SCHED_DEADLINE scheduler tests.\n"
> +"\n"
> +"All tests are statically linked and run serially. Tests require root\n"
> +"privileges to set SCHED_DEADLINE scheduling policy.\n"
> +"\n"
> +"Usage: %s [-t TEST] [-h]\n"
> +"\n"
> +" -t TEST Only run tests whose name includes this string\n"
> +" -s Include print output for skipped tests\n"
> +" -l List all available tests\n"
> +" -q Don't print the test descriptions during run\n"
> +" -h Display this help and exit\n";
> +
> +static volatile int exit_req;
> +static bool quiet, print_skipped, list;
> +
> +#define MAX_DL_TESTS 256
> +
> +static struct dl_test *__dl_tests[MAX_DL_TESTS];
> +static unsigned int __dl_num_tests;
> +
> +static void sigint_handler(int sig)
> +{
> + exit_req = 1;
> +}
> +
> +static void print_test_preamble(const struct dl_test *test, bool quiet)
> +{
> + printf("===== START =====\n");
> + printf("TEST: %s\n", test->name);
> + if (!quiet)
> + printf("DESCRIPTION: %s\n", test->description);
> + printf("OUTPUT:\n");
> +}
> +
> +static const char *status_to_result(enum dl_test_status status)
> +{
> + switch (status) {
> + case DL_TEST_PASS:
> + case DL_TEST_SKIP:
> + return "ok";
> + case DL_TEST_FAIL:
> + return "not ok";
> + default:
> + return "<UNKNOWN>";
> + }
> +}
> +
> +static void print_test_result(const struct dl_test *test,
> + enum dl_test_status status,
> + unsigned int testnum)
> +{
> + const char *result = status_to_result(status);
> + const char *directive = status == DL_TEST_SKIP ? "SKIP " : "";
> +
> + printf("%s %u %s # %s\n", result, testnum, test->name, directive);
> + printf("===== END =====\n");
> +}
> +
> +static bool should_skip_test(const struct dl_test *test, const char *filter)
> +{
> + return filter && !strstr(test->name, filter);
> +}
> +
> +static enum dl_test_status run_test(const struct dl_test *test)
> +{
> + enum dl_test_status status;
> + void *context = NULL;
> +
> + if (test->setup) {
> + status = test->setup(&context);
> + if (status != DL_TEST_PASS)
> + return status;
> + }
> +
> + status = test->run(context);
> +
> + if (test->cleanup)
> + test->cleanup(context);
> +
> + return status;
> +}
> +
> +static bool test_valid(const struct dl_test *test)
> +{
> + if (!test) {
> + fprintf(stderr, "NULL test detected\n");
> + return false;
> + }
> +
> + if (!test->name) {
> + fprintf(stderr,
> + "Test with no name found. Must specify test
> name.\n");
> + return false;
> + }
> +
> + if (!test->description) {
> + fprintf(stderr, "Test %s requires description.\n", test-
> >name);
> + return false;
> + }
> +
> + if (!test->run) {
> + fprintf(stderr, "Test %s has no run() callback\n", test-
> >name);
> + return false;
> + }
> +
> + return true;
> +}
> +
> +int main(int argc, char **argv)
> +{
> + const char *filter = NULL;
> + unsigned int testnum = 0, i;
> + unsigned int passed = 0, skipped = 0, failed = 0;
> + int opt;
> +
> + signal(SIGINT, sigint_handler);
> + signal(SIGTERM, sigint_handler);
> +
> + while ((opt = getopt(argc, argv, "qslt:h")) != -1) {
> + switch (opt) {
> + case 'q':
> + quiet = true;
> + break;
> + case 's':
> + print_skipped = true;
> + break;
> + case 'l':
> + list = true;
> + break;
> + case 't':
> + filter = optarg;
> + break;
> + default:
> + fprintf(stderr, help_fmt, argv[0]);
> + return opt != 'h';
> + }
> + }
> +
> + for (i = 0; i < __dl_num_tests; i++) {
> + enum dl_test_status status;
> + struct dl_test *test = __dl_tests[i];
> +
> + if (list) {
> + printf("%s\n", test->name);
> + if (i == (__dl_num_tests - 1))
> + return 0;
> + continue;
> + }
> +
> + if (should_skip_test(test, filter)) {
> + /*
> + * Printing the skipped tests and their preambles can
> + * add a lot of noise to the runner output. Printing
> + * this is only really useful for CI, so let's skip
> it
> + * by default.
> + */
> + if (print_skipped) {
> + print_test_preamble(test, quiet);
> + print_test_result(test, DL_TEST_SKIP,
> ++testnum);
> + }
> + continue;
> + }
> +
> + print_test_preamble(test, quiet);
> + status = run_test(test);
> + print_test_result(test, status, ++testnum);
> +
> + switch (status) {
> + case DL_TEST_PASS:
> + passed++;
> + break;
> + case DL_TEST_SKIP:
> + skipped++;
> + break;
> + case DL_TEST_FAIL:
> + failed++;
> + break;
> + }
> +
> + if (exit_req) {
> + fprintf(stderr, "\nInterrupted by signal\n");
> + break;
> + }
> + }
> +
> + printf("\n\n=============================\n\n");
> + printf("RESULTS:\n\n");
> + printf("PASSED: %u\n", passed);
> + printf("SKIPPED: %u\n", skipped);
> + printf("FAILED: %u\n", failed);
> +
> + return failed > 0 ? 1 : 0;
> +}
> +
> +void dl_test_register(struct dl_test *test)
> +{
> + DL_BUG_ON(!test_valid(test), "Invalid test found");
> + DL_BUG_ON(__dl_num_tests >= MAX_DL_TESTS, "Maximum tests exceeded");
> +
> + __dl_tests[__dl_num_tests++] = test;
> +}