diff --git a/.github/utest/configs/components/dlmodule.cfg b/.github/utest/configs/components/dlmodule.cfg new file mode 100644 index 000000000000..89be1ae106bf --- /dev/null +++ b/.github/utest/configs/components/dlmodule.cfg @@ -0,0 +1,9 @@ +# Real ARM ELF fixtures, loaded through the filesystem and dlopen(). +CONFIG_RT_CONSOLEBUF_SIZE=1024 +CONFIG_RT_NAME_MAX=24 +CONFIG_RT_USING_CI_ACTION=y +CONFIG_RT_USING_MODULE=y +CONFIG_RT_USING_DFS=y +CONFIG_RT_USING_POSIX_FS=y +CONFIG_RT_UTEST_DLMODULE=y +CONFIG_UTEST_THR_STACK_SIZE=8192 diff --git a/.github/workflows/utest_auto_run.yml b/.github/workflows/utest_auto_run.yml index e05a26f1a963..760c10e1688b 100644 --- a/.github/workflows/utest_auto_run.yml +++ b/.github/workflows/utest_auto_run.yml @@ -152,6 +152,8 @@ jobs: config_file: "components/dfs.cfg" - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } config_file: "components/libc.cfg" + - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } + config_file: "components/dlmodule.cfg" env: TEST_QEMU_ARCH: ${{ matrix.platform.QEMU_ARCH }} @@ -300,6 +302,10 @@ jobs: fi scons --pyconfig-silent -C $TEST_BSP_ROOT + if [[ "$TEST_CONFIG_FILE" == "components/dlmodule.cfg" ]]; then + grep -q '^#define RT_UTEST_DLMODULE' $TEST_BSP_ROOT/rtconfig.h + python3 tools/ci/test_run_dlmodule_qemu.py + fi scons -j$(nproc) --strict -C $TEST_BSP_ROOT - name: QEMU Run Test @@ -312,7 +318,10 @@ jobs: mkfs.fat $TEST_BSP_ROOT/sd.bin fi - if [[ "${{ matrix.platform.QEMU_ARCH }}" == "aarch64" ]]; then + if [[ "$TEST_CONFIG_FILE" == "components/dlmodule.cfg" ]]; then + python3 tools/ci/run_dlmodule_qemu.py --kernel $TEST_BSP_ROOT/rtthread.bin \ + --disk $TEST_BSP_ROOT/sd.bin --log qemu_output_$TEST_QEMU_ARCH.log + elif [[ "${{ matrix.platform.QEMU_ARCH }}" == "aarch64" ]]; then sudo apt-get -yqq install device-tree-compiler if [[ "${{ matrix.platform.QEMU_MACHINE }}" == "virt" ]]; then qemu-system-$TEST_QEMU_ARCH -M virt -cpu max -smp 4 -m 128M -kernel $TEST_BSP_ROOT/rtthread.bin -nographic \ @@ -406,9 +415,11 @@ jobs: > qemu_output_$TEST_QEMU_ARCH.log 2>&1 & fi - QEMU_PID=$! - disown $QEMU_PID - + if [[ "$TEST_CONFIG_FILE" != "components/dlmodule.cfg" ]]; then + QEMU_PID=$! + disown $QEMU_PID + fi + - name: Monitor qemu log if: ${{ env.TOOLCHAIN_INSTALLED != '' && success() }} # Same known-flaky qemu-virt64-riscv standard kernel utest run as the previous step. diff --git a/components/libc/posix/libdl/SConscript b/components/libc/posix/libdl/SConscript index cf6648a4a979..f17b847282a8 100644 --- a/components/libc/posix/libdl/SConscript +++ b/components/libc/posix/libdl/SConscript @@ -8,5 +8,7 @@ CPPPATH = [cwd] if rtconfig.PLATFORM in ['gcc']: group = DefineGroup('Libc', src, depend = ['RT_USING_MODULE'], CPPPATH = CPPPATH) + if GetDepend('RT_UTEST_DLMODULE'): + group += SConscript('utest/SConscript') Return('group') diff --git a/components/libc/posix/libdl/utest/Kconfig b/components/libc/posix/libdl/utest/Kconfig new file mode 100644 index 000000000000..49ed556e25b7 --- /dev/null +++ b/components/libc/posix/libdl/utest/Kconfig @@ -0,0 +1,10 @@ +config RT_UTEST_DLMODULE + bool "dlmodule ELF loading and relocation tests (32-bit ARM GCC)" + depends on ARCH_ARM_CORTEX_A && !ARCH_CPU_64BIT && !RT_USING_SMART + depends on RT_USING_MODULE && RT_USING_DFS && RT_USING_POSIX_FS + default n + help + Build real 32-bit ARM ELF modules and test dlopen, dlsym and dlclose. + Requires GCC and a writable filesystem mounted at /. + Fixture generation uses -marm -mcpu=arm926ej-s and verifies the + GCC-generated ELF layout, section types and symbol properties. diff --git a/components/libc/posix/libdl/utest/SConscript b/components/libc/posix/libdl/utest/SConscript new file mode 100644 index 000000000000..0a6879422958 --- /dev/null +++ b/components/libc/posix/libdl/utest/SConscript @@ -0,0 +1,23 @@ +import os +import subprocess +import sys +from building import * + +Import('env') + +cwd = GetCurrentDir() +variant = Dir('.').abspath +fixture_sources = Glob('fixtures/*.c') + Glob('fixtures/*.ld') +builder = os.path.join(cwd, 'build_fixtures.py') + +def build_fixtures(target, source, env): + subprocess.check_call([sys.executable, builder, '--cc', env.subst('$CC'), + '--output', str(target[0])], env=env['ENV']) + +header = env.Command('dlmodule_fixtures.h', [builder] + fixture_sources, + Action(build_fixtures, 'Building dlmodule ELF fixtures')) +group = DefineGroup('dlmodule-utest', ['dlmodule_tc.c'], + depend=['RT_UTEST_DLMODULE'], CPPPATH=[cwd, variant]) +env.Depends(group, header) + +Return('group') diff --git a/components/libc/posix/libdl/utest/build_fixtures.py b/components/libc/posix/libdl/utest/build_fixtures.py new file mode 100644 index 000000000000..7ae6f6af9218 --- /dev/null +++ b/components/libc/posix/libdl/utest/build_fixtures.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Build ARM test modules from source; never commit generated ELF/header files.""" +import argparse +from pathlib import Path +import shlex +import struct +import subprocess + + +def verify_fixture(name, data): + """Check the ELF shape so a compiler change cannot silently remove a test.""" + if data[:6] != b'\x7fELF\x01\x01': + raise RuntimeError(name + ': expected little-endian ELF32') + kind, machine = struct.unpack_from(' 0 for s in sections) + elif name == 'db_multi': + assert section_names[symbols['bss_value'][5]] == '.bss' + assert section_names[symbols['common_value'][5]] == '.common' + assert sections[symbols['bss_value'][5]][1] == 8 + assert sections[symbols['common_value'][5]][1] == 8 + elif name == 'dn_addr': + assert sections[symbols['probe'][5]][3] == 0x1000 and symbols['probe'][1] == 0 + assert sections[symbols['initialized'][5]][3] == 0x2000 + elif name == 'dd_dbg': + assert any(s[1] == 9 and section_names[s[7]].startswith('.debug') for s in sections) + elif name == 'dt_text': + assert symbols['first'][5] != symbols['second'][5] + assert symbols['first'][1] == symbols['second'][1] == 0 + elif name in ('du_sym', 'dz_none'): + assert symbols['external_func'][5] == 0 and symbols['external_func'][3] == 0x12 + if name == 'dz_none': + assert not any(s[3] == 0x12 and s[5] != 0 for s in symbols.values()) + elif name == 'dg_imp': + # Dynamic symbols are in .dynsym; inspect the string table as well. + assert b'rt_kprintf\0' in data + + +def build(cc, output): + sources = Path(__file__).resolve().parent / 'fixtures' + directory = output.parent / 'dlmodule-fixtures' + directory.mkdir(parents=True, exist_ok=True) + # ARMv5 literal-pool relocations are supported by libdl/arch/arm.c. + # Cortex-A9 also executes these instructions; avoid unsupported MOVW/MOVT relocations. + flags = ['-marm', '-mcpu=arm926ej-s', '-O0', '-fcommon', '-fno-pic', + '-fno-stack-protector', '-fno-unwind-tables', '-fno-asynchronous-unwind-tables'] + modules = [ + ('dg_rel', 'globals', [], []), + ('dg_dyn', 'globals', ['-fPIC', '-mlong-calls', '-DDYNAMIC_FIXTURE'], ['-shared']), + ('dg_imp', 'globals', ['-fPIC', '-mlong-calls', '-DDYNAMIC_FIXTURE', '-DKERNEL_IMPORT'], ['-shared']), + ('dc_ptr', 'common_target', [], []), + ('db_multi', 'multi_bss', [], ['-Wl,-d,-T,' + str(sources / 'multi_bss.ld')]), + ('dn_addr', 'sections', [], ['-Wl,-T,' + str(sources / 'nonzero.ld')]), + ('dd_dbg', 'sections', ['-g'], []), + ('dt_text', 'multi_text', [], []), + ('du_sym', 'undefined', [], []), + ('dz_none', 'undefined', ['-DZERO_EXPORTS'], []), + ] + lines = ['/* Generated ELF fixtures; do not edit. */', + 'struct dlmodule_fixture { const char *name; const unsigned char *data; unsigned size; };'] + for name, source, extra, link in modules: + obj = directory / (name + '.o') + elf = directory / (name + '.mo') + subprocess.run(cc + flags + extra + ['-c', str(sources / (source + '.c')), '-o', str(obj)], check=True) + mode = [] if '-shared' in link else ['-r'] + subprocess.run(cc + ['-nostdlib'] + mode + link + [str(obj), '-o', str(elf)], check=True) + data = elf.read_bytes() + verify_fixture(name, data) + lines.append('static const unsigned char ' + name + '[] = {') + for offset in range(0, len(data), 16): + lines.append(' ' + ', '.join('0x%02x' % byte for byte in data[offset:offset + 16]) + ',') + lines.append('};') + lines.append('static const struct dlmodule_fixture dlmodule_fixtures[] = {') + for name, _, _, _ in modules: + lines.append(' {"%s.mo", %s, sizeof(%s)},' % (name, name, name)) + lines.append('};') + output.write_text('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cc', required=True) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + build(shlex.split(args.cc), args.output.resolve()) diff --git a/components/libc/posix/libdl/utest/dlmodule_tc.c b/components/libc/posix/libdl/utest/dlmodule_tc.c new file mode 100644 index 000000000000..8f1f16e70996 --- /dev/null +++ b/components/libc/posix/libdl/utest/dlmodule_tc.c @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "utest.h" +#include "../dlmodule.h" +#include "dlmodule_fixtures.h" + +#define TEST_DIR "/dlmutest" + +typedef uint32_t *(*probe_fn)(void); +typedef int (*value_fn)(void); + +static rt_bool_t in_module(void *handle, const void *address, rt_size_t size) +{ + struct rt_dlmodule *module = handle; + rt_ubase_t base = (rt_ubase_t)module->mem_space; + rt_ubase_t value = (rt_ubase_t)address; + + return address != RT_NULL && value >= base && size <= module->mem_size && + value - base <= module->mem_size - size; +} + +static void fixture_path(char *path, rt_size_t size, unsigned index) +{ + rt_snprintf(path, size, "%s/%s", TEST_DIR, dlmodule_fixtures[index].name); +} + +static void *load_fixture(unsigned index) +{ + const struct dlmodule_fixture *fixture = &dlmodule_fixtures[index]; + char path[64]; + unsigned written = 0; + int fd; + + fixture_path(path, sizeof(path), index); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + uassert_true(fd >= 0); + if (fd < 0) + { + return RT_NULL; + } + + while (written < fixture->size) + { + int count = write(fd, fixture->data + written, fixture->size - written); + if (count <= 0) + { + uassert_true(count > 0); + close(fd); + return RT_NULL; + } + written += count; + } + if (close(fd) != 0) + { + uassert_true(0); + return RT_NULL; + } + + return dlopen(path, RTLD_NOW); +} + +static uint32_t *get_results(void *handle, unsigned count) +{ + probe_fn probe = (probe_fn)dlsym(handle, "probe"); + uint32_t *result; + rt_bool_t valid; + + valid = in_module(handle, (void *)probe, 4); + uassert_true(valid); + if (!valid) + { + return RT_NULL; + } + result = probe(); + valid = in_module(handle, result, count * sizeof(*result)); + uassert_true(valid); + return valid ? result : RT_NULL; +} + +static void close_fixture(void *handle) +{ + uassert_int_equal(dlclose(handle), RT_TRUE); +} + +static void globals_test(unsigned index) +{ + void *handle = load_fixture(index); + uint32_t *result; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + + result = get_results(handle, 8); + if (result != RT_NULL) + { + rt_bool_t addresses_valid = RT_TRUE; + for (unsigned i = 0; i < 3; i++) + { + rt_bool_t valid = in_module(handle, (void *)result[i], sizeof(uint32_t)); + uassert_true(valid); + addresses_valid = addresses_valid && valid; + } + uassert_int_not_equal(result[0], result[1]); + uassert_int_not_equal(result[1], result[2]); + uassert_int_not_equal(result[0], result[2]); + if (index == 0) + { + uassert_int_equal(result[2] & 15, 0); + } + uassert_int_equal(result[3], 7); + uassert_int_equal(result[4], 0); + uassert_int_equal(result[5], 0); + uassert_int_equal(result[6], 0); + uassert_true((void *)result[7] == dlsym(handle, "probe")); + if (addresses_valid) + { + uassert_int_equal(*(uint32_t *)result[0], 0x11223344); + uassert_int_equal(*(uint32_t *)result[1], 0x55667788); + uassert_int_equal(*(uint32_t *)result[2], 0x12345678); + } + } + close_fixture(handle); +} + +static void globals_rel(void) +{ + globals_test(0); +} + +static void globals_dyn(void) +{ + globals_test(1); +} + +static void kernel_import(void) +{ + globals_test(2); +} + +static void common_target(void) +{ + void *handle = load_fixture(3); + uint32_t *result; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + result = get_results(handle, 4); + if (result != RT_NULL) + { + uassert_true(in_module(handle, (void *)result[0], 4)); + uassert_int_equal(result[0], result[1]); + uassert_int_equal(result[2], 42); + uassert_int_equal(result[3], 0); + } + close_fixture(handle); +} + +static void multi_nobits(void) +{ + void *handle = load_fixture(4); + uint32_t *result; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + result = get_results(handle, 6); + if (result != RT_NULL) + { + uassert_true(in_module(handle, (void *)result[0], 4)); + uassert_true(in_module(handle, (void *)result[1], 4)); + uassert_int_not_equal(result[0], result[1]); + uassert_int_equal(result[2], 0); + uassert_int_equal(result[3], 0); + uassert_int_equal(result[4], 0x11223344); + uassert_int_equal(result[5], 0x55667788); + } + close_fixture(handle); +} + +static void sections_test(unsigned index) +{ + void *handle = load_fixture(index); + uint32_t *result; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + result = get_results(handle, 4); + if (result != RT_NULL) + { + uassert_int_equal(result[0], 7); + uassert_int_equal(result[1], 9); + uassert_int_equal(result[2], 0); + uassert_true((void *)result[3] == dlsym(handle, "probe")); + } + close_fixture(handle); +} + +static void nonzero_sections(void) +{ + sections_test(5); +} + +static void debug_sections(void) +{ + sections_test(6); +} + +static void multi_text(void) +{ + void *handle = load_fixture(7); + value_fn first, second; + rt_bool_t valid; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + first = (value_fn)dlsym(handle, "first"); + second = (value_fn)dlsym(handle, "second"); + valid = in_module(handle, (void *)first, 4) && in_module(handle, (void *)second, 4); + uassert_true(valid); + uassert_true(first != second); + if (valid) + { + uassert_int_equal(first(), 11); + uassert_int_equal(second(), 22); + } + close_fixture(handle); +} + +static void undefined_symbol(void) +{ + void *handle = load_fixture(8); + value_fn present; + rt_bool_t valid; + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + uassert_null(dlsym(handle, "external_func")); + present = (value_fn)dlsym(handle, "present"); + valid = in_module(handle, (void *)present, 4); + uassert_true(valid); + if (valid) + { + uassert_int_equal(present(), 42); + } + close_fixture(handle); +} + +static void zero_exports(void) +{ + void *handle = load_fixture(9); + + uassert_not_null(handle); + if (handle == RT_NULL) + { + return; + } + uassert_int_equal(((struct rt_dlmodule *)handle)->nsym, 0); + uassert_null(dlsym(handle, "external_func")); + close_fixture(handle); +} + +static rt_err_t test_init(void) +{ + struct stat st; + + if (stat(TEST_DIR, &st) == 0) + { + return S_ISDIR(st.st_mode) ? RT_EOK : -RT_ERROR; + } + return mkdir(TEST_DIR, 0700) == 0 ? RT_EOK : -RT_ERROR; +} + +static rt_err_t test_cleanup(void) +{ + char path[64]; + + for (unsigned i = 0; i < sizeof(dlmodule_fixtures) / sizeof(dlmodule_fixtures[0]); i++) + { + fixture_path(path, sizeof(path), i); + unlink(path); + } + return rmdir(TEST_DIR) == 0 ? RT_EOK : -RT_ERROR; +} + +static void testcase(void) +{ + UTEST_UNIT_RUN(globals_rel); + UTEST_UNIT_RUN(globals_dyn); + UTEST_UNIT_RUN(kernel_import); + UTEST_UNIT_RUN(common_target); + UTEST_UNIT_RUN(multi_nobits); + UTEST_UNIT_RUN(nonzero_sections); + UTEST_UNIT_RUN(debug_sections); + UTEST_UNIT_RUN(multi_text); + UTEST_UNIT_RUN(undefined_symbol); + UTEST_UNIT_RUN(zero_exports); +} + +UTEST_TC_EXPORT(testcase, "components.libc.posix.libdl.regression", test_init, test_cleanup, 30); diff --git a/components/libc/posix/libdl/utest/fixtures/common_target.c b/components/libc/posix/libdl/utest/fixtures/common_target.c new file mode 100644 index 000000000000..404346fcc333 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/common_target.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include + +int common_value; +int *common_pointer = &common_value; +static uint32_t results[4]; + +int function(void) +{ + return 42; +} + +uint32_t *probe(void) +{ + results[0] = (uint32_t)&common_value; + results[1] = (uint32_t)common_pointer; + results[2] = function(); + results[3] = common_value; + return results; +} diff --git a/components/libc/posix/libdl/utest/fixtures/globals.c b/components/libc/posix/libdl/utest/fixtures/globals.c new file mode 100644 index 000000000000..137344430e67 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/globals.c @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include + +int common_value; +#ifdef DYNAMIC_FIXTURE +int aligned_value; +#else +int aligned_value __attribute__((aligned(16))); +#endif +int initialized = 7; +static int bss_value; +static uint32_t results[8]; + +#ifdef KERNEL_IMPORT +extern int rt_kprintf(const char *, ...); +#endif + +uint32_t *probe(void) +{ +#ifdef KERNEL_IMPORT + rt_kprintf("dlmodule fixture: kernel import\n"); +#endif + results[0] = (uint32_t)&bss_value; + results[1] = (uint32_t)&common_value; + results[2] = (uint32_t)&aligned_value; + results[3] = initialized; + results[4] = bss_value; + results[5] = common_value; + results[6] = aligned_value; + results[7] = (uint32_t)&probe; + bss_value = 0x11223344; + common_value = 0x55667788; + aligned_value = 0x12345678; + return results; +} diff --git a/components/libc/posix/libdl/utest/fixtures/multi_bss.c b/components/libc/posix/libdl/utest/fixtures/multi_bss.c new file mode 100644 index 000000000000..847508dff54e --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/multi_bss.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include + +int bss_value = 0; +int common_value; +static uint32_t results[6]; + +uint32_t *probe(void) +{ + results[0] = (uint32_t)&bss_value; + results[1] = (uint32_t)&common_value; + results[2] = bss_value; + results[3] = common_value; + bss_value = 0x11223344; + common_value = 0x55667788; + results[4] = bss_value; + results[5] = common_value; + return results; +} diff --git a/components/libc/posix/libdl/utest/fixtures/multi_bss.ld b/components/libc/posix/libdl/utest/fixtures/multi_bss.ld new file mode 100644 index 000000000000..5802281f93a8 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/multi_bss.ld @@ -0,0 +1,7 @@ +SECTIONS +{ + .text 0 : { *(.text*) } + .data 0 : { *(.data*) } + .bss 0 : { *(.bss*) } + .common 0 : { *(COMMON) } +} diff --git a/components/libc/posix/libdl/utest/fixtures/multi_text.c b/components/libc/posix/libdl/utest/fixtures/multi_text.c new file mode 100644 index 000000000000..89ef63a8e93a --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/multi_text.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +__attribute__((section(".text.first"))) int first(void) +{ + return 11; +} + +__attribute__((section(".text.second"))) int second(void) +{ + return 22; +} diff --git a/components/libc/posix/libdl/utest/fixtures/nonzero.ld b/components/libc/posix/libdl/utest/fixtures/nonzero.ld new file mode 100644 index 000000000000..21eb78a31624 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/nonzero.ld @@ -0,0 +1,7 @@ +SECTIONS +{ + .text 0x1000 : { *(.text*) } + .rodata 0x1800 : { *(.rodata*) } + .data 0x2000 : { *(.data*) } + .bss 0x3000 : { *(.bss*) } +} diff --git a/components/libc/posix/libdl/utest/fixtures/sections.c b/components/libc/posix/libdl/utest/fixtures/sections.c new file mode 100644 index 000000000000..02cbc0ea22b7 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/sections.c @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include + +int initialized = 7; +static const int rodata_value = 9; +static int bss_value; +static uint32_t results[4]; + +uint32_t *probe(void) +{ + results[0] = initialized; + results[1] = *(volatile const int *)&rodata_value; + results[2] = bss_value; + results[3] = (uint32_t)&probe; + return results; +} diff --git a/components/libc/posix/libdl/utest/fixtures/undefined.c b/components/libc/posix/libdl/utest/fixtures/undefined.c new file mode 100644 index 000000000000..97eab30f33d1 --- /dev/null +++ b/components/libc/posix/libdl/utest/fixtures/undefined.c @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Keep a GLOBAL/FUNC/UND symbol without introducing an import relocation. */ +__asm__(".global external_func\n.type external_func, %function\n"); + +#ifdef ZERO_EXPORTS +int initialized = 7; +#else +int present(void) +{ + return 42; +} +#endif diff --git a/components/utilities/utest/Kconfig b/components/utilities/utest/Kconfig index d6daaf14295c..2855acd3ae09 100644 --- a/components/utilities/utest/Kconfig +++ b/components/utilities/utest/Kconfig @@ -28,6 +28,7 @@ menu "RT-Thread Utestcases" rsource "../../../components/dfs/utest/Kconfig" rsource "../../../components/dfs/dfs_v2/filesystems/tmpfs/utest/Kconfig" rsource "../../../components/libc/compilers/common/utest/Kconfig" + rsource "../../../components/libc/posix/libdl/utest/Kconfig" rsource "../../../components/libc/cplusplus/utest/Kconfig" rsource "../../../components/lwp/utest/Kconfig" rsource "../../../components/mm/utest/Kconfig" diff --git a/src/defunct.c b/src/defunct.c index 18d686903580..7b9bea5d3cad 100644 --- a/src/defunct.c +++ b/src/defunct.c @@ -12,6 +12,10 @@ #include #include +#ifdef RT_USING_MODULE +#include +#endif + #ifndef SYSTEM_THREAD_STACK_SIZE #define SYSTEM_THREAD_STACK_SIZE IDLE_THREAD_STACK_SIZE #endif diff --git a/tools/ci/run_dlmodule_qemu.py b/tools/ci/run_dlmodule_qemu.py new file mode 100644 index 000000000000..be12eb510cd8 --- /dev/null +++ b/tools/ci/run_dlmodule_qemu.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Run the ARM dlmodule suite, requiring every expected utest to pass.""" +import argparse +from pathlib import Path +import re +import selectors +import subprocess +import time + + +CASES = { + 'globals_rel', 'globals_dyn', 'kernel_import', 'common_target', 'multi_nobits', + 'nonzero_sections', 'debug_sections', 'multi_text', 'undefined_symbol', 'zero_exports', +} +PREFIX = 'components.libc.posix.libdl.' + + +def run(args): + command = [args.qemu, '-M', 'vexpress-a9', '-smp', '2', '-m', '128M', + '-kernel', str(args.kernel.resolve()), '-nographic', '-monitor', 'none', + '-drive', f'file={args.disk.resolve()},if=sd,format=raw'] + args.log.parent.mkdir(parents=True, exist_ok=True) + process = subprocess.Popen(command, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + units = set() + passed = False + deadline = time.monotonic() + args.timeout + pending = b'' + selector = selectors.DefaultSelector() + selector.register(process.stdout, selectors.EVENT_READ) + try: + with args.log.open('w', encoding='utf-8') as log: + while time.monotonic() < deadline: + for key, _ in selector.select(timeout=0.2): + chunk = key.fileobj.read1(4096) + if not chunk: + raise RuntimeError('QEMU exited before the test completion marker') + pending += chunk + while b'\n' in pending: + raw, pending = pending.split(b'\n', 1) + line = raw.decode('utf-8', errors='replace').rstrip('\r') + print(line, flush=True) + log.write(line + '\n') + log.flush() + if '[ FAILED ]' in line or re.search( + r'assert(?:ion)? failed|data abort|prefetch abort|undefined instruction', + line, re.IGNORECASE): + raise RuntimeError('Test failure or CPU exception detected') + match = re.search(r'\[==========\] utest unit name: \(([^)]*)\)', line) + if match: + units.add(match.group(1)) + if '[ PASSED ] [ result ] testcase (' + PREFIX + 'regression)' in line: + passed = True + if '[==========] [ utest ] finished' in line: + missing = CASES - units + if missing: + raise RuntimeError('Missing passing tests: ' + ', '.join(sorted(missing))) + if not passed: + raise RuntimeError('The dlmodule suite did not report success') + print(f'DLMODULE_QEMU_PASS cases={len(CASES)}', flush=True) + return + raise RuntimeError(f'QEMU test timeout after {args.timeout} seconds') + finally: + selector.close() + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + process.stdout.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--kernel', type=Path, required=True) + parser.add_argument('--disk', type=Path, required=True) + parser.add_argument('--log', type=Path, default=Path('qemu_output_arm.log')) + parser.add_argument('--qemu', default='qemu-system-arm') + parser.add_argument('--timeout', type=float, default=120) + arguments = parser.parse_args() + try: + run(arguments) + except (OSError, RuntimeError) as error: + parser.exit(1, f'dlmodule QEMU test failed: {error}\n') diff --git a/tools/ci/test_run_dlmodule_qemu.py b/tools/ci/test_run_dlmodule_qemu.py new file mode 100644 index 000000000000..4b65b55f0733 --- /dev/null +++ b/tools/ci/test_run_dlmodule_qemu.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Exercise runner exit conditions without requiring a board or QEMU.""" +from contextlib import redirect_stdout +import importlib.util +import io +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest + +spec = importlib.util.spec_from_file_location('dlmodule_runner', Path(__file__).with_name('run_dlmodule_qemu.py')) +runner = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runner) + + +class RunnerTests(unittest.TestCase): + def execute(self, lines, delay=0, timeout=3): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fake = root / 'qemu' + fake.write_text('#!/usr/bin/env python3\nimport time\n' + + f'for line in {lines!r}: print(line, flush=True)\n' + + f'time.sleep({delay})\n') + fake.chmod(0o755) + args = SimpleNamespace(qemu=str(fake), kernel=root/'kernel', disk=root/'disk', + log=root/'log', timeout=timeout) + with redirect_stdout(io.StringIO()): + runner.run(args) + + def passing_lines(self): + return [f'[==========] utest unit name: ({name})' for name in sorted(runner.CASES)] + [ + '[ PASSED ] [ result ] testcase (' + runner.PREFIX + 'regression)', + '[==========] [ utest ] finished'] + + def test_all_cases_required(self): + self.execute(self.passing_lines()) + + def test_empty_suite_fails(self): + with self.assertRaisesRegex(RuntimeError, 'Missing passing tests'): + self.execute(['[==========] [ utest ] finished']) + + def test_missing_unit_fails(self): + with self.assertRaisesRegex(RuntimeError, 'Missing passing tests'): + self.execute(self.passing_lines()[1:]) + + def test_failure_before_success_fails(self): + with self.assertRaisesRegex(RuntimeError, 'failure'): + self.execute(['[ FAILED ] [ result ] testcase (broken)'] + self.passing_lines()) + + def test_suite_success_required(self): + lines = self.passing_lines() + del lines[-2] + with self.assertRaisesRegex(RuntimeError, 'did not report success'): + self.execute(lines) + + def test_early_exit_fails(self): + with self.assertRaisesRegex(RuntimeError, 'exited'): + self.execute([]) + + def test_cpu_exception_fails(self): + with self.assertRaisesRegex(RuntimeError, 'exception'): + self.execute(['data abort: exception']) + + def test_timeout_fails(self): + with self.assertRaisesRegex(RuntimeError, 'timeout'): + self.execute([], delay=10, timeout=0.2) + + +if __name__ == '__main__': + unittest.main()