diff options
Diffstat (limited to 'kernel')
23 files changed, 1262 insertions, 32 deletions
diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt index cb3d8a5..1b71a5f 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable("kernel" +add_library("kernel_objs" OBJECT # Platform-independent KAPI implementation "kapi/boot_modules.cpp" "kapi/cio.cpp" @@ -12,10 +12,8 @@ add_executable("kernel" "kstd/print.cpp" # Kernel Implementation - "src/main.cpp" "src/memory/bitmap_allocator.cpp" "src/memory/block_list_allocator.cpp" - "src/memory/operators.cpp" "src/memory.cpp" "src/devices/block_device.cpp" "src/devices/block_device_utils.cpp" @@ -40,38 +38,101 @@ add_executable("kernel" "src/filesystem/vfs.cpp" ) -target_include_directories("kernel" PRIVATE +target_include_directories("kernel_objs" PUBLIC "include" ) -target_link_libraries("kernel" PRIVATE - "os::arch" +target_link_libraries("kernel_objs" PUBLIC "os::kapi" ) -target_link_options("kernel" PRIVATE - "-T${KERNEL_LINKER_SCRIPT}" - "-no-pie" - "-nostdlib" -) - -set_property(TARGET "kernel" - APPEND - PROPERTY LINK_DEPENDS - "${KERNEL_LINKER_SCRIPT}" -) - file(GLOB_RECURSE KERNEL_HEADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "include/**.hpp") -target_sources("kernel" PUBLIC +target_sources("kernel_objs" PUBLIC FILE_SET HEADERS BASE_DIRS "include" FILES ${KERNEL_HEADERS} ) -target_disassemble("kernel") -target_extract_debug_symbols("kernel") -target_strip("kernel") +add_library("os::kernel" ALIAS "kernel_objs") + +if(CMAKE_CROSSCOMPILING) + add_executable("kernel" + "src/main.cpp" + "src/memory/operators.cpp" + ) + + target_link_libraries("kernel" PRIVATE + "os::arch" + "os::kernel" + ) + + target_link_options("kernel" PRIVATE + "-T${KERNEL_LINKER_SCRIPT}" + "-no-pie" + "-nostdlib" + ) + + set_property(TARGET "kernel" + APPEND + PROPERTY LINK_DEPENDS + "${KERNEL_LINKER_SCRIPT}" + ) + + target_disassemble("kernel") + target_extract_debug_symbols("kernel") + target_strip("kernel") + + target_generate_bootable_iso("kernel") +else() + enable_coverage("kernel_objs") + + add_library("kernel_test_support" OBJECT + "src/test_support/kapi/cpu.cpp" + "src/test_support/kapi/cio.cpp" + "src/test_support/kapi/interrupts.cpp" + "src/test_support/kapi/memory.cpp" + "src/test_support/log_buffer.cpp" + "src/test_support/page_mapper.cpp" + "src/test_support/simulated_memory.cpp" + "src/test_support/state_reset_listener.cpp" + ) + add_library("os::kernel_test_support" ALIAS "kernel_test_support") + + target_link_libraries("kernel_test_support" PUBLIC + "os::kernel" + "Catch2::Catch2WithMain" + ) + + add_executable("kernel_tests" + # KAPI Shim Tests + "kapi/cpu.tests.cpp" + "kapi/system.tests.cpp" + + # KSTD Shim Tests + "kstd/print.tests.cpp" + + # Memory Subsystem Tests + "src/memory/bitmap_allocator.tests.cpp" + "src/memory/block_list_allocator.tests.cpp" + + # Storage Subsystem Tests + "src/devices/storage/ram_disk/device.tests.cpp" + ) + add_executable("os::kernel_tests" ALIAS "kernel_tests") + + target_link_libraries("kernel_tests" PRIVATE + "os::kernel" + "os::kernel_test_support" + ) + + set_target_properties("kernel_tests" PROPERTIES + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + ) + + enable_coverage("kernel_tests") + catch_discover_tests("os::kernel_tests") +endif() -target_generate_bootable_iso("kernel") diff --git a/kernel/include/kernel/test_support/bump_frame_allocator.hpp b/kernel/include/kernel/test_support/bump_frame_allocator.hpp new file mode 100644 index 0000000..5203565 --- /dev/null +++ b/kernel/include/kernel/test_support/bump_frame_allocator.hpp @@ -0,0 +1,32 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_BUMP_FRAME_ALLOCATOR_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_BUMP_FRAME_ALLOCATOR_HPP + +#include "kapi/memory.hpp" + +#include <cstddef> +#include <optional> +#include <utility> + +namespace kernel::tests +{ + + struct bump_frame_allocator : kapi::memory::frame_allocator + { + auto mark_used(kapi::memory::frame) -> void override {} + + auto allocate_many(std::size_t count) noexcept + -> std::optional<std::pair<kapi::memory::frame, std::size_t>> override + { + auto start = next_free_frame; + next_free_frame += count; + return std::pair{kapi::memory::frame{start}, count}; + } + + auto release_many(std::pair<kapi::memory::frame, std::size_t>) -> void override {} + + std::size_t next_free_frame{}; + }; + +} // namespace kernel::tests + +#endif
\ No newline at end of file diff --git a/kernel/include/kernel/test_support/cpu.hpp b/kernel/include/kernel/test_support/cpu.hpp new file mode 100644 index 0000000..5445473 --- /dev/null +++ b/kernel/include/kernel/test_support/cpu.hpp @@ -0,0 +1,18 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_CPU_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_CPU_HPP + +#include <stdexcept> + +namespace kernel::tests::cpu +{ + + struct halt : std::runtime_error + { + halt() + : std::runtime_error{"CPU halt requested!"} + {} + }; + +} // namespace kernel::tests::cpu + +#endif
\ No newline at end of file diff --git a/kernel/include/kernel/test_support/log_buffer.hpp b/kernel/include/kernel/test_support/log_buffer.hpp new file mode 100644 index 0000000..581b32f --- /dev/null +++ b/kernel/include/kernel/test_support/log_buffer.hpp @@ -0,0 +1,32 @@ +#ifndef KERNEL_TEST_SUPPORT_LOG_BUFFER_HPP +#define KERNEL_TEST_SUPPORT_LOG_BUFFER_HPP + +#include <string> +#include <vector> + +namespace kernel::tests::log_buffer +{ + + //! Append a message to the testing log buffer. + //! + //! @param message The message to append. + auto append(std::string const & message) -> void; + + //! Clear the testing log buffer. + auto clear() -> void; + + //! Get the testing log buffer as a single string. + //! + //! @return The testing log buffer as a single string. + auto flat_messages() -> std::string; + + //! Get the testing log buffer. + //! + //! @note Messages may be split across multiple entries if they are longer than the internal kernel print buffer size. + //! + //! @return The testing log buffer. + auto messages() -> std::vector<std::string> const &; + +} // namespace kernel::tests::log_buffer + +#endif
\ No newline at end of file diff --git a/kernel/include/kernel/test_support/memory.hpp b/kernel/include/kernel/test_support/memory.hpp new file mode 100644 index 0000000..c6b7449 --- /dev/null +++ b/kernel/include/kernel/test_support/memory.hpp @@ -0,0 +1,11 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_MEMORY_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_MEMORY_HPP + +#include "kapi/memory.hpp" + +namespace kernel::tests::memory +{ + auto heap_base() -> kapi::memory::linear_address; +} + +#endif
\ No newline at end of file diff --git a/kernel/include/kernel/test_support/page_mapper.hpp b/kernel/include/kernel/test_support/page_mapper.hpp new file mode 100644 index 0000000..a40aa2e --- /dev/null +++ b/kernel/include/kernel/test_support/page_mapper.hpp @@ -0,0 +1,33 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_PAGE_MAPPER_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_PAGE_MAPPER_HPP + +#include "kapi/memory.hpp" + +#include "kernel/test_support/simulated_memory.hpp" + +#include <kstd/units> + +#include <cstddef> +#include <cstdint> +#include <unordered_map> + +namespace kernel::tests +{ + + struct page_mapper : kapi::memory::page_mapper + { + explicit page_mapper(kstd::units::bytes memory_size); + + auto map(kapi::memory::page page, kapi::memory::frame frame, flags) -> std::byte * override; + + auto unmap(kapi::memory::page page) -> void override; + + auto try_unmap(kapi::memory::page page) noexcept -> bool override; + + kernel::tests::simulated_memory memory; + std::unordered_map<std::uint64_t, kapi::memory::frame> page_mappings; + }; + +} // namespace kernel::tests + +#endif
\ No newline at end of file diff --git a/kernel/include/kernel/test_support/simulated_memory.hpp b/kernel/include/kernel/test_support/simulated_memory.hpp new file mode 100644 index 0000000..9a391d8 --- /dev/null +++ b/kernel/include/kernel/test_support/simulated_memory.hpp @@ -0,0 +1,36 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_SIMULATED_MEMORY_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_SIMULATED_MEMORY_HPP + +#include "kapi/memory.hpp" + +#include <kstd/units> + +#include <cstddef> + +namespace kernel::tests +{ + + struct simulated_memory + { + explicit simulated_memory(kstd::units::bytes size); + + ~simulated_memory(); + + auto clear() -> void; + + [[nodiscard]] auto ram_base() noexcept -> std::byte *; + [[nodiscard]] auto ram_base() const noexcept -> std::byte const *; + [[nodiscard]] auto heap_base() const noexcept -> kapi::memory::linear_address; + [[nodiscard]] auto heap_size() const noexcept -> kstd::units::bytes; + [[nodiscard]] auto memory_descriptor() const noexcept -> int; + + private: + int m_memory_descriptor{}; + kstd::units::bytes m_size{0}; + std::byte * m_physical_base{nullptr}; + std::byte * m_virtual_base{nullptr}; + }; + +} // namespace kernel::tests + +#endif
\ No newline at end of file diff --git a/kernel/kapi/cpu.tests.cpp b/kernel/kapi/cpu.tests.cpp new file mode 100644 index 0000000..85b20fd --- /dev/null +++ b/kernel/kapi/cpu.tests.cpp @@ -0,0 +1,19 @@ +#include "kapi/cpu.hpp" + +#include "kernel/test_support/cpu.hpp" + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Kernel testing kapi::cpu shims", "[support]") +{ + GIVEN("the test support infrastructure is initialized") + { + WHEN("a CPU halt is requested") + { + THEN("the correct exception is thrown") + { + REQUIRE_THROWS_AS(kapi::cpu::halt(), kernel::tests::cpu::halt); + } + } + } +}
\ No newline at end of file diff --git a/kernel/kapi/memory.cpp b/kernel/kapi/memory.cpp index 2803d76..06a3165 100644 --- a/kernel/kapi/memory.cpp +++ b/kernel/kapi/memory.cpp @@ -125,14 +125,18 @@ namespace kapi::memory } auto const flags = page_mapper::flags::writable | page_mapper::flags::supervisor_only | page_mapper::flags::global; + auto bitmap_ptr = static_cast<std::uint64_t *>(nullptr); std::ranges::for_each(std::views::iota(0uz, bitmap_pages), [&](auto index) { auto page = page::containing(pmm_metadata_base + index * page::size); auto frame = memory::frame(bitmap_frames->first.number() + index); - active_page_mapper->map(page, frame, flags); + auto mapped = active_page_mapper->map(page, frame, flags); + if (!bitmap_ptr) + { + bitmap_ptr = reinterpret_cast<std::uint64_t *>(mapped); + } }); - auto bitmap_ptr = static_cast<std::uint64_t *>(pmm_metadata_base); auto bitmap = std::span{bitmap_ptr, (bitmap_bytes + kstd::type_size<std::uint64_t> - 1_B) / kstd::type_size<std::uint64_t>}; diff --git a/kernel/kapi/system.tests.cpp b/kernel/kapi/system.tests.cpp new file mode 100644 index 0000000..ee31c51 --- /dev/null +++ b/kernel/kapi/system.tests.cpp @@ -0,0 +1,30 @@ +#include "kapi/system.hpp" + +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/log_buffer.hpp" + +#include <kstd/print> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Kernel testing kapi::system shims", "[support]") +{ + GIVEN("the test support infrastructure is initialized") + { + WHEN("a the system panics") + { + kernel::tests::log_buffer::clear(); + + THEN("the correct exception is thrown") + { + REQUIRE_THROWS_AS(kapi::system::panic("[kernel:tests] Test Panic"), kernel::tests::cpu::halt); + } + + THEN("the message is appended to the log buffer") + { + CHECK_THROWS(kapi::system::panic("[kernel:tests] Test Panic")); + REQUIRE(kernel::tests::log_buffer::flat_messages().contains("[kernel:tests] Test Panic")); + } + } + } +}
\ No newline at end of file diff --git a/kernel/kstd/print.tests.cpp b/kernel/kstd/print.tests.cpp new file mode 100644 index 0000000..f8e600f --- /dev/null +++ b/kernel/kstd/print.tests.cpp @@ -0,0 +1,23 @@ +#include "kstd/print" + +#include "kernel/test_support/log_buffer.hpp" + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Kernel testing kstd shims", "[support]") +{ + GIVEN("the test support infrastructure is initialized") + { + WHEN("a regular print is issued") + { + kernel::tests::log_buffer::clear(); + + kstd::println("[kernel:tests] Test Print"); + + THEN("the message is appended to the log buffer") + { + REQUIRE(kernel::tests::log_buffer::flat_messages().contains("[kernel:tests] Test Print")); + } + } + } +}
\ No newline at end of file diff --git a/kernel/src/devices/storage/ram_disk/device.tests.cpp b/kernel/src/devices/storage/ram_disk/device.tests.cpp new file mode 100644 index 0000000..eacdb04 --- /dev/null +++ b/kernel/src/devices/storage/ram_disk/device.tests.cpp @@ -0,0 +1,117 @@ +#include "kernel/devices/storage/ram_disk/device.hpp" + +#include "kapi/boot_module/boot_module.hpp" +#include "kapi/memory.hpp" + +#include "catch2/matchers/catch_matchers.hpp" +#include "catch2/matchers/catch_matchers_range_equals.hpp" + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> +#include <ranges> +#include <stdexcept> +#include <vector> + +SCENARIO("RAM Disk Device Construction and Initialization", "[ram_disk_device]") +{ + GIVEN("an empty boot module") + { + kapi::boot_modules::boot_module boot_module{}; + boot_module.start_address = kapi::memory::linear_address{nullptr}; + boot_module.size = 0; + + WHEN("constructing the device") + { + kernel::devices::storage::ram_disk::device device{boot_module, 0, 0}; + + THEN("init return false") + { + REQUIRE_FALSE(device.init()); + } + } + } + + GIVEN("a boot module with a valid start address and size") + { + kapi::boot_modules::boot_module boot_module{}; + boot_module.start_address = kapi::memory::linear_address{reinterpret_cast<std::byte *>(0x1000)}; + boot_module.size = 512; + + WHEN("constructing the device") + { + kernel::devices::storage::ram_disk::device device{boot_module, 0, 0}; + + THEN("init return true") + { + REQUIRE(device.init()); + } + } + } +} + +SCENARIO("RAM Disk Device Read and Write", "[ram_disk_device]") +{ + GIVEN("a device initialized with a valid boot module") + { + auto storage = std::vector{4096, std::byte{0xff}}; + auto module = + kapi::boot_modules::boot_module{"test_module", kapi::memory::linear_address{storage.data()}, storage.size()}; + auto device = kernel::devices::storage::ram_disk::device{module, 0, 0}; + REQUIRE(device.init()); + + WHEN("reading a full block from the device") + { + auto buffer = std::vector<std::byte>(device.block_size()); + device.read_block(0, buffer.data()); + + THEN("the buffer is filled with the module data") + { + REQUIRE_THAT(buffer, Catch::Matchers::RangeEquals(std::views::take(storage, device.block_size()))); + } + } + + WHEN("reading from a block index beyond the module size") + { + auto buffer = std::vector<std::byte>(device.block_size()); + device.read_block(10, buffer.data()); + + THEN("the buffer is filled with zeros") + { + REQUIRE_THAT(buffer, Catch::Matchers::RangeEquals(std::vector<std::byte>(device.block_size(), std::byte{0}))); + } + } + + WHEN("reading into a null buffer") + { + REQUIRE_THROWS_AS(device.read_block(0, nullptr), std::runtime_error); + } + + WHEN("writing to a full block") + { + auto buffer = std::vector<std::byte>(device.block_size(), std::byte{0x01}); + device.write_block(0, buffer.data()); + + THEN("the module data is updated") + { + REQUIRE_THAT(std::views::take(storage, device.block_size()), Catch::Matchers::RangeEquals(buffer)); + } + } + + WHEN("writing to a block index beyond the module size") + { + auto buffer = std::vector<std::byte>(device.block_size(), std::byte{0x01}); + device.write_block(10, buffer.data()); + + THEN("the module data is not updated") + { + REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector{4096, std::byte{0xff}})); + } + } + + WHEN("writing from a null buffer") + { + REQUIRE_THROWS_AS(device.write_block(0, nullptr), std::runtime_error); + } + } +}
\ No newline at end of file diff --git a/kernel/src/memory/bitmap_allocator.cpp b/kernel/src/memory/bitmap_allocator.cpp index c010f77..caaf5a4 100644 --- a/kernel/src/memory/bitmap_allocator.cpp +++ b/kernel/src/memory/bitmap_allocator.cpp @@ -6,6 +6,7 @@ #include <cstddef> #include <cstdint> #include <optional> +#include <ranges> #include <span> #include <utility> @@ -17,7 +18,9 @@ namespace kernel::memory , m_frame_count{frame_count} , m_last_index{} { - std::ranges::fill(m_bitmap, ~0uz); + constexpr auto bits_per_word = 64uz; + auto to_fill = (frame_count + bits_per_word - 1) / bits_per_word; + std::ranges::fill(std::views::take(m_bitmap, to_fill), ~0uz); } auto bitmap_frame_allocator::allocate_many(std::size_t count) noexcept @@ -78,22 +81,25 @@ namespace kernel::memory auto bitmap_frame_allocator::test(std::size_t index) const noexcept -> bool { - auto entry_entry = index / 64; - auto entry_offset = index % 64; + constexpr auto bits_per_word = 64uz; + auto entry_entry = index / bits_per_word; + auto entry_offset = index % bits_per_word; return (m_bitmap[entry_entry] & (1uz << entry_offset)); } auto bitmap_frame_allocator::set(std::size_t index) noexcept -> void { - auto entry_entry = index / 64; - auto entry_offset = index % 64; + constexpr auto bits_per_word = 64uz; + auto entry_entry = index / bits_per_word; + auto entry_offset = index % bits_per_word; m_bitmap[entry_entry] |= (1uz << entry_offset); } auto bitmap_frame_allocator::clear(std::size_t index) noexcept -> void { - auto entry_entry = index / 64; - auto entry_offset = index % 64; + constexpr auto bits_per_word = 64uz; + auto entry_entry = index / bits_per_word; + auto entry_offset = index % bits_per_word; m_bitmap[entry_entry] &= ~(1uz << entry_offset); } diff --git a/kernel/src/memory/bitmap_allocator.tests.cpp b/kernel/src/memory/bitmap_allocator.tests.cpp new file mode 100644 index 0000000..56ec0a4 --- /dev/null +++ b/kernel/src/memory/bitmap_allocator.tests.cpp @@ -0,0 +1,289 @@ +#include "kernel/memory/bitmap_allocator.hpp" + +#include "kapi/memory.hpp" + +#include "catch2/matchers/catch_matchers.hpp" +#include "catch2/matchers/catch_matchers_range_equals.hpp" + +#include <catch2/catch_test_macros.hpp> + +#include <cstdint> +#include <limits> +#include <ranges> +#include <span> +#include <vector> + +constexpr auto all_bits_set = std::numeric_limits<std::uint64_t>::max(); +constexpr auto available_frames = 1024uz; + +SCENARIO("Bitmap allocator construction and initialization", "[memory][bitmap_allocator]") +{ + GIVEN("A storage region") + { + auto storage = std::vector(available_frames / 64, 0uz); + + WHEN("constructing the allocator with 0 frames") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 0}; + + THEN("the storage region is not modified") + { + REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector(16, 0uz))); + } + } + + WHEN("constructing with 1 frame") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 1}; + + THEN("the first word of the storage region is set to all ones") + { + REQUIRE_THAT(std::views::take(storage, 1), Catch::Matchers::RangeEquals(std::vector(1, all_bits_set))); + } + + THEN("the rest of the storage region is not modified") + { + REQUIRE_THAT(std::views::drop(storage, 1), Catch::Matchers::RangeEquals(std::vector(15, 0uz))); + } + } + + WHEN("constructing with 64 frames") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 64}; + + THEN("the first word of the storage region is set to all ones") + { + REQUIRE_THAT(std::views::take(storage, 1), Catch::Matchers::RangeEquals(std::vector(1, all_bits_set))); + } + + THEN("the rest of the storage region is not modified") + { + REQUIRE_THAT(std::views::drop(storage, 1), Catch::Matchers::RangeEquals(std::vector(15, 0uz))); + } + } + + WHEN("constructing with all available frames") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + + THEN("the storage region is filled with all ones") + { + REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector(16, all_bits_set))); + } + } + + WHEN("constructing with half the available frames") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames / 2}; + + THEN("the first half of the storage region is filled with all ones") + { + REQUIRE_THAT(std::views::take(storage, (available_frames / 2) / 64), + Catch::Matchers::RangeEquals(std::vector((available_frames / 2) / 64, all_bits_set))); + } + + THEN("the second half of the storage region is filled with all zeros") + { + REQUIRE_THAT(std::views::drop(storage, (available_frames / 2) / 64), + Catch::Matchers::RangeEquals(std::vector((available_frames / 2) / 64, 0uz))); + } + } + } +} + +SCENARIO("Bitmap allocator frame allocation", "[memory][bitmap_allocator]") +{ + GIVEN("A storage region") + { + auto storage = std::vector(available_frames / 64, 0uz); + + AND_GIVEN("an allocator constructed with all available frames but no free ones") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + + WHEN("allocating 1 frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is empty") + { + REQUIRE_FALSE(result.has_value()); + } + } + } + + AND_GIVEN("an allocator constructed with all available frames but only one free one") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + allocator.release_many({kapi::memory::frame{0}, 1}); + + WHEN("allocating 1 frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 1 frame") + { + REQUIRE(result->second == 1); + } + } + + WHEN("allocating more frames than are free") + { + auto result = allocator.allocate_many(2); + + THEN("the result is empty") + { + REQUIRE_FALSE(result.has_value()); + } + + AND_WHEN("allocating a single frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + } + + WHEN("allocating 0 frames") + { + auto result = allocator.allocate_many(0); + + THEN("the result is empty") + { + REQUIRE_FALSE(result.has_value()); + } + } + } + } + + AND_GIVEN("an allocator with many single frame holes") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + for (auto i = 0uz; i < available_frames; i += 2) + { + allocator.release_many({kapi::memory::frame{i}, 1}); + } + + WHEN("allocating 1 frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 1 frame") + { + REQUIRE(result->second == 1); + } + } + + WHEN("allocating 2 frames") + { + auto result = allocator.allocate_many(2); + + THEN("the result is empty") + { + REQUIRE_FALSE(result.has_value()); + } + } + } + + AND_GIVEN("and allocator with all frames marked as free") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + allocator.release_many({kapi::memory::frame{0}, available_frames}); + + WHEN("allocating 1 frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 1 frame") + { + REQUIRE(result->second == 1); + } + } + + WHEN("allocating multiple frames") + { + auto result = allocator.allocate_many(20); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 20 frames") + { + REQUIRE(result->second == 20); + } + } + + WHEN("marking all frames as used") + { + for (auto i = 0uz; i < available_frames; i++) + { + allocator.mark_used(kapi::memory::frame{i}); + } + + THEN("the allocator has no free frames") + { + REQUIRE_FALSE(allocator.allocate_many(1).has_value()); + } + } + } + + AND_GIVEN("an allocator with a contiguous block of free frames") + { + auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames}; + allocator.release_many({kapi::memory::frame{0}, available_frames}); + for (auto i = 0uz; i < available_frames / 2; i++) + { + allocator.mark_used(kapi::memory::frame{i}); + } + + WHEN("allocating a single frame") + { + auto result = allocator.allocate_many(1); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 1 frame") + { + REQUIRE(result->second == 1); + } + } + + WHEN("allocating multiple frames") + { + auto result = allocator.allocate_many(20); + + THEN("the result is not empty") + { + REQUIRE(result.has_value()); + } + + THEN("the result contains 20 frames") + { + REQUIRE(result->second == 20); + } + } + } + } +}
\ No newline at end of file diff --git a/kernel/src/memory/block_list_allocator.tests.cpp b/kernel/src/memory/block_list_allocator.tests.cpp new file mode 100644 index 0000000..8ca426d --- /dev/null +++ b/kernel/src/memory/block_list_allocator.tests.cpp @@ -0,0 +1,93 @@ +#include "kernel/memory/block_list_allocator.hpp" + +#include "kapi/memory.hpp" + +#include "kernel/test_support/memory.hpp" + +#include <kstd/units> + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> + +using namespace kstd::units_literals; + +namespace kapi +{ + namespace memory + { + auto reset() -> void; + } +} // namespace kapi + +SCENARIO("Block List Allocator Operations", "[memory][allocator]") +{ + GIVEN("A newly initialized block list allocator mapped via the test sandbox") + { + kapi::memory::reset(); + kapi::memory::init(); + + auto sandbox_base = kernel::tests::memory::heap_base(); + kernel::memory::block_list_allocator allocator{sandbox_base}; + + WHEN("a basic allocation request is made") + { + void * ptr = allocator.allocate(128_B, 8_B); + + THEN("a valid, non-null pointer is returned") + { + REQUIRE(ptr != nullptr); + } + + AND_THEN("the returned memory is writeable without causing segmentation faults") + { + auto byte_ptr = static_cast<std::byte *>(ptr); + byte_ptr[0] = std::byte{0xDE}; + byte_ptr[127] = std::byte{0xAD}; + REQUIRE(byte_ptr[0] == std::byte{0xDE}); + REQUIRE(byte_ptr[127] == std::byte{0xAD}); + } + + allocator.deallocate(ptr); + } + + WHEN("multiple allocations are made sequentially") + { + void * ptr1 = allocator.allocate(64_B, 8_B); + void * ptr2 = allocator.allocate(64_B, 8_B); + void * ptr3 = allocator.allocate(1_KiB, 16_B); + + THEN("they return distinct, non-overlapping memory blocks") + { + REQUIRE(ptr1 != nullptr); + REQUIRE(ptr2 != nullptr); + REQUIRE(ptr3 != nullptr); + REQUIRE(ptr1 != ptr2); + REQUIRE(ptr2 != ptr3); + REQUIRE(ptr1 != ptr3); + } + + allocator.deallocate(ptr1); + allocator.deallocate(ptr2); + allocator.deallocate(ptr3); + } + + WHEN("a block is allocated and then completely freed") + { + void * original_ptr = allocator.allocate(512_B, 16_B); + allocator.deallocate(original_ptr); + + AND_WHEN("a new allocation of equal or smaller size is requested") + { + void * new_ptr = allocator.allocate(128_B, 16_B); + + THEN("the allocator actively reuses the coalesced space") + { + REQUIRE(new_ptr == original_ptr); + } + + allocator.deallocate(new_ptr); + } + } + } +} diff --git a/kernel/src/test_support/kapi/cio.cpp b/kernel/src/test_support/kapi/cio.cpp new file mode 100644 index 0000000..35452d4 --- /dev/null +++ b/kernel/src/test_support/kapi/cio.cpp @@ -0,0 +1,43 @@ +#include <kapi/cio.hpp> + +#include "kernel/test_support/log_buffer.hpp" + +#include <iostream> +#include <string> +#include <string_view> + +namespace kapi::cio +{ + + namespace + { + + class test_output_device : public output_device + { + public: + test_output_device() = default; + + auto write(output_stream stream, std::string_view text) -> void override + { + auto & standard_stream = stream == output_stream::stdout ? std::cout : std::cerr; + standard_stream << text; + if (text != "\n") + { + kernel::tests::log_buffer::append(std::string{text}); + } + } + } device{}; + + } // namespace + + auto init() -> void + { + set_output_device(device); + } + + auto reset() -> void + { + kernel::tests::log_buffer::clear(); + } + +} // namespace kapi::cio
\ No newline at end of file diff --git a/kernel/src/test_support/kapi/cpu.cpp b/kernel/src/test_support/kapi/cpu.cpp new file mode 100644 index 0000000..6592d15 --- /dev/null +++ b/kernel/src/test_support/kapi/cpu.cpp @@ -0,0 +1,41 @@ +#include "kernel/test_support/cpu.hpp" + +#include <kapi/cpu.hpp> + +#include <atomic> +#include <stdexcept> + +namespace kapi::cpu +{ + + namespace + { + auto static initialized = std::atomic_flag{}; + } + + auto reset() -> void + { + if (!initialized.test()) + { + throw std::logic_error{"kapi::cpu::reset() called before kapi::cpu::init()"}; + } + + initialized.clear(); + } + + auto init() -> void + { + if (initialized.test_and_set()) + { + throw std::logic_error("kapi::cpu::init() called more than once"); + } + + // TODO: make sure that simulated interrupt can run. + } + + auto halt() -> void + { + throw kernel::tests::cpu::halt{}; + } + +} // namespace kapi::cpu
\ No newline at end of file diff --git a/kernel/src/test_support/kapi/interrupts.cpp b/kernel/src/test_support/kapi/interrupts.cpp new file mode 100644 index 0000000..0077266 --- /dev/null +++ b/kernel/src/test_support/kapi/interrupts.cpp @@ -0,0 +1,11 @@ +#include <kapi/interrupts.hpp> + +namespace kapi::interrupts +{ + + auto enable() -> void + { + // TODO: enable simulated interrupts. + } + +} // namespace kapi::interrupts
\ No newline at end of file diff --git a/kernel/src/test_support/kapi/memory.cpp b/kernel/src/test_support/kapi/memory.cpp new file mode 100644 index 0000000..f244b7f --- /dev/null +++ b/kernel/src/test_support/kapi/memory.cpp @@ -0,0 +1,71 @@ +#include "kapi/memory.hpp" + +#include <kapi/memory.hpp> + +#include "kernel/test_support/bump_frame_allocator.hpp" +#include "kernel/test_support/page_mapper.hpp" + +#include <kstd/units> + +#include <optional> + +namespace +{ + //! The size of the simulated RAM. + constexpr auto memory_size = kstd::units::MiB(32); + constexpr auto number_of_frames = memory_size / kapi::memory::frame::size; + + auto constinit bump_allocator = std::optional<kernel::tests::bump_frame_allocator>{}; + auto constinit test_mapper = std::optional<kernel::tests::page_mapper>{}; + + auto constinit old_allocator = std::optional<kapi::memory::frame_allocator *>{}; + auto constinit old_mapper = std::optional<kapi::memory::page_mapper *>{}; + + auto handoff_to_kernel_pmm(kapi::memory::frame_allocator & new_allocator) -> void + { + auto first_free_frame = bump_allocator->next_free_frame; + auto number_of_free_frames = number_of_frames - first_free_frame; + new_allocator.release_many({kapi::memory::frame{first_free_frame}, number_of_free_frames}); + } + +} // namespace + +namespace kapi::memory +{ + + auto init() -> void + { + bump_allocator.emplace(); + test_mapper.emplace(memory_size); + + old_allocator = set_frame_allocator(*bump_allocator); + old_mapper = set_page_mapper(*test_mapper); + + init_pmm(memory_size / frame::size, handoff_to_kernel_pmm); + } + + auto reset() -> void + { + if (old_allocator && *old_allocator) + { + set_frame_allocator(**old_allocator); + } + + if (old_mapper && *old_mapper) + { + set_page_mapper(**old_mapper); + } + + bump_allocator.reset(); + test_mapper.reset(); + } + +} // namespace kapi::memory + +namespace kernel::tests::memory +{ + auto heap_base() -> kapi::memory::linear_address + { + return test_mapper->memory.heap_base(); + } +} // namespace kernel::tests::memory
\ No newline at end of file diff --git a/kernel/src/test_support/log_buffer.cpp b/kernel/src/test_support/log_buffer.cpp new file mode 100644 index 0000000..36ed15e --- /dev/null +++ b/kernel/src/test_support/log_buffer.cpp @@ -0,0 +1,39 @@ +#include "kernel/test_support/log_buffer.hpp" + +#include <algorithm> +#include <string> +#include <vector> + +namespace kernel::tests::log_buffer +{ + + namespace + { + std::vector<std::string> recorded_messages{}; + } + + auto append(std::string const & message) -> void + { + recorded_messages.push_back(message); + } + + auto clear() -> void + { + recorded_messages.clear(); + } + + auto flat_messages() -> std::string + { + return std::ranges::fold_left(recorded_messages, std::string{}, + [](std::string accumulator, std::string const & message) { + accumulator += message; + return accumulator; + }); + } + + auto messages() -> std::vector<std::string> const & + { + return recorded_messages; + } + +} // namespace kernel::tests::log_buffer diff --git a/kernel/src/test_support/page_mapper.cpp b/kernel/src/test_support/page_mapper.cpp new file mode 100644 index 0000000..abdcae5 --- /dev/null +++ b/kernel/src/test_support/page_mapper.cpp @@ -0,0 +1,78 @@ +#include "kernel/test_support/page_mapper.hpp" + +#include "kapi/memory.hpp" + +#include <kstd/units> + +#include <cstddef> +#include <format> +#include <stdexcept> +#include <sys/mman.h> + +namespace kernel::tests +{ + + page_mapper::page_mapper(kstd::units::bytes memory_size) + : memory{memory_size} + {} + + auto page_mapper::map(kapi::memory::page page, kapi::memory::frame frame, flags) -> std::byte * + { + auto result = page_mappings.insert({page.number(), frame}); + if (!result.second) + { + auto error = std::format("Page {} was already mapped!", page.number()); + throw std::invalid_argument{error}; + } + + auto page_address = page.start_address(); + auto sandbox_start = memory.heap_base(); + auto sandbox_end = sandbox_start + memory.heap_size(); + + if (page_address >= sandbox_start && page_address < sandbox_end) + { + auto virtual_target = static_cast<std::byte *>(page_address); + auto physical_offset = frame.number() * kapi::memory::frame::size; + auto mapped_ptr = mmap(virtual_target, kapi::memory::page::size.value, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_FIXED, memory.memory_descriptor(), physical_offset.value); + if (mapped_ptr == MAP_FAILED) + { + throw std::runtime_error("Failed to map page"); + } + + return static_cast<std::byte *>(mapped_ptr); + } + else if (page_address >= kapi::memory::mmio_base) + { + throw std::runtime_error("MMIO mapping not yet supported in testing!"); + } + else if (page_address >= kapi::memory::higher_half_direct_map_base) + { + auto offset = frame.number() * kapi::memory::frame::size; + return memory.ram_base() + offset; + } + + return nullptr; + } + + auto page_mapper::unmap(kapi::memory::page page) -> void + { + if (!try_unmap(page)) + { + auto error = std::format("Page {} was never mapped!", page.number()); + throw std::invalid_argument{error}; + } + } + + auto page_mapper::try_unmap(kapi::memory::page page) noexcept -> bool + { + if (page_mappings.contains(page.number())) + { + page_mappings.erase(page.number()); + return true; + } + + return false; + } + +} // namespace kernel::tests
\ No newline at end of file diff --git a/kernel/src/test_support/simulated_memory.cpp b/kernel/src/test_support/simulated_memory.cpp new file mode 100644 index 0000000..fa3d36c --- /dev/null +++ b/kernel/src/test_support/simulated_memory.cpp @@ -0,0 +1,93 @@ +#include "kernel/test_support/simulated_memory.hpp" + +#include "kapi/memory.hpp" + +#include <kstd/units> + +#include <cstddef> +#include <cstring> +#include <stdexcept> +#include <sys/mman.h> +#include <sys/types.h> +#include <unistd.h> + +using namespace kstd::units_literals; + +namespace kernel::tests +{ + namespace + { + constexpr auto virtual_size = 1_GiB; + } + + simulated_memory::simulated_memory(kstd::units::bytes size) + : m_memory_descriptor{memfd_create("teachos_simulated_memory", 0)} + , m_size{size} + { + if (m_memory_descriptor < 0) + { + throw std::runtime_error("Failed to create simulated memory"); + } + + if (ftruncate(m_memory_descriptor, static_cast<off_t>(m_size.value)) < 0) + { + throw std::runtime_error("Failed to resize simulated memory"); + } + + auto mapped_pointer = mmap(nullptr, m_size.value, PROT_READ | PROT_WRITE, MAP_SHARED, m_memory_descriptor, 0); + if (mapped_pointer == MAP_FAILED) + { + throw std::runtime_error("Failed to map simulated memory"); + } + + m_physical_base = static_cast<std::byte *>(mapped_pointer); + + auto virtual_pointer = mmap(nullptr, virtual_size.value, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (virtual_pointer == MAP_FAILED) + { + throw std::runtime_error("Failed to map simulated memory"); + } + + m_virtual_base = static_cast<std::byte *>(virtual_pointer); + + clear(); + } + + simulated_memory::~simulated_memory() + { + munmap(m_physical_base, m_size.value); + munmap(m_virtual_base, virtual_size.value); + close(m_memory_descriptor); + } + + auto simulated_memory::clear() -> void + { + std::memset(m_physical_base, 0, m_size.value); + } + + auto simulated_memory::ram_base() noexcept -> std::byte * + { + return m_physical_base; + } + + auto simulated_memory::ram_base() const noexcept -> std::byte const * + { + return m_physical_base; + } + + auto simulated_memory::heap_base() const noexcept -> kapi::memory::linear_address + { + return kapi::memory::linear_address{m_virtual_base}; + } + + auto simulated_memory::heap_size() const noexcept -> kstd::units::bytes + { + return virtual_size; + } + + auto simulated_memory::memory_descriptor() const noexcept -> int + { + return m_memory_descriptor; + } + +} // namespace kernel::tests
\ No newline at end of file diff --git a/kernel/src/test_support/state_reset_listener.cpp b/kernel/src/test_support/state_reset_listener.cpp new file mode 100644 index 0000000..e201a10 --- /dev/null +++ b/kernel/src/test_support/state_reset_listener.cpp @@ -0,0 +1,50 @@ +#include "kapi/cio.hpp" +#include "kapi/cpu.hpp" +#include "kapi/memory.hpp" + +#include <catch2/catch_test_case_info.hpp> +#include <catch2/interfaces/catch_interfaces_reporter.hpp> +#include <catch2/reporters/catch_reporter_event_listener.hpp> +#include <catch2/reporters/catch_reporter_registrars.hpp> + +namespace kapi +{ + namespace cio + { + auto reset() -> void; + } + + namespace cpu + { + auto reset() -> void; + } + + namespace memory + { + auto reset() -> void; + } +} // namespace kapi + +struct state_reset_listener : Catch::EventListenerBase +{ + using EventListenerBase::EventListenerBase; + + void testCaseStarting(Catch::TestCaseInfo const &) override + { + kapi::cio::init(); + kapi::cpu::init(); + kapi::memory::init(); + } + + void testCaseEnded(Catch::TestCaseStats const &) override + { + kapi::memory::reset(); + kapi::cpu::reset(); + kapi::cio::reset(); + } +}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +CATCH_REGISTER_LISTENER(state_reset_listener); +#pragma GCC diagnostic pop
\ No newline at end of file |
