diff --git a/tests/thread_float/Makefile b/tests/thread_float/Makefile index c2ea8f27a3..3cf9f94389 100644 --- a/tests/thread_float/Makefile +++ b/tests/thread_float/Makefile @@ -3,6 +3,11 @@ include ../Makefile.tests_common USEMODULE += printf_float USEMODULE += xtimer +# native has known issues: the context switch via glibc's setcontext() +# apparently doesn't properly save and restore the FPU state. This results in +# occasionally wrong results (often nan) being printed for the same calculation +TEST_ON_CI_BLACKLIST += native + #DISABLE_MODULE += cortexm_fpu include $(RIOTBASE)/Makefile.include diff --git a/tests/thread_float/README.md b/tests/thread_float/README.md new file mode 100644 index 0000000000..e06b8df881 --- /dev/null +++ b/tests/thread_float/README.md @@ -0,0 +1,16 @@ +Testing for Absence of Interactions between Floating Point Calculations and Context Switches +============================================================================================ + +This tests launches three threads, t1, t2 and t3 that will perform a long and costly series of +floating point calculations with different input data, while a software timer triggers context +switches. The threads t1 and t3 will print the results. All threads will do this in an endless +loop. + +This allows for testing the following: + +1. When using the pseudo module `printf_float`, floating point numbers can be correctly printed +2. `THREAD_STACKSIZE_MAIN` is large enough to print floats without stack overflows. +3. Context switches while the (soft) FPU is busy does not result in precision loss. + - This could happen if the FPU state is not properly saved / restored on context switch. This + could be needed if the FPU internally uses a higher resolution that `float` / `double` + (e.g. the x86 FPU uses 80 bits internally, instead of 64 bits for double) diff --git a/tests/thread_float/main.c b/tests/thread_float/main.c index 693e127dfa..5d6a336090 100644 --- a/tests/thread_float/main.c +++ b/tests/thread_float/main.c @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 OTA keys S.A. + * 2021 Otto-von-Guericke-Universität Magdeburg * * This file is subject to the terms and conditions of the GNU Lesser * General Public License v2.1. See the file LICENSE in the top level @@ -14,11 +15,13 @@ * @brief Thread test application * * @author Vincent Dupont + * @author Marian Buschsieweke * * @} */ #include +#include #include "thread.h" #include "msg.h" @@ -44,46 +47,29 @@ static void timer_cb(void *arg) xtimer_set(&timer, OFFSET); } -static void *thread1(void *arg) +static void *thread_1_2_3(void *_arg) { - (void) arg; - + const char *arg = _arg; float f, init; - printf("THREAD %" PRIkernel_pid " start\n", thread_getpid()); + mutex_lock(&lock); + printf("THREAD %s start\n", arg); + mutex_unlock(&lock); - init = 1.0 * thread_getpid(); + /* Use number at end of thread name, e.g. 3 for "t3", to seed the calculation */ + init = 1.0 * (arg[strlen(arg) - 1] - '0'); f = init; while (1) { for (unsigned long i = 0; i < 10000ul; i++) { f = f + 1.0 / f; } - mutex_lock(&lock); - printf("T(%" PRIkernel_pid "): %f\n", thread_getpid(), (double)f); - mutex_unlock(&lock); - init += 1.0; - f = init; - } - return NULL; -} - -static void *thread2(void *arg) -{ - (void) arg; - - float f, init; - - printf("THREAD %" PRIkernel_pid " start\n", thread_getpid()); - - init = 1.0 * thread_getpid(); - f = init; - - while (1) { - for (unsigned long i = 0; i < 100000ul; i++) { - f = f + 1.0 / f; + /* only t1 and t3 should print */ + if (strcmp("t2", arg) != 0) { + mutex_lock(&lock); + printf("%s: %f\n", arg, (double)f); + mutex_unlock(&lock); } - init += 1.0; f = init; } return NULL; @@ -91,15 +77,18 @@ static void *thread2(void *arg) int main(void) { + const char *t1_name = "t1"; + const char *t2_name = "t2"; + const char *t3_name = "t3"; p1 = thread_create(t1_stack, sizeof(t1_stack), THREAD_PRIORITY_MAIN + 1, THREAD_CREATE_WOUT_YIELD | THREAD_CREATE_STACKTEST, - thread1, NULL, "nr1"); + thread_1_2_3, (void *)t1_name, t1_name); p2 = thread_create(t2_stack, sizeof(t2_stack), THREAD_PRIORITY_MAIN + 1, THREAD_CREATE_WOUT_YIELD | THREAD_CREATE_STACKTEST, - thread2, NULL, "nr2"); + thread_1_2_3, (void *)t2_name, t2_name); p3 = thread_create(t3_stack, sizeof(t3_stack), THREAD_PRIORITY_MAIN + 1, THREAD_CREATE_WOUT_YIELD | THREAD_CREATE_STACKTEST, - thread1, NULL, "nr3"); + thread_1_2_3, (void *)t3_name, t3_name); puts("THREADS CREATED\n"); timer.callback = timer_cb; diff --git a/tests/thread_float/tests/01-run.py b/tests/thread_float/tests/01-run.py new file mode 100755 index 0000000000..b317483390 --- /dev/null +++ b/tests/thread_float/tests/01-run.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2021 Otto-von-Guericke-Universität Magdeburg +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +# @author Marian Buschsieweke + +import sys +from testrunner import run + +MIN_PRINTS = 5 + + +def assertAlmostEqual(first, second, delta=0.05): + assert first + delta > second + assert first - delta < second + + +def same_computation_as_in_c_prog(thread_num): + f = 1.0 * thread_num + for _ in range(10000): + f = f + 1.0 / f + return f + + +def testfunc(child): + child.expect_exact("THREADS CREATED") + child.expect_exact("THREAD t1 start") + child.expect_exact("THREAD t2 start") + child.expect_exact("THREAD t3 start") + + child.expect(r"t(\d): (\d{3}\.\d+)\r\n") + first_thread = int(child.match.group(1)) + # Note: intentionally keeping the float output as string to also test that printf("%f", ...) + # prints the exact same char sequence for the same float value each time + first_result = child.match.group(2) + + # wait for second thread to print, but wait at most 50 messages + second_thread = None + for _ in range(50): + child.expect(r"t(\d): (\d{3}\.\d+)\r\n") + if int(child.match.group(1)) != first_thread: + second_thread = int(child.match.group(1)) + second_result = child.match.group(2) + break + + assert second_thread is not None, "both threads t1 and t3 should print" + assert first_thread in [1, 3], "only thread t1 and t3 should print" + assert second_thread in [1, 3], "only thread t1 and t3 should print" + assertAlmostEqual(float(first_result), same_computation_as_in_c_prog(first_thread)) + assertAlmostEqual(float(second_result), same_computation_as_in_c_prog(second_thread)) + + count_first_thread = 0 + count_second_thread = 0 + + # wait for both threads to print at least MIN_PRINTS times, but wait at most 100 messages + for _ in range(100): + child.expect(r"t(\d): (\d{3}\.\d+)\r\n") + thread = int(child.match.group(1)) + assert thread in [1, 3], "only thread t1 and t3 should print" + result = child.match.group(2) + + if thread == first_thread: + assert result == first_result, "same calculation but different result" + count_first_thread += 1 + else: + assert result == second_result, "same calculation but different result" + count_second_thread += 1 + + if (count_first_thread >= MIN_PRINTS) and (count_second_thread >= MIN_PRINTS): + break + + msg = f"Either t1 or t3 printed less than {MIN_PRINTS} times within 100 messages" + assert (count_first_thread >= MIN_PRINTS) and (count_second_thread >= MIN_PRINTS), msg + + +if __name__ == "__main__": + sys.exit(run(testfunc))