Как стать автором
Обновить

CMake: Подключение riscv-arch-test для тестирования имплементации rv32

Время на прочтение6 мин
Количество просмотров812

При написании своей VM для RISC-V возникла необходимость в тестировании.

Сначала я пытался писать юнит-тесты самостоятельно, но выходило, что я просто копирую логику из основной.

И по сути тестирую не соответствие спецификации, а соответствие моему пониманию.

Через некоторое время я наткнулся на официальный набор тестов для RISC-V и решил их использовать.

Это помогло найти несколько багов в моём коде.

Что ж.

Смотрим в репозиторий и огорчаемся - поддержки cmake там нет.

Ну она особо и не нужна, а нужны исходники тестов.

Ищем как в cmake скачать репозиторий -> ExternalProject -> ExternalProject_Add

В настройках нужно указать команды конфигурации и сборки(RTFM, please) но сейчас нужны только данные, по этому отключаем.

Тесты не всегда нужны, поэтому ввел настройку:

option(YETI_ENABLE_ARCH_TESTS "Build riscv-arch-tests tests" ON)

Подключаем данные:

if (YETI_ENABLE_ARCH_TESTS)
    enable_testing()

    ExternalProject_Add(riscv_arch_test
            GIT_REPOSITORY https://github.com/riscv-non-isa/riscv-arch-test.git
            GIT_TAG fc32e41d49480fd99ba0a192dfff9c3319b44873 # 3.10.0
            GIT_PROGRESS ON
            SOURCE_DIR "${DOWNLOAD_BASE_DIR}/riscv-arch-test"
            CONFIGURE_COMMAND ""
            BUILD_COMMAND ""
            INSTALL_COMMAND ""
            BUILD_IN_SOURCE ON
    )

    add_subdirectory(tests/arch-tests)
endif ()

Теперь нужно создать "запускатор" тестов и скомпилировать сами тесты под RV32IM

Тесты требуют определенной настройки под тестируемую архитектуру - создание model_test.h.
В нем описываются макросы для тестирования а-ля assert
У меня вышло следующее:

// config for https://github.com/riscv-non-isa/riscv-arch-test/
// docs: https://github.com/riscv-non-isa/riscv-arch-test/releases

#ifndef YETI_VM_MODEL_H
#define YETI_VM_MODEL_H

// Supports rv32
#define XLEN 32
// float32
#define FLEN 32

#define ALIGNMENT 2

#define TEST_CASE_1

// startup code
#define RVMODEL_BOOT \
    RVMODEL_IO_INIT  \

// stop code
#define RVMODEL_HALT \
.do_exit:            \
    li a7, 10;       \
    ecall;           \
    .global _assert_failed; \
    _assert_failed: ebreak; \
    j _assert_failed;\

#define RVMODEL_DATA_BEGIN \
    .align 4;              \
    .global begin_signature; \
    begin_signature:

#define RVMODEL_DATA_END \
    .align 4;            \
    .global end_signature; \
    end_signature:

#define RVMODEL_IO_INIT
#define RVMODEL_IO_WRITE_STR(_R, _STR)
#define RVMODEL_IO_CHECK()

// asserts: testreg, destreg, correctval
// store values in "DEV" memory
// generic purpose registers:
#define LBL_OK(_S, _R, _I, _L) .assert_ok ## _L

#define RVMODEL_IO_ASSERT_GPR_EQ_IMPL(_S, _R, _I, _L) \
    LI(_S, _I);                               \
    beq _S, _R, LBL_OK(_S, _R, _I, _L);          \
    LA(_S, __dev_start);                     \
    sw zero, 0(_S);                          \
    sw _R, 4(_S);                            \
    LI(_R, _I);                              \
    sw _R, 8(_S);                            \
    LI(_R, _L);                              \
    sw _R, 12(_S);                                    \
    j  _assert_failed;                                \
    LBL_OK(_S, _R, _I, _L):                           \

#define RVMODEL_IO_ASSERT_GPR_EQ(_S, _R, _I) \
    RVMODEL_IO_ASSERT_GPR_EQ_IMPL(_S, _R, _I, __LINE__); \

