kernel: Add the package and test targets
0ea2981a6b6dec11f0f0def6bed8f12a360747f32e258bb1169ff1377e5289d5
Build kernel tests as a separate package with entry-package test discovery. Add checked half-open ranges for memory and execution windows. Assisted-by: Codex:gpt-6
1 parent
56a00753
Makefile
+15 -2
| 21 | 21 | ||
| 22 | 22 | # Verify the emulator binary exists. |
|
| 23 | 23 | EMU_PATH := $(shell command -v $(EMU) 2>/dev/null) |
|
| 24 | 24 | ||
| 25 | 25 | default: emulator $(RAD_BIN) |
|
| 26 | - | test: emulator seed-test std-test bin-test |
|
| 26 | + | test: emulator seed-test std-test bin-test kernel-test |
|
| 27 | 27 | ||
| 28 | 28 | seed-test: |
|
| 29 | 29 | @seed/test |
|
| 30 | 30 | ||
| 31 | 31 | # Emulator command check |
| 64 | 64 | @rm -f lib/std.test.rv64 \ |
|
| 65 | 65 | lib/std.test.rv64.debug \ |
|
| 66 | 66 | lib/std.test.rv64.s \ |
|
| 67 | 67 | lib/std.test.rv64.o |
|
| 68 | 68 | ||
| 69 | + | # Kernel modules and tests use a separate package and test entry point. |
|
| 70 | + | KERNEL_SRC := $(shell find kernel -name '*.rad' ! -name 'tests.rad' ! -path '*/tests/*' 2>/dev/null) |
|
| 71 | + | KERNEL_TEST_SRC := kernel/kernel/tests.rad $(shell find kernel/kernel/tests -name '*.rad' 2>/dev/null) |
|
| 72 | + | KERNEL := -pkg kernel $(patsubst %,-mod %,$(sort $(KERNEL_SRC))) |
|
| 73 | + | KERNEL_TEST := $(BIN_DIR)/kernel.test.rv64 |
|
| 74 | + | ||
| 75 | + | kernel-test: emulator $(KERNEL_TEST) |
|
| 76 | + | @$(EMU) $(EMU_FLAGS) -run $(KERNEL_TEST) |
|
| 77 | + | ||
| 78 | + | $(KERNEL_TEST): $(KERNEL_SRC) $(KERNEL_TEST_SRC) $(STD_LIB) $(RAD_BIN) |
|
| 79 | + | @echo "radiance kernel tests => $@" |
|
| 80 | + | @$(RADIANCE) -test $(STD_TEST) $(KERNEL) $(patsubst %,-mod %,$(KERNEL_TEST_SRC)) -entry kernel -o $@ |
|
| 81 | + | ||
| 69 | 82 | # Binary Tests |
|
| 70 | 83 | ||
| 71 | 84 | BIN_TEST_DIR := test/tests |
|
| 72 | 85 | # Only tests with `//! returns:` are compiled to binaries and executed. |
|
| 73 | 86 | BIN_TEST_EXE_SRC := $(shell grep -rl '^//! returns:' $(BIN_TEST_DIR)) |
| 121 | 134 | clean: clean-std-test clean-bin-test clean-rad |
|
| 122 | 135 | ||
| 123 | 136 | t: test |
|
| 124 | 137 | c: clean |
|
| 125 | 138 | ||
| 126 | - | .PHONY: test clean default seed-test std-test bin-test seed \ |
|
| 139 | + | .PHONY: test clean default seed-test std-test bin-test kernel-test seed \ |
|
| 127 | 140 | clean-std-test clean-bin-test clean-rad emulator |
|
| 128 | 141 | .SUFFIXES: |
|
| 129 | 142 | .DELETE_ON_ERROR: |
|
| 130 | 143 | .SILENT: |
compiler/radiance.rad
+5 -3
| 665 | 665 | return ast::synthNode(arena, ast::NodeValue::Call(ast::Call { callee, args })); |
|
| 666 | 666 | } |
|
| 667 | 667 | ||
| 668 | 668 | /// Inject a test runner into the entry package's root module. |
|
| 669 | 669 | /// |
|
| 670 | - | /// Scans all parsed modules for `@test fn` declarations, then appends |
|
| 670 | + | /// Scans the entry package for `@test fn` declarations, then appends |
|
| 671 | 671 | /// a synthetic entry point to the root module's AST block: |
|
| 672 | 672 | /// |
|
| 673 | 673 | /// ``` |
|
| 674 | 674 | /// @default fn #testMain() -> i32 { |
|
| 675 | 675 | /// return testing::runAllTests(&[ |
| 685 | 685 | arena: &mut ast::NodeArena |
|
| 686 | 686 | ) throws (Error) { |
|
| 687 | 687 | let entryPkg = try getEntryPackage(ctx); |
|
| 688 | 688 | let root = try getRootModule(&entryPkg, &ctx.graph); |
|
| 689 | 689 | ||
| 690 | - | // Collect all test functions across all modules. |
|
| 690 | + | // Collect test functions from the entry package's modules. |
|
| 691 | 691 | let mut tests: [TestDesc; MAX_TESTS] = undefined; |
|
| 692 | 692 | let mut testCount: u32 = 0; |
|
| 693 | 693 | ||
| 694 | 694 | for modIdx in 0..ctx.graph.entriesLen { |
|
| 695 | 695 | if let entry = module::get(&ctx.graph, modIdx as u16) { |
|
| 696 | - | collectModuleTests(entry, &mut tests[..], &mut testCount); |
|
| 696 | + | if entry.packageId == entryPkg.id { |
|
| 697 | + | collectModuleTests(entry, &mut tests[..], &mut testCount); |
|
| 698 | + | } |
|
| 697 | 699 | } |
|
| 698 | 700 | } |
|
| 699 | 701 | if testCount == 0 { |
|
| 700 | 702 | throw error(&["fatal:", "no test functions found"]); |
|
| 701 | 703 | } |
kernel/kernel.rad
added
+6 -0
| 1 | + | //! Kernel resource management and machine execution. |
|
| 2 | + | ||
| 3 | + | use std::testing; |
|
| 4 | + | ||
| 5 | + | export mod range; |
|
| 6 | + | @test export mod tests; |
kernel/kernel/range.rad
added
+22 -0
| 1 | + | //! Checked half-open ranges for physical memory and CPU windows. |
|
| 2 | + | ||
| 3 | + | /// A nonempty half-open interval. Construct intervals with `new`. |
|
| 4 | + | export record Range: Copy { |
|
| 5 | + | /// First included address or tick. |
|
| 6 | + | start: u64, |
|
| 7 | + | /// First excluded address or tick. |
|
| 8 | + | end: u64, |
|
| 9 | + | } |
|
| 10 | + | ||
| 11 | + | /// Construct a nonempty interval. Reject addition overflow. |
|
| 12 | + | export fn new(start: u64, length: u64) -> ?Range { |
|
| 13 | + | if length == 0 or length > 0xffffffffffffffff - start { |
|
| 14 | + | return nil; |
|
| 15 | + | } |
|
| 16 | + | return Range { start, end: start + length }; |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | /// Check that the complete nonempty interval is inside the outer interval. |
|
| 20 | + | export fn contains(outer: Range, inner: Range) -> bool { |
|
| 21 | + | return outer.start <= inner.start and inner.start < inner.end and inner.end <= outer.end; |
|
| 22 | + | } |
kernel/kernel/tests.rad
added
+3 -0
| 1 | + | //! Kernel unit tests. Machine execution tests have separate entry points. |
|
| 2 | + | ||
| 3 | + | export mod range; |
kernel/kernel/tests/range.rad
added
+27 -0
| 1 | + | //! Bounds and overflow checks for kernel intervals. |
|
| 2 | + | ||
| 3 | + | use kernel::range; |
|
| 4 | + | use std::testing; |
|
| 5 | + | ||
| 6 | + | /// Check empty intervals and overflow at the address-space boundary. |
|
| 7 | + | @test fn construction() throws (testing::TestError) { |
|
| 8 | + | try testing::expect(range::new(0, 0) == nil); |
|
| 9 | + | try testing::expect(range::new(0xffffffffffffffff, 1) == nil); |
|
| 10 | + | try testing::expect(range::new(1, 0xffffffffffffffff) == nil); |
|
| 11 | + | let span = range::new(0, 0xffffffffffffffff) else { |
|
| 12 | + | throw testing::TestError::Failed; |
|
| 13 | + | }; |
|
| 14 | + | try testing::expect(span.start == 0); |
|
| 15 | + | try testing::expect(span.end == 0xffffffffffffffff); |
|
| 16 | + | } |
|
| 17 | + | ||
| 18 | + | /// Check exact bounds, partial overlap, and the excluded upper bound. |
|
| 19 | + | @test fn containment() throws (testing::TestError) { |
|
| 20 | + | let outer = range::Range { start: 4096, end: 8192 }; |
|
| 21 | + | try testing::expect(range::contains(outer, outer)); |
|
| 22 | + | try testing::expect(range::contains(outer, range::Range { start: 4097, end: 8191 })); |
|
| 23 | + | try testing::expect(not range::contains(outer, range::Range { start: 4095, end: 4097 })); |
|
| 24 | + | try testing::expect(not range::contains(outer, range::Range { start: 8191, end: 8193 })); |
|
| 25 | + | try testing::expect(not range::contains(outer, range::Range { start: 8192, end: 8193 })); |
|
| 26 | + | try testing::expect(not range::contains(outer, range::Range { start: 4096, end: 4096 })); |
|
| 27 | + | } |