#include "elna/tester.hpp" #include #include #include #include namespace elna { std::uint32_t test_results::total() const noexcept { return m_total; } std::uint32_t test_results::passed() const noexcept { return m_passed; } std::uint32_t test_results::failed() const noexcept { return m_total - m_passed; } int test_results::exit_code() const noexcept { return m_total == m_passed ? EXIT_SUCCESS : EXIT_FAILURE; } void test_results::add_exit_code(const test_status status) noexcept { ++m_total; if (status == test_status::successful) { ++m_passed; } } static std::filesystem::path in_build_directory() { return "build/riscv"; } static std::string in_build_directory(const std::filesystem::path& path) { return in_build_directory() / path; } static std::filesystem::path in_actual_directory() { return "build/tests"; } static std::filesystem::path in_root_directory() { return "build/root"; } static std::filesystem::path in_root_directory(const std::filesystem::path& path) { return in_root_directory() / path; } boost::process::v2::process_stdio get_output_streams(const std::uint8_t stream_number, boost::asio::readable_pipe& read_pipe) { if (stream_number == 1) { return boost::process::v2::process_stdio{ nullptr, read_pipe }; } if (stream_number == 2) { return boost::process::v2::process_stdio{ nullptr, {}, read_pipe }; } return boost::process::v2::process_stdio{ nullptr, read_pipe, read_pipe }; } int run_for_output(boost::asio::io_context& context, const std::filesystem::path& log_path, const std::filesystem::path& binary, std::initializer_list arguments) { boost::asio::readable_pipe read_pipe{ context }; std::string output; boost::asio::dynamic_string_buffer buffer = boost::asio::dynamic_buffer(output); boost::system::error_code ec; boost::process::v2::process_stdio process_stdio; boost::process::v2::process elna_child(context, binary, arguments, boost::process::v2::process_stdio{ nullptr, read_pipe, 1 }); std::ofstream log_file{ log_path }; do { std::size_t transferred = read_pipe.read_some(buffer.prepare(512), ec); if (transferred == 0) { break; } buffer.commit(transferred); log_file.write(boost::asio::buffer_cast(buffer.data()), buffer.size()); buffer.consume(transferred); } while (ec == boost::system::errc::success); return elna_child.wait(); } test_status build_test(boost::asio::io_context& context, const std::filesystem::directory_entry& test_entry) { const std::filesystem::path test_filename = test_entry.path().filename(); std::filesystem::path test_binary = in_root_directory("tests" / test_filename); std::filesystem::path test_object = in_build_directory(test_filename); std::filesystem::path test_log = in_build_directory(test_filename); test_binary.replace_extension(); test_object.replace_extension(".o"); test_log.replace_extension(".log"); std::filesystem::remove(test_binary); std::filesystem::remove(test_object); auto result_code = run_for_output(context, test_log, "./build/bin/elna", { "-o", test_object.string(), test_entry.path().string() }); if (result_code != 0) { return test_status::compile_failed; } test_status result = result_code == 0 ? test_status::successful : test_status::compile_failed; std::vector environment; std::vector linker_arguments = { "-o", test_binary.string() }; linker_arguments.push_back(test_object.string()); boost::process::v2::execute(boost::process::v2::process(context, boost::process::search_path("ld"), linker_arguments )); return result; } static test_status check_expectation(const std::filesystem::path& expectation_path, const std::filesystem::path& actual_directory) { std::filesystem::path log_filename = expectation_path.filename(); log_filename.replace_extension(".log"); std::filesystem::path test_log = actual_directory / log_filename; test_log.replace_extension(".log"); boost::process::child diff( boost::process::search_path("diff"), "-Nur", "--color", expectation_path, test_log ); diff.wait(); if (diff.exit_code() == 0) { return test_status::successful; } return test_status::expectation_failed; } void run_test(boost::asio::io_context& context, const std::filesystem::path& test_binary) { std::filesystem::path test_log = test_binary; test_log.replace_extension(".log"); run_for_output(context, test_log, boost::process::search_path("qemu-riscv32"), { test_binary.string() }); } static void cpio_archive(boost::asio::io_context& context) { boost::asio::readable_pipe read_pipe{ context }; boost::asio::writable_pipe write_pipe{ context }; std::string output; boost::asio::dynamic_string_buffer buffer = boost::asio::dynamic_buffer(output); boost::system::error_code ec; std::ofstream archive_output{ "build/root.cpio", std::ios::binary }; boost::process::v2::process_stdio process_stdio; auto current_path = std::filesystem::current_path(); std::filesystem::current_path(in_root_directory()); boost::process::v2::process cpio_child(context, boost::process::search_path("cpio"), { "-o", "--format=newc" }, boost::process::v2::process_stdio{ write_pipe, read_pipe, nullptr }); for (const auto& entry : std::filesystem::recursive_directory_iterator(".")) { auto entry_path = entry.path().string() + "\n"; auto entry_iterator = std::cbegin(entry_path); std::size_t written{ 0 }; while (entry_iterator != std::cend(entry_path)) { std::size_t written = write_pipe.write_some(boost::asio::buffer(entry_iterator.base(), std::distance(entry_iterator, std::cend(entry_path)))); std::advance(entry_iterator, written); } } write_pipe.close(); do { std::size_t transferred = read_pipe.read_some(buffer.prepare(512), ec); if (transferred == 0) { break; } buffer.commit(transferred); archive_output.write(boost::asio::buffer_cast(buffer.data()), buffer.size()); buffer.consume(transferred); } while (ec == boost::system::errc::success); std::filesystem::current_path(current_path); } static void run_vm(boost::asio::io_context& context) { std::vector arguments = { "-nographic", "-M", "virt", "-bios", "default", "-kernel", "build/tools/linux-5.15.159/arch/riscv/boot/Image", "-append", "quiet", "-initrd", "build/root.cpio" }; auto process = boost::process::v2::process(context, boost::process::search_path("qemu-system-riscv32"), arguments); boost::process::v2::execute(std::move(process)); } static void create_init() { std::filesystem::copy("build/tools/init", in_root_directory()); } static test_results run_in_path(const std::filesystem::path test_directory) { test_results results; boost::asio::io_context context; for (const auto& test_entry : std::filesystem::directory_iterator(test_directory)) { if (test_entry.path().extension() != ".eln" || !test_entry.is_regular_file()) { continue; } test_status result; std::cout << "Compiling " << test_entry << std::endl; try { build_test(context, test_entry); } catch (const boost::process::process_error& exception) { std::cout << exception.what() << std::endl; } } for (const auto& executable_test : std::filesystem::directory_iterator(in_build_directory())) { if (executable_test.path().has_extension()) { continue; } run_test(context, executable_test.path()); } for (const auto& expectation_entry : std::filesystem::directory_iterator(test_directory / "failures")) { auto test_entry = test_directory / expectation_entry.path().filename(); test_entry.replace_extension(".eln"); auto result = check_expectation(expectation_entry.path(), in_build_directory()); print_result(test_entry, result); results.add_exit_code(result); } std::filesystem::copy(test_directory / "expectations", in_root_directory("expectations"), std::filesystem::copy_options::recursive); create_init(); cpio_archive(context); run_vm(context); return results; } void print_result(const std::filesystem::path& test_entry, const test_status& result) { if (result != test_status::successful) { std::cout << test_entry << " failed." << std::endl; } } }; int main() { std::filesystem::create_directory(elna::in_build_directory()); std::filesystem::create_directory(elna::in_actual_directory()); std::filesystem::remove_all(elna::in_root_directory()); std::filesystem::create_directory(elna::in_root_directory()); std::filesystem::create_directory(elna::in_root_directory("expectations")); std::filesystem::create_directory(elna::in_root_directory("tests")); auto current_environment = boost::this_process::environment(); current_environment["PATH"] += "./build/tools/sysroot/bin"; std::cout << "Run all tests and check the results" << std::endl; std::filesystem::path test_directory{ "tests" }; const auto results = elna::run_in_path(test_directory); std::cout << std::endl; std::cout << results.total() << " tests run, " << results.passed() << " passed, " << results.failed() << " failed." << std::endl; return results.exit_code(); }