// float32 registers
#define RVMODEL_IO_ASSERT_SFPR_EQ(_F, _R, _I)
// float64 registers
#define RVMODEL_IO_ASSERT_DFPR_EQ(_D, _R, _I)

// machine-mode interrupts
// use default behavior - end test
// TODO: learn about it
//#define RVMODEL_SET_MSW_INT
//#define RVMODEL_CLEAR_MSW_INT
//#define RVMODEL_CLEAR_MTIMER_INT
//#define RVMODEL_CLEAR_MEXT_INT

#endif // YETI_VM_MODEL_H

В качестве запускатора служит код:

#include <yeti-vm/vm_basic.hxx>

#include <iostream>
#include <cstring>

namespace vm::yeti_runner
{
struct Runner: protected vm::basic_vm
{
    bool initProgram(int testIdx, char ** argv)
    {
        bool isa_ok = init_isa();
        bool mem_ok = init_memory();
        mem_ok = mem_ok && add_memory(std::make_shared<DeviceMemory>(this));

        bool init_ok = isa_ok && mem_ok;
        init_ok = init_ok && initSysCalls();

        auto code = vm::parse_hex(argv[testIdx]);
        init_ok = init_ok && code.has_value();
        init_ok = init_ok && set_program(code.value());

        return init_ok;
    }
    bool initSysCalls()
    {
        syscall_should_throw(false);
        using call = vm::syscall_functor;
        auto& sys = get_syscalls();
        bool ok = sys.register_handler(
                call::create(10, "exit"
                             , [this](vm::MachineInterface* m)
                             { return do_exit(m); }));

        return ok;
    }
    void do_exit(vm::MachineInterface*)
    {
        basic_vm::halt();
    }

    bool exec(bool debug = false)
    {
        enable_debugging(debug);
        start();
        try
        {
            run();
        }
        catch (std::exception& e)
        {
            std::cerr << std::endl << "Exception: " << e.what() << std::endl;
            dump_state(std::cerr);
            return false;
        }
        return !set_dev; // no failures
    }
protected:
    void debug() override
    {
        if (set_dev)
        {
            dump_state(std::cerr);
            auto fill_c = std::cerr.fill();
            std::cerr << std::dec;
            std::cerr << "set_dev == true " << std::endl;
            std::cerr << "DEV MEM: " << std::endl;
            for(auto v: dev_mem)
            {
                std::cerr << "\t" << std::hex << std::setfill('0') << std::setw(8) << v << std::endl;
            }
            std::cerr << "\t:DEV MEM" << std::endl;
            std::cerr << std::dec << std::setfill(fill_c);
            halt();
        }
        return basic_vm::debug();
    }
    void assert_set(uint32_t idx, uint32_t v)
    {
        dev_mem[idx] = v;
        set_dev = true;
    }
    bool set_dev = false;
    std::array<uint32_t, 4> dev_mem{};
protected:
    struct DeviceMemory final: public vm::memory_block
    {
        explicit DeviceMemory(Runner* runner)
            : vm::memory_block{def_data_base + def_data_size, def_data_size}
            , runner{runner} {}

        [[nodiscard]]
        bool load(address_type address, void *dest, size_type size) const final
        {
            std::memset(dest, 0, size);
            return true;
        }

        [[nodiscard]]
        bool store(memory_block::address_type address, const void *source, memory_block::size_type size) final
        {
            runner->set_dev = true;
            if (size != 4) return false;
            auto offset = (address - get_start_address()) / 4;
            if (offset >= runner->dev_mem.size()) return false;
            runner->assert_set(offset, *reinterpret_cast<const uint32_t*>(source));
            return true;
        }

    protected:
        [[nodiscard]]
        const void *get_ro(address_type address, size_type size) const final
        {
            return nullptr;
        }

        [[nodiscard]]
        void *get_rw(address_type address, size_type size) final
        {
            return nullptr;
        }
    private:
        Runner* runner = nullptr;
    };
};
} // vm::yeti_runner

