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;
> +}


Reply via email to