aboutsummaryrefslogtreecommitdiff
path: root/kernel/src
diff options
context:
space:
mode:
authorMarcel Braun <marcel.braun@ost.ch>2026-04-02 08:48:00 +0200
committerMarcel Braun <marcel.braun@ost.ch>2026-04-02 08:48:00 +0200
commit0c01a95325b26151ff3c9a70142f5dc83ff7d53f (patch)
tree9bf034f544ae773b653554a54edfce232f835754 /kernel/src
parent022d3e872de9c5a6a52c67f74af13706552330c0 (diff)
parent3eb680cf5bcef626505cac82820996d8db4170d7 (diff)
downloadteachos-0c01a95325b26151ff3c9a70142f5dc83ff7d53f.tar.xz
teachos-0c01a95325b26151ff3c9a70142f5dc83ff7d53f.zip
Merge branch 'fmorgner/develop-SA-FS26/kernel-bht' into 'develop-BA-FS26'
Add experimental support for kernel tests See merge request teachos/kernel!20
Diffstat (limited to 'kernel/src')
-rw-r--r--kernel/src/devices/storage/ram_disk/device.tests.cpp117
-rw-r--r--kernel/src/memory/bitmap_allocator.cpp20
-rw-r--r--kernel/src/memory/bitmap_allocator.tests.cpp289
-rw-r--r--kernel/src/memory/block_list_allocator.tests.cpp93
-rw-r--r--kernel/src/test_support/kapi/cio.cpp43
-rw-r--r--kernel/src/test_support/kapi/cpu.cpp41
-rw-r--r--kernel/src/test_support/kapi/interrupts.cpp11
-rw-r--r--kernel/src/test_support/kapi/memory.cpp71
-rw-r--r--kernel/src/test_support/log_buffer.cpp39
-rw-r--r--kernel/src/test_support/page_mapper.cpp78
-rw-r--r--kernel/src/test_support/simulated_memory.cpp93
-rw-r--r--kernel/src/test_support/state_reset_listener.cpp50
12 files changed, 938 insertions, 7 deletions
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();
+ }