int main(int argc, char ** argv)
{
    int numFails = 0;
    for (int testIdx = 1; testIdx < argc; ++testIdx)
    {
        vm::yeti_runner::Runner yetiVM;
        if (!yetiVM.initProgram(testIdx, argv))
        {
            std::cerr << "Unable init: " << std::dec << testIdx << " " << argv[testIdx] << std::endl;
            return EXIT_FAILURE;
        }
        if (!yetiVM.exec(argc == 2)) // single file - enable debug output
        {
            std::cerr << "Fail: " << std::dec << testIdx << " " << argv[testIdx] << std::endl;
            ++numFails;
        }
    }
    return numFails;
}

В нем используется memory-mapped device, которое обслуживает событие "завершение работы"

Для компиляции тестов нужен toolchain riscv64-unknown-elf

Для поиска используется функция find_riscv_toolchain

Собственно для компиляции и запуска тестов используется код

# use POST_BUILD step to compile tests
block()
    set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
    foreach (_subset IN LISTS ARCH_TEST_SUBSETS)
        set(_subset_dir "${ARCH_TEST_SUITE_RV32}/${_subset}/src/")
        file(GLOB _subset_tests RELATIVE "${_subset_dir}" "${_subset_dir}/*.S")
        message(DEBUG "Dir: ${_subset_dir} ... tests: ${_subset_tests}")

        set(_tests_to_run)

        foreach (_test_asm IN LISTS _subset_tests)
            get_filename_component(_test_name "${_test_asm}" NAME_WLE)
            set(_test_id "${_subset}/${_test_name}")
            message(DEBUG "Add test ${_test_id}")
            set(_input_file "${_subset_dir}/${_test_asm}")
            set(_elf_file "${_out_dir}/${_subset}_${_test_name}.elf")
            set(_hex_file "${_out_dir}/${_subset}_${_test_name}.hex")
            list(APPEND _build_args
                    "${YETI_VM_ARCH_ARGS}"
                    "${ARCH_TEST_INCLUDE_DIRS}"
                    -T "${CMAKE_CURRENT_LIST_DIR}/config/link.ld"
                    "${_input_file}"
                    -o "${_elf_file}"
            )
            add_custom_command(TARGET yeti-runner POST_BUILD
                    COMMENT "Build ${_test_id}"
                    COMMAND rv_tools::_gcc
                    ARGS "${_build_args}"
                    DEPENDS "${_input_file}"
                        "${CMAKE_CURRENT_LIST_DIR}/config/link.ld"
                        "${CMAKE_CURRENT_LIST_DIR}/config/model_test.h"
                    BYPRODUCTS "${_elf_file}"
                    COMMAND_EXPAND_LISTS
            )
            add_custom_command(TARGET yeti-runner POST_BUILD
                    COMMENT "Make hex file ${_test_id}"
                    COMMAND rv_tools::_objcopy
                    ARGS -O ihex "${_elf_file}" "${_hex_file}"
                    DEPENDS "${_elf_file}"
                    BYPRODUCTS "${_hex_file}"
            )

            list(APPEND _tests_to_run "${_hex_file}")

            unset(_input_file)
            unset(_elf_file)
            unset(_hex_file)
            unset(_build_args)
        endforeach ()

        add_test(NAME "RV32_ISA_${_subset}"
                COMMAND yeti-runner "${_tests_to_run}"
                COMMAND_EXPAND_LISTS
                )
        unset(_subset_dir)
        unset(_tests_to_run)
    endforeach ()
    unset(_subset)
    unset(_out_dir)
endblock()

В котором для каждого теста из выбранного набора происходит компиляция, преобразование в ihex и добавление команды для «запускатора».

Теги:
Хабы:
+4
Комментарии0

Публикации

Истории

Работа

QT разработчик
5 вакансий
Программист C++
74 вакансии

Ближайшие события

4 – 5 апреля
Геймтон «DatsCity»
Онлайн
8 апреля
Конференция TEAMLY WORK MANAGEMENT 2025
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область