При написании своей 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 и добавление команды для «запускатора»
ссылка на оригинал статьи https://habr.com/ru/articles/896986/
Добавить комментарий