При написании своей 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 и добавление команды для «запускатора».