Skip to content

Commit 68e7f7c

Browse files
committed
integration-test: restructure + use Limine as Multiboot(2) bootloader
Using Limine as bootloader has the main benefit that we easily can create a hybrid-bootable ISO that can boot on UEFI and BIOS environments, but we still have a Multiboot2 32-bit hand-off. We only tested BIOS so far, but next, we can also extend our testing to UEFI environments.
1 parent 2cfa78e commit 68e7f7c

File tree

17 files changed

+201
-165
lines changed

17 files changed

+201
-165
lines changed

integration-test/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
serial.txt

integration-test/README.md

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@
33
This directory contains integration tests for the `multiboot2` and the
44
`multiboot2-header` crate. The integration tests start a QEMU VM and do certain
55
checks at runtime. If something fails, they instruct QEMU to exit with an error
6-
code. All output of the VM is printed to the screen. If
6+
code. All output of the VM is printed to the screen.
77

8-
The `bins` directory contains binaries that **are** the tests. The `tests`
8+
The `bins` directory contains Rust binaries that **are** the tests. The `tests`
99
directory contains test definitions, run scripts, and other relevant files. The
1010
main entry to run all tests is `./run.sh` in this directory.
1111

12-
## TL;DR:
13-
- `$ nix-shell --run ./run.sh` to execute the integration tests with Nix (recommended)
14-
- `$ ./run.sh` to execute the integration tests (you have to install dependencies manually)
12+
## TL;DR
13+
14+
- `$ nix-shell --run ./run.sh` to execute the integration tests with Nix
15+
(recommended)
16+
- `$ nix-shell --run "integration-test/run.sh"` to run the test from the
17+
project root
18+
- `$ ./run.sh` to execute the integration tests (you have to get the
19+
dependencies manually)
1520

1621
## Prerequisites
22+
1723
The tests are executed best when using [`nix`](https://nixos.org/)/`nix-shell`
1824
to get the relevant tools. Otherwise, please make sure the following packages
1925
are available:
20-
- grub helper tools
26+
27+
- grub helper tools (grub-file)
2128
- rustup
29+
- OVMF
2230
- QEMU
2331
- xorriso

integration-test/bins/multiboot2_payload/src/multiboot2_header.S

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
.align 8
2222
.Lmb2_header_tag_information_request_start:
2323
.word 1 # type (16bit)
24-
.word 0 # flags (16bit)
24+
.word 1 # flags (16bit)
2525
.long .Lmb2_header_tag_information_request_end - .Lmb2_header_tag_information_request_start # size (32bit)
2626
.long 1
2727
.long 2

integration-test/bins/multiboot2_payload/src/verify/grub.rs integration-test/bins/multiboot2_payload/src/verify/bootloader.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ fn basic_sanity_checks(mbi: &BootInformation) -> anyhow::Result<()> {
2323
.map_err(anyhow::Error::msg)?
2424
.cmdline()
2525
.map_err(anyhow::Error::msg)?;
26-
assert!(bootloader_name.starts_with("GRUB 2."));
27-
assert_eq!(cmdline, "some commandline arguments");
26+
assert!(bootloader_name.starts_with("GRUB 2.") || bootloader_name.starts_with("Limine"));
27+
assert_eq!(cmdline, "some kernel cmdline");
2828

2929
Ok(())
3030
}

integration-test/bins/multiboot2_payload/src/verify/mod.rs

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
mod bootloader;
12
mod chainloader;
2-
mod grub;
33

44
use alloc::format;
55
use alloc::vec::Vec;
@@ -17,8 +17,11 @@ pub fn run(mbi: &BootInformation) -> anyhow::Result<()> {
1717
.map_err(anyhow::Error::msg)?;
1818

1919
if bootloader.to_lowercase().contains("grub") {
20-
log::info!("loaded by grub");
21-
grub::run(mbi)?;
20+
log::info!("loaded by GRUB");
21+
bootloader::run(mbi)?;
22+
} else if bootloader.to_lowercase().contains("limine") {
23+
log::info!("loaded by Limine");
24+
bootloader::run(mbi)?;
2225
} else {
2326
log::info!("loaded by chainloader");
2427
chainloader::run(mbi)?;
@@ -78,6 +81,15 @@ pub(self) fn print_module_info(mbi: &BootInformation) -> anyhow::Result<()> {
7881
}
7982
let module = modules.first().unwrap();
8083
let module_cmdline = module.cmdline().map_err(anyhow::Error::msg)?;
84+
85+
let allowed_module_cmdlines = ["Limine bootloader config", "multiboot2_payload"];
86+
assert!(
87+
allowed_module_cmdlines
88+
.iter()
89+
.any(|&str| module_cmdline == str),
90+
"The module cmdline must be one of {allowed_module_cmdlines:?} but is {module_cmdline}"
91+
);
92+
8193
println!("Modules:");
8294
println!(
8395
" 0x{:010x} - 0x{:010x} ({} B, cmdline='{}')",
@@ -86,13 +98,13 @@ pub(self) fn print_module_info(mbi: &BootInformation) -> anyhow::Result<()> {
8698
module.module_size(),
8799
module_cmdline
88100
);
89-
println!(" grub cfg passed as boot module:");
101+
println!(" bootloader cfg passed as boot module:");
90102
let grup_cfg_ptr = module.start_address() as *const u32 as *const u8;
91103
let grub_cfg =
92104
unsafe { core::slice::from_raw_parts(grup_cfg_ptr, module.module_size() as usize) };
93105

94-
// In the GRUB bootflow case, we pass the config as module with it. This is
95-
// not done for the chainloaded case.
106+
// In the Limine bootflow case, we pass the config as module with it. This
107+
// is not done for the chainloaded case.
96108
if let Ok(str) = core::str::from_utf8(grub_cfg) {
97109
println!("=== file begin ===");
98110
for line in str.lines() {

integration-test/run.sh

+137-12
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,155 @@ IFS=$'\n\t'
77
DIR=$(dirname "$(realpath "$0")")
88
cd "$DIR" || exit
99

10+
BINS_DIR=bins/target/x86-unknown-none/release
11+
ANSI_HIGHLIGHT="\e[1;32m" # green + bold
12+
ANSI_HIGHLIGHT_ERROR="\e[1;31m" # red + bold
13+
ANSI_RESET="\e[0m"
14+
QEMU_ARGS_BASE=(
15+
-machine q35,accel=kvm
16+
-m 128m # for OVMF, we need more than just 24
17+
-debugcon stdio
18+
-serial file:serial.txt
19+
-no-reboot
20+
-device isa-debug-exit,iobase=0xf4,iosize=0x04
21+
-display none `# relevant for the CI`
22+
)
23+
1024
function fn_main() {
25+
git submodule update --init
26+
fn_build_limine_hosttool
1127
fn_build_rust_bins
12-
fn_multiboot2_integrationtest
13-
fn_multiboot2_header_integrationtest
28+
29+
fn_test_payload
30+
fn_test_loader
31+
}
32+
33+
function fn_build_limine_hosttool() {
34+
cd limine-bootloader
35+
make
36+
test -f ./limine
37+
file --brief ./limine | grep -q "ELF 64-bit LSB executable"
38+
cd -
1439
}
1540

1641
function fn_build_rust_bins() {
1742
cd "bins"
1843
cargo --version
1944
cargo build --release --verbose
20-
cd "$DIR"
45+
cd -
46+
47+
test -f $BINS_DIR/multiboot2_chainloader
48+
file --brief $BINS_DIR/multiboot2_chainloader | grep -q "ELF 32-bit LSB executable"
49+
# For simplicity, the chainloader itself boots via Multiboot 1. Sufficient.
50+
grub-file --is-x86-multiboot $BINS_DIR/multiboot2_chainloader
51+
52+
test -f $BINS_DIR/multiboot2_payload
53+
file --brief $BINS_DIR/multiboot2_payload | grep -q "ELF 32-bit LSB executable"
54+
grub-file --is-x86-multiboot2 $BINS_DIR/multiboot2_payload
55+
}
56+
57+
function fn_prepare_test_vol() {
58+
TEST_VOL="$TEST_DIR/.vol"
59+
rm -rf $TEST_VOL
60+
mkdir -p $TEST_VOL
61+
cp $TEST_DIR/limine.cfg $TEST_VOL
62+
63+
# copy limine artifacts
64+
mkdir -p $TEST_VOL/limine
65+
cp limine-bootloader/limine-bios-cd.bin $TEST_VOL/limine
66+
cp limine-bootloader/limine-bios.sys $TEST_VOL/limine
67+
cp limine-bootloader/limine-uefi-cd.bin $TEST_VOL/limine
68+
69+
mkdir -p $TEST_VOL/EFI/BOOT
70+
cp limine-bootloader/BOOTX64.EFI $TEST_VOL/EFI_BOOT
2171
}
2272

23-
function fn_multiboot2_integrationtest() {
24-
cd tests/multiboot2
25-
./build_img.sh
26-
./run_qemu.sh
27-
cd "$DIR"
73+
74+
75+
# Builds a hybrid-bootable image using Limine as bootloader. Expects that
76+
# all relevant files are in the directory describing the root volume.
77+
function fn_build_limine_iso() {
78+
xorriso -as mkisofs -b limine/limine-bios-cd.bin \
79+
-no-emul-boot -boot-load-size 4 -boot-info-table \
80+
--efi-boot limine/limine-uefi-cd.bin \
81+
-efi-boot-part --efi-boot-image --protective-msdos-label \
82+
$TEST_VOL -o $TEST_DIR/image.iso 2>/dev/null
83+
84+
./limine-bootloader/limine bios-install $TEST_DIR/image.iso 2>/dev/null
2885
}
2986

30-
function fn_multiboot2_header_integrationtest() {
31-
cd tests/multiboot2-header
32-
./run_qemu.sh
33-
cd "$DIR"
87+
function fn_run_qemu() {
88+
set +e
89+
90+
# As QEMU can't print serial and debugcon to stdout simultaneously, I
91+
# add a background task watching serial.txt
92+
rm serial.txt
93+
touch serial.txt
94+
tail -f serial.txt &
95+
96+
qemu-system-x86_64 "${QEMU_ARGS[@]}"
97+
EXIT_CODE=$?
98+
# Custom exit code used by the integration test to report success.
99+
QEMU_EXIT_SUCCESS=73
100+
101+
set -e
102+
103+
echo "#######################################"
104+
if [[ $EXIT_CODE -eq $QEMU_EXIT_SUCCESS ]]; then
105+
echo -e "${ANSI_HIGHLIGHT}SUCCESS${ANSI_RESET}"
106+
echo # newline
107+
else
108+
echo -e "${ANSI_HIGHLIGHT_ERROR}FAILED - Integration Test 'multiboot2-header'${ANSI_RESET}"
109+
exit "$EXIT_CODE"
110+
fi
111+
}
112+
113+
function fn_run_test_bios() {
114+
local ISO=$1
115+
local QEMU_ARGS=("${QEMU_ARGS_BASE[@]}") # copy array
116+
local QEMU_ARGS+=(
117+
-cdrom "$ISO"
118+
)
119+
echo -e "Running '${ANSI_HIGHLIGHT}$ISO${ANSI_RESET}' in QEMU (with legacy BIOS firmware)"
120+
fn_run_qemu
121+
}
122+
123+
function fn_run_test_uefi() {
124+
local ISO=$1
125+
local QEMU_ARGS=("${QEMU_ARGS_BASE[@]}") # copy array
126+
local QEMU_ARGS+=(
127+
# Usually, this comes from the Nix shell.
128+
-bios $OVMF
129+
-cdrom "$ISO"
130+
)
131+
echo -e "Running '${ANSI_HIGHLIGHT}$ISO${ANSI_RESET}' in QEMU (with UEFI/OVMF firmware)"
132+
fn_run_qemu $QEMU_ARGS
133+
}
134+
135+
function fn_test_payload() {
136+
local TEST_DIR=tests/01-boot-payload
137+
fn_prepare_test_vol
138+
139+
cp $BINS_DIR/multiboot2_payload $TEST_VOL/kernel
140+
141+
fn_build_limine_iso
142+
143+
fn_run_test_bios $TEST_DIR/image.iso
144+
fn_run_test_uefi $TEST_DIR/image.iso
145+
}
146+
147+
# Tests the loader by chainloading the Multiboot2 payload.
148+
function fn_test_loader() {
149+
local TEST_DIR=tests/02-boot-loader-and-chainload
150+
fn_prepare_test_vol
151+
152+
cp $BINS_DIR/multiboot2_chainloader $TEST_VOL/kernel
153+
cp $BINS_DIR/multiboot2_payload $TEST_VOL/payload
154+
155+
fn_build_limine_iso
156+
157+
fn_run_test_bios $TEST_DIR/image.iso
158+
fn_run_test_uefi $TEST_DIR/image.iso
34159
}
35160

36161
fn_main

integration-test/tests/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vol
2+
image.iso
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
TIMEOUT=0
2+
SERIAL=yes
3+
VERBOSE=yes
4+
INTERFACE_BRANDING=integration-test
5+
6+
:integration-test
7+
PROTOCOL=multiboot2
8+
KERNEL_PATH=boot:///kernel
9+
KERNEL_CMDLINE=some kernel cmdline
10+
MODULE_PATH=boot:///limine.cfg
11+
MODULE_STRING=Limine bootloader config
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
TIMEOUT=0
2+
SERIAL=yes
3+
VERBOSE=yes
4+
INTERFACE_BRANDING=integration-test
5+
6+
:integration-test
7+
# For simplicity reasons, the loader itself boots via Multiboot 1. Sufficient.
8+
PROTOCOL=multiboot
9+
KERNEL_PATH=boot:///kernel
10+
KERNEL_CMDLINE=some kernel cmdline
11+
MODULE_PATH=boot:///payload
12+
MODULE_STRING=multiboot2_payload

integration-test/tests/multiboot2-header/README.md

-7
This file was deleted.

integration-test/tests/multiboot2-header/run_qemu.sh

-41
This file was deleted.

integration-test/tests/multiboot2/.gitignore

-2
This file was deleted.

integration-test/tests/multiboot2/README.md

-5
This file was deleted.

integration-test/tests/multiboot2/build_img.sh

-29
This file was deleted.

0 commit comments

Comments
 (0)