From 13fe95b51de540dd57e1803f1095fde6fe44b4a1 Mon Sep 17 00:00:00 2001 From: Chris Friedt Date: Fri, 16 May 2025 07:01:21 -0400 Subject: [PATCH] tests: lib: timeutil: add timespec util testsuite Add a timespec util testsuite. This should have reasonably high enough coverage to be useful. I would have preferred to add this as an architecture-independent unit test (for the unit_testing platform) under tests/unit/timeutil but there is an inconsistency about the size of time_t on the unit_testing and native_sim/native platforms. On every other platform supported by Zephyr, time_t is 64-bits. However, on those platforms, time_t is only 32-bits. Signed-off-by: Chris Friedt --- tests/lib/timespec_util/CMakeLists.txt | 8 + tests/lib/timespec_util/prj.conf | 1 + tests/lib/timespec_util/src/main.c | 380 +++++++++++++++++++++++++ tests/lib/timespec_util/testcase.yaml | 36 +++ 4 files changed, 425 insertions(+) create mode 100644 tests/lib/timespec_util/CMakeLists.txt create mode 100644 tests/lib/timespec_util/prj.conf create mode 100644 tests/lib/timespec_util/src/main.c create mode 100644 tests/lib/timespec_util/testcase.yaml diff --git a/tests/lib/timespec_util/CMakeLists.txt b/tests/lib/timespec_util/CMakeLists.txt new file mode 100644 index 00000000000..e436a632ba0 --- /dev/null +++ b/tests/lib/timespec_util/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(timespec_util) + +FILE(GLOB app_sources src/main.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/timespec_util/prj.conf b/tests/lib/timespec_util/prj.conf new file mode 100644 index 00000000000..9467c292689 --- /dev/null +++ b/tests/lib/timespec_util/prj.conf @@ -0,0 +1 @@ +CONFIG_ZTEST=y diff --git a/tests/lib/timespec_util/src/main.c b/tests/lib/timespec_util/src/main.c new file mode 100644 index 00000000000..f538a41e725 --- /dev/null +++ b/tests/lib/timespec_util/src/main.c @@ -0,0 +1,380 @@ +/* + * Copyright 2025 Tenstorrent AI ULC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include + +BUILD_ASSERT(sizeof(time_t) == sizeof(int64_t), "time_t must be 64-bit"); +BUILD_ASSERT(sizeof(((struct timespec *)0)->tv_sec) == sizeof(int64_t), "tv_sec must be 64-bit"); + +/* need NSEC_PER_SEC to be signed for the purposes of this testsuite */ +#undef NSEC_PER_SEC +#define NSEC_PER_SEC 1000000000L + +#undef CORRECTABLE +#define CORRECTABLE true + +#undef UNCORRECTABLE +#define UNCORRECTABLE false + +/* + * test spec for simple timespec validation + * + * If a timespec is expected to be valid, then invalid_ts and valid_ts are equal. + * + * If a timespec is expected to be invalid, then invalid_ts and valid_ts are not equal. + */ +struct ts_test_spec { + struct timespec invalid_ts; + struct timespec valid_ts; + bool expect_valid; + bool correctable; +}; + +#define DECL_VALID_TS_TEST(sec, nsec) \ + { \ + .invalid_ts = {(sec), (nsec)}, \ + .valid_ts = {(sec), (nsec)}, \ + .expect_valid = true, \ + .correctable = false, \ + } + +/* + * Invalid timespecs can usually be converted to valid ones by adding or subtracting + * multiples of `NSEC_PER_SEC` from tv_nsec, and incrementing or decrementing tv_sec + * proportionately, unless doing so would result in signed integer overflow. + * + * There are two particular corner cases: + * 1. making tv_sec more negative would overflow the tv_sec field in the negative direction + * 1. making tv_sec more positive would overflow the tv_sec field in the positive direction + */ +#define DECL_INVALID_TS_TEST(invalid_sec, invalid_nsec, valid_sec, valid_nsec, is_correctable) \ + { \ + .valid_ts = {(valid_sec), (valid_nsec)}, \ + .invalid_ts = {(invalid_sec), (invalid_nsec)}, \ + .expect_valid = false, \ + .correctable = (is_correctable), \ + } + +/* + * Easily verifiable tests for both valid and invalid timespecs. + */ +static const struct ts_test_spec ts_tests[] = { + /* Valid cases */ + DECL_VALID_TS_TEST(0, 0), + DECL_VALID_TS_TEST(0, 1), + DECL_VALID_TS_TEST(1, 0), + DECL_VALID_TS_TEST(1, 1), + DECL_VALID_TS_TEST(1, NSEC_PER_SEC - 1), + DECL_VALID_TS_TEST(-1, 0), + DECL_VALID_TS_TEST(-1, 1), + DECL_VALID_TS_TEST(-1, 0), + DECL_VALID_TS_TEST(-1, 1), + DECL_VALID_TS_TEST(-1, NSEC_PER_SEC - 1), + DECL_VALID_TS_TEST(INT64_MIN, 0), + DECL_VALID_TS_TEST(INT64_MIN, NSEC_PER_SEC - 1), + DECL_VALID_TS_TEST(INT64_MAX, 0), + DECL_VALID_TS_TEST(INT64_MAX, NSEC_PER_SEC - 1), + + /* Correctable, invalid cases */ + DECL_INVALID_TS_TEST(0, -2 * NSEC_PER_SEC + 1, -2, 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, -2 * NSEC_PER_SEC - 1, -3, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, -NSEC_PER_SEC - 1, -2, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, -1, -1, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, NSEC_PER_SEC, 1, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(0, NSEC_PER_SEC + 1, 1, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(1, -1, 0, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(1, NSEC_PER_SEC, 2, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(-1, -1, -2, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, NSEC_PER_SEC, 1, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(1, -1, 0, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(1, NSEC_PER_SEC, 2, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MIN, NSEC_PER_SEC, INT64_MIN + 1, 0, CORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MAX, -1, INT64_MAX - 1, NSEC_PER_SEC - 1, CORRECTABLE), + DECL_INVALID_TS_TEST(0, LONG_MIN, LONG_MAX / NSEC_PER_SEC, 145224192, CORRECTABLE), + DECL_INVALID_TS_TEST(0, LONG_MAX, LONG_MAX / NSEC_PER_SEC, LONG_MAX % NSEC_PER_SEC, + CORRECTABLE), + + /* Uncorrectable, invalid cases */ + DECL_INVALID_TS_TEST(INT64_MIN + 2, -2 * NSEC_PER_SEC - 1, 0, 0, UNCORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MIN + 1, -NSEC_PER_SEC - 1, 0, 0, UNCORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MIN + 1, -NSEC_PER_SEC - 1, 0, 0, UNCORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MIN, -1, 0, 0, UNCORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MAX, NSEC_PER_SEC, 0, 0, UNCORRECTABLE), + DECL_INVALID_TS_TEST(INT64_MAX - 1, 2 * NSEC_PER_SEC, 0, 0, UNCORRECTABLE), +}; + +ZTEST(timeutil_api, test_timespec_is_valid) +{ + ARRAY_FOR_EACH(ts_tests, i) { + const struct ts_test_spec *const tspec = &ts_tests[i]; + bool valid = timespec_is_valid(&tspec->invalid_ts); + + zexpect_equal(valid, tspec->expect_valid, + "%d: timespec_is_valid({%ld, %ld}) = %s, expected true", i, + tspec->valid_ts.tv_sec, tspec->valid_ts.tv_nsec, + tspec->expect_valid ? "false" : "true"); + } +} + +ZTEST(timeutil_api, test_timespec_normalize) +{ + ARRAY_FOR_EACH(ts_tests, i) { + bool different; + bool overflow; + const struct ts_test_spec *const tspec = &ts_tests[i]; + struct timespec norm = tspec->invalid_ts; + + overflow = !timespec_normalize(&norm); + zexpect_not_equal(tspec->expect_valid || tspec->correctable, overflow, + "%d: timespec_normalize({%ld, %ld}) %s, unexpectedly", i, + (long)tspec->invalid_ts.tv_sec, (long)tspec->invalid_ts.tv_nsec, + tspec->correctable ? "failed" : "succeeded"); + + if (!tspec->expect_valid && tspec->correctable) { + different = !timespec_equal(&tspec->invalid_ts, &norm); + zexpect_true(different, "%d: {%ld, %ld} and {%ld, %ld} are unexpectedly %s", + i, tspec->invalid_ts.tv_sec, tspec->invalid_ts.tv_nsec, + norm.tv_sec, tspec->valid_ts.tv_sec, + (tspec->expect_valid || tspec->correctable) ? "different" + : "equal"); + } + } +} + +ZTEST(timeutil_api, test_timespec_add) +{ + bool overflow; + struct timespec actual; + const struct atspec { + struct timespec a; + struct timespec b; + struct timespec result; + bool expect; + } tspecs[] = { + /* non-overflow cases */ + {.a = {0, 0}, .b = {0, 0}, .result = {0, 0}, .expect = false}, + {.a = {1, 1}, .b = {1, 1}, .result = {2, 2}, .expect = false}, + {.a = {-1, 1}, .b = {-1, 1}, .result = {-2, 2}, .expect = false}, + {.a = {-1, NSEC_PER_SEC - 1}, .b = {0, 1}, .result = {0, 0}, .expect = false}, + /* overflow cases */ + {.a = {INT64_MAX, 0}, .b = {1, 0}, .result = {0}, .expect = true}, + {.a = {INT64_MIN, 0}, .b = {-1, 0}, .result = {0}, .expect = true}, + {.a = {INT64_MAX, NSEC_PER_SEC - 1}, .b = {1, 1}, .result = {0}, .expect = true}, + {.a = {INT64_MIN, NSEC_PER_SEC - 1}, .b = {-1, 0}, .result = {0}, .expect = true}, + }; + + ARRAY_FOR_EACH(tspecs, i) { + const struct atspec *const tspec = &tspecs[i]; + + actual = tspec->a; + overflow = !timespec_add(&actual, &tspec->b); + + zexpect_equal(overflow, tspec->expect, + "%d: timespec_add({%ld, %ld}, {%ld, %ld}) %s, unexpectedly", i, + tspec->a.tv_sec, tspec->a.tv_nsec, tspec->b.tv_sec, tspec->b.tv_nsec, + tspec->expect ? "succeeded" : "failed"); + + if (!tspec->expect) { + zexpect_equal(timespec_equal(&actual, &tspec->result), true, + "%d: {%ld, %ld} and {%ld, %ld} are unexpectedly different", i, + actual.tv_sec, actual.tv_nsec, tspec->result.tv_sec, + tspec->result.tv_nsec); + } + } +} + +ZTEST(timeutil_api, test_timespec_negate) +{ + struct ntspec { + struct timespec ts; + struct timespec result; + bool expect_failure; + } tspecs[] = { + /* non-overflow cases */ + {.ts = {0, 0}, .result = {0, 0}, .expect_failure = false}, + {.ts = {1, 1}, .result = {-2, NSEC_PER_SEC - 1}, .expect_failure = false}, + {.ts = {-1, 1}, .result = {0, NSEC_PER_SEC - 1}, .expect_failure = false}, + {.ts = {INT64_MAX, 0}, .result = {INT64_MIN + 1, 0}, .expect_failure = false}, + /* overflow cases */ + {.ts = {INT64_MIN, 0}, .result = {0}, .expect_failure = true}, + }; + + ARRAY_FOR_EACH(tspecs, i) { + bool overflow; + const struct ntspec *const tspec = &tspecs[i]; + struct timespec actual = tspec->ts; + + overflow = !timespec_negate(&actual); + zexpect_equal(overflow, tspec->expect_failure, + "%d: timespec_negate({%ld, %ld}) %s, unexpectedly", i, + tspec->ts.tv_sec, tspec->ts.tv_nsec, + tspec->expect_failure ? "did not overflow" : "overflowed"); + + if (!tspec->expect_failure) { + zexpect_true(timespec_equal(&actual, &tspec->result), + "%d: {%ld, %ld} and {%ld, %ld} are unexpectedly different", i, + actual.tv_sec, actual.tv_nsec, tspec->result.tv_sec, + tspec->result.tv_nsec); + } + } +} + +ZTEST(timeutil_api, test_timespec_sub) +{ + struct timespec a = {0}; + struct timespec b = {0}; + + zexpect_true(timespec_sub(&a, &b)); +} + +ZTEST(timeutil_api, test_timespec_compare) +{ + struct timespec a; + struct timespec b; + + a = (struct timespec){0}; + b = (struct timespec){0}; + zexpect_equal(timespec_compare(&a, &b), 0); + + a = (struct timespec){-1}; + b = (struct timespec){0}; + zexpect_equal(timespec_compare(&a, &b), -1); + + a = (struct timespec){1}; + b = (struct timespec){0}; + zexpect_equal(timespec_compare(&a, &b), 1); +} + +ZTEST(timeutil_api, test_timespec_equal) +{ + struct timespec a; + struct timespec b; + + a = (struct timespec){0}; + b = (struct timespec){0}; + zexpect_true(timespec_equal(&a, &b)); + + a = (struct timespec){-1}; + b = (struct timespec){0}; + zexpect_false(timespec_equal(&a, &b)); +} + +#define K_TICK_MAX ((uint64_t)(CONFIG_TIMEOUT_64BIT ? (INT64_MAX) : (UINT32_MAX))) +#define NS_PER_TICK (NSEC_PER_SEC / CONFIG_SYS_CLOCK_TICKS_PER_SEC) + +/* 0 := lower limit, 2 := upper limit */ +static const struct timespec k_timeout_limits[] = { + /* K_NO_WAIT + 1 tick */ + { + .tv_sec = 0, + .tv_nsec = NS_PER_TICK, + }, + /* K_FOREVER - 1 tick */ + { + .tv_sec = CLAMP((NS_PER_TICK * K_TICK_MAX) / NSEC_PER_SEC, 0, INT64_MAX), + .tv_nsec = CLAMP((NS_PER_TICK * K_TICK_MAX) % NSEC_PER_SEC, 0, NSEC_PER_SEC - 1), + }, +}; + +static const struct tospec { + k_timeout_t timeout; + struct timespec tspec; + int saturation; +} tospecs[] = { + {K_NO_WAIT, {INT64_MIN, 0}, -1}, + {K_NO_WAIT, {-1, 0}, -1}, + {K_NO_WAIT, {-1, NSEC_PER_SEC - 1}, -1}, + {K_NO_WAIT, {0, 0}, 0}, + {K_NSEC(0), {0, 0}, 0}, + {K_NSEC(2000000000), {2, 0}, 0}, + {K_USEC(0), {0, 0}, 0}, + {K_USEC(2000000), {2, 0}, 0}, + {K_MSEC(100), {0, 100000000}, 0}, + {K_MSEC(2000), {2, 0}, 0}, + {K_SECONDS(0), {0, 0}, 0}, + {K_SECONDS(1), {1, 0}, 0}, + {K_SECONDS(100), {100, 0}, 0}, + {K_FOREVER, {INT64_MAX, NSEC_PER_SEC - 1}, 0}, +}; + +ZTEST(timeutil_api, test_timespec_from_timeout) +{ + ARRAY_FOR_EACH(tospecs, i) { + const struct tospec *const tspec = &tospecs[i]; + struct timespec actual; + + if (tspec->saturation != 0) { + /* saturation cases are only checked in test_timespec_to_timeout */ + continue; + } + + timespec_from_timeout(tspec->timeout, &actual); + zexpect_true(timespec_equal(&actual, &tspec->tspec), + "%d: {%ld, %ld} and {%ld, %ld} are unexpectedly different", i, + actual.tv_sec, actual.tv_nsec, tspec->tspec.tv_sec, + tspec->tspec.tv_nsec); + } +} + +ZTEST(timeutil_api, test_timespec_to_timeout) +{ + ARRAY_FOR_EACH(tospecs, i) { + const struct tospec *const tspec = &tospecs[i]; + k_timeout_t actual; + + if (tspec->saturation == 0) { + /* no saturation / exact match */ + actual = timespec_to_timeout(&tspec->tspec); + zexpect_equal(actual.ticks, tspec->timeout.ticks, + "%d: {%" PRId64 "} and {%" PRId64 + "} are unexpectedly different", + i, (int64_t)actual.ticks, (int64_t)tspec->timeout.ticks); + continue; + } + + if ((tspec->saturation < 0) || + (timespec_compare(&tspec->tspec, &k_timeout_limits[0]) < 0)) { + /* K_NO_WAIT saturation */ + actual = timespec_to_timeout(&tspec->tspec); + zexpect_equal(actual.ticks, K_NO_WAIT.ticks, + "%d: {%" PRId64 "} and {%" PRId64 + "} are unexpectedly different", + i, (int64_t)actual.ticks, (int64_t)K_NO_WAIT.ticks); + continue; + } + + if ((tspec->saturation > 0) || + (timespec_compare(&tspec->tspec, &k_timeout_limits[1]) > 0)) { + /* K_FOREVER saturation */ + actual = timespec_to_timeout(&tspec->tspec); + zexpect_equal(actual.ticks, K_TICKS_FOREVER, + "%d: {%" PRId64 "} and {%" PRId64 + "} are unexpectedly different", + i, (int64_t)actual.ticks, (int64_t)K_TICKS_FOREVER); + continue; + } + } +} + +static void *setup(void) +{ + printk("CONFIG_TIMEOUT_64BIT=%c\n", CONFIG_TIMEOUT_64BIT ? 'y' : 'n'); + printk("K_TICK_MAX: %lld\n", (long long)K_TICK_MAX); + printk("minimum timeout: {%lld, %lld}\n", (long long)k_timeout_limits[0].tv_sec, + (long long)k_timeout_limits[0].tv_nsec); + printk("maximum timeout: {%lld, %lld}\n", (long long)k_timeout_limits[1].tv_sec, + (long long)k_timeout_limits[1].tv_nsec); + + return NULL; +} + +ZTEST_SUITE(timeutil_api, NULL, setup, NULL, NULL, NULL); diff --git a/tests/lib/timespec_util/testcase.yaml b/tests/lib/timespec_util/testcase.yaml new file mode 100644 index 00000000000..d69c5bb755f --- /dev/null +++ b/tests/lib/timespec_util/testcase.yaml @@ -0,0 +1,36 @@ +# FIXME: this should be under tests/unit/timeutil but will not work due to #90029 +common: + filter: not CONFIG_NATIVE_LIBC + tags: + - timeutils + # 1 tier0 platform per supported architecture + platform_key: + - arch + - simulation + integration_platforms: + - native_sim/native/64 +tests: + libraries.timespec_utils: {} + libraries.timespec_utils.speed: + extra_configs: + - CONFIG_SPEED_OPTIMIZATIONS=y + libraries.timespec_utils.armclang_std_libc: + toolchain_allow: armclang + extra_configs: + - CONFIG_ARMCLANG_STD_LIBC=y + libraries.timespec_utils.arcmwdtlib: + toolchain_allow: arcmwdt + extra_configs: + - CONFIG_ARCMWDT_LIBC=y + libraries.timespec_utils.minimal: + extra_configs: + - CONFIG_MINIMAL_LIBC=y + libraries.timespec_utils.newlib: + filter: TOOLCHAIN_HAS_NEWLIB == 1 + extra_configs: + - CONFIG_NEWLIB_LIBC=y + libraries.timespec_utils.picolibc: + tags: picolibc + filter: CONFIG_PICOLIBC_SUPPORTED + extra_configs: + - CONFIG_PICOLIBC=y