From de3ba3e871d57ae6b6c0776292a22b6bb1d110ee Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Mon, 12 Jul 2021 22:08:22 +0200 Subject: [PATCH] Refactor Tests and Makefile - Carve out common files for tests - Add boot tests starting tutorial 3 - Overhaul the Makefile for more structure --- .githooks/pre-commit | 3 +- 01_wait_forever/Makefile | 81 ++- 02_runtime_init/Makefile | 81 ++- 02_runtime_init/README.md | 4 +- 03_hacky_hello_world/Makefile | 109 +++- 03_hacky_hello_world/README.md | 70 ++- .../tests/boot_test_string.rb | 3 + 04_safe_globals/Makefile | 109 +++- 04_safe_globals/tests/boot_test_string.rb | 3 + 05_drivers_gpio_uart/Makefile | 124 +++- 05_drivers_gpio_uart/README.md | 66 ++- .../tests/boot_test_string.rb | 3 + 06_uart_chainloader/Makefile | 181 ++++-- 06_uart_chainloader/README.md | 235 +++++--- 06_uart_chainloader/tests/chainboot_test.rb | 80 +++ 06_uart_chainloader/tests/qemu_minipush.rb | 80 --- 07_timestamps/Makefile | 125 +++- 07_timestamps/README.md | 233 ++++---- 07_timestamps/tests/boot_test_string.rb | 3 + 08_hw_debug_JTAG/Makefile | 180 ++++-- 08_hw_debug_JTAG/README.md | 60 +- 08_hw_debug_JTAG/tests/boot_test_string.rb | 3 + 09_privilege_level/Makefile | 180 ++++-- 09_privilege_level/README.md | 9 + 09_privilege_level/tests/boot_test_string.rb | 3 + .../Makefile | 180 ++++-- .../tests/boot_test_string.rb | 3 + 11_exceptions_part1_groundwork/Makefile | 180 ++++-- 11_exceptions_part1_groundwork/README.md | 9 + .../tests/boot_test_string.rb | 3 + 12_integrated_testing/Makefile | 253 +++++--- 12_integrated_testing/README.md | 555 ++++++++---------- 12_integrated_testing/src/lib.rs | 3 +- 12_integrated_testing/src/panic_wait.rs | 4 +- .../tests/00_console_sanity.rb | 13 +- .../tests/00_console_sanity.rs | 9 +- .../tests/02_exception_sync_page_fault.rs | 2 +- .../tests/boot_test_string.rb | 3 + 12_integrated_testing/tests/runner.rb | 143 ----- 13_exceptions_part2_peripheral_IRQs/Makefile | 253 +++++--- 13_exceptions_part2_peripheral_IRQs/README.md | 13 + .../src/lib.rs | 3 +- .../src/panic_wait.rs | 4 +- .../tests/00_console_sanity.rb | 13 +- .../tests/00_console_sanity.rs | 9 +- .../tests/02_exception_sync_page_fault.rs | 2 +- .../tests/boot_test_string.rb | 3 + .../tests/runner.rb | 143 ----- 14_virtual_mem_part2_mmio_remap/Makefile | 253 +++++--- 14_virtual_mem_part2_mmio_remap/README.md | 2 +- 14_virtual_mem_part2_mmio_remap/src/lib.rs | 3 +- .../src/panic_wait.rs | 4 +- .../tests/00_console_sanity.rb | 13 +- .../tests/00_console_sanity.rs | 9 +- .../tests/02_exception_sync_page_fault.rs | 2 +- .../tests/boot_test_string.rb | 3 + .../tests/runner.rb | 143 ----- .../Makefile | 258 +++++--- .../README.md | 24 +- .../src/lib.rs | 3 +- .../src/panic_wait.rs | 4 +- .../tests/00_console_sanity.rb | 13 +- .../tests/00_console_sanity.rs | 9 +- .../tests/02_exception_sync_page_fault.rs | 2 +- .../tests/boot_test_string.rb | 3 + .../tests/runner.rb | 143 ----- .../Makefile | 258 +++++--- .../README.md | 2 +- .../src/lib.rs | 3 +- .../src/panic_wait.rs | 4 +- .../tests/00_console_sanity.rb | 13 +- .../tests/00_console_sanity.rs | 9 +- .../tests/02_exception_sync_page_fault.rs | 2 +- .../tests/boot_test_string.rb | 3 + .../tests/runner.rb | 143 ----- X1_JTAG_boot/Makefile | 125 +++- X1_JTAG_boot/tests/boot_test_string.rb | 3 + {utils => common}/color.mk.in | 0 {utils => common/serial}/minipush.rb | 33 +- .../serial}/minipush/progressbar_patch.rb | 0 {utils => common/serial}/miniterm.rb | 3 +- common/tests/boot_test.rb | 75 +++ common/tests/console_io_test.rb | 48 ++ common/tests/dispatch.rb | 35 ++ common/tests/exit_code_test.rb | 52 ++ common/tests/test.rb | 82 +++ devtool_completion.bash | 2 +- doc/12_demo.gif | Bin 0 -> 224703 bytes doc/13_demo.gif | Bin 441164 -> 0 bytes doc/demo_PS1.txt | 1 + utils/devtool.rb | 126 ++-- utils/devtool/copyright.rb | 2 + utils/update_copyright.rb | 2 + 93 files changed, 3558 insertions(+), 2190 deletions(-) create mode 100644 03_hacky_hello_world/tests/boot_test_string.rb create mode 100644 04_safe_globals/tests/boot_test_string.rb create mode 100644 05_drivers_gpio_uart/tests/boot_test_string.rb create mode 100644 06_uart_chainloader/tests/chainboot_test.rb delete mode 100644 06_uart_chainloader/tests/qemu_minipush.rb create mode 100644 07_timestamps/tests/boot_test_string.rb create mode 100644 08_hw_debug_JTAG/tests/boot_test_string.rb create mode 100644 09_privilege_level/tests/boot_test_string.rb create mode 100644 10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb create mode 100644 11_exceptions_part1_groundwork/tests/boot_test_string.rb create mode 100644 12_integrated_testing/tests/boot_test_string.rb delete mode 100755 12_integrated_testing/tests/runner.rb create mode 100644 13_exceptions_part2_peripheral_IRQs/tests/boot_test_string.rb delete mode 100755 13_exceptions_part2_peripheral_IRQs/tests/runner.rb create mode 100644 14_virtual_mem_part2_mmio_remap/tests/boot_test_string.rb delete mode 100755 14_virtual_mem_part2_mmio_remap/tests/runner.rb create mode 100644 15_virtual_mem_part3_precomputed_tables/tests/boot_test_string.rb delete mode 100755 15_virtual_mem_part3_precomputed_tables/tests/runner.rb create mode 100644 16_virtual_mem_part4_higher_half_kernel/tests/boot_test_string.rb delete mode 100755 16_virtual_mem_part4_higher_half_kernel/tests/runner.rb create mode 100644 X1_JTAG_boot/tests/boot_test_string.rb rename {utils => common}/color.mk.in (100%) rename {utils => common/serial}/minipush.rb (83%) rename {utils => common/serial}/minipush/progressbar_patch.rb (100%) rename {utils => common/serial}/miniterm.rb (99%) create mode 100644 common/tests/boot_test.rb create mode 100644 common/tests/console_io_test.rb create mode 100755 common/tests/dispatch.rb create mode 100644 common/tests/exit_code_test.rb create mode 100644 common/tests/test.rb create mode 100644 doc/12_demo.gif delete mode 100644 doc/13_demo.gif create mode 100644 doc/demo_PS1.txt diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 6636f007..46890643 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,8 +5,6 @@ # # Copyright (c) 2018-2021 Andre Richter -require 'rubygems' -require 'bundler/setup' require_relative '../utils/devtool/copyright' def copyright_check(staged_files) @@ -14,6 +12,7 @@ def copyright_check(staged_files) staged_files = staged_files.select do |f| next if f.include?('build.rs') + next if f.include?('boot_test_string.rb') f.include?('Makefile') || f.include?('Dockerfile') || diff --git a/01_wait_forever/Makefile b/01_wait_forever/Makefile index 2e44e7ac..16607fe6 100644 --- a/01_wait_forever/Makefile +++ b/01_wait_forever/Makefile @@ -2,12 +2,22 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 -# BSP-specific arguments + + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -32,11 +42,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -53,61 +70,99 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ --section .text \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json diff --git a/02_runtime_init/Makefile b/02_runtime_init/Makefile index a20b283b..ea979232 100644 --- a/02_runtime_init/Makefile +++ b/02_runtime_init/Makefile @@ -2,12 +2,22 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 -# BSP-specific arguments + + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -32,11 +42,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -53,51 +70,84 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -106,10 +156,15 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json diff --git a/02_runtime_init/README.md b/02_runtime_init/README.md index 11ad00cc..d37b3c9f 100644 --- a/02_runtime_init/README.md +++ b/02_runtime_init/README.md @@ -53,7 +53,7 @@ diff -uNr 01_wait_forever/Cargo.toml 02_runtime_init/Cargo.toml diff -uNr 01_wait_forever/Makefile 02_runtime_init/Makefile --- 01_wait_forever/Makefile +++ 02_runtime_init/Makefile -@@ -102,6 +102,8 @@ +@@ -152,6 +152,8 @@ $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ --section .text \ @@ -61,7 +61,7 @@ diff -uNr 01_wait_forever/Makefile 02_runtime_init/Makefile + --section .got \ $(KERNEL_ELF) | rustfilt - nm: $(KERNEL_ELF) + ##------------------------------------------------------------------------------ diff -uNr 01_wait_forever/src/_arch/aarch64/cpu/boot.rs 02_runtime_init/src/_arch/aarch64/cpu/boot.rs --- 01_wait_forever/src/_arch/aarch64/cpu/boot.rs diff --git a/03_hacky_hello_world/Makefile b/03_hacky_hello_world/Makefile index 2ed82a5f..19e50b33 100644 --- a/03_hacky_hello_world/Makefile +++ b/03_hacky_hello_world/Makefile @@ -2,12 +2,22 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 -# BSP-specific arguments + + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -32,11 +42,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -53,51 +70,87 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -106,10 +159,40 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/03_hacky_hello_world/README.md b/03_hacky_hello_world/README.md index 01bbf54c..ce262bc9 100644 --- a/03_hacky_hello_world/README.md +++ b/03_hacky_hello_world/README.md @@ -12,6 +12,10 @@ - `src/console.rs` introduces interface `Traits` for console commands. - `src/bsp/raspberrypi/console.rs` implements the interface for QEMU's emulated UART. - The panic handler makes use of the new `print!()` to display user error messages. +- There is a new Makefile target, `make test`, intended for automated testing. It boots the compiled + kernel in `QEMU`, and checks for an expected output string produced by the kernel. + - In this tutorial, it checks for the string `Stopping here`, which is emitted by the `panic!()` + at the end of `main.rs`. ## Test it @@ -43,7 +47,7 @@ diff -uNr 02_runtime_init/Cargo.toml 03_hacky_hello_world/Cargo.toml diff -uNr 02_runtime_init/Makefile 03_hacky_hello_world/Makefile --- 02_runtime_init/Makefile +++ 03_hacky_hello_world/Makefile -@@ -13,7 +13,7 @@ +@@ -23,7 +23,7 @@ KERNEL_BIN = kernel8.img QEMU_BINARY = qemu-system-aarch64 QEMU_MACHINE_TYPE = raspi3 @@ -52,7 +56,7 @@ diff -uNr 02_runtime_init/Makefile 03_hacky_hello_world/Makefile OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf -@@ -24,7 +24,7 @@ +@@ -34,7 +34,7 @@ KERNEL_BIN = kernel8.img QEMU_BINARY = qemu-system-aarch64 QEMU_MACHINE_TYPE = @@ -61,6 +65,60 @@ diff -uNr 02_runtime_init/Makefile 03_hacky_hello_world/Makefile OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf +@@ -70,17 +70,20 @@ + --strip-all \ + -O binary + +-EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) ++EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) ++EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb + + ##------------------------------------------------------------------------------ + ## Dockerization + ##------------------------------------------------------------------------------ +-DOCKER_IMAGE = rustembedded/osdev-utils +-DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +-DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i ++DOCKER_IMAGE = rustembedded/osdev-utils ++DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial ++DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i ++DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common + + DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) + DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) ++DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + + + +@@ -168,3 +171,28 @@ + ##------------------------------------------------------------------------------ + check: + @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json ++ ++ ++ ++##-------------------------------------------------------------------------------------------------- ++## Testing targets ++##-------------------------------------------------------------------------------------------------- ++.PHONY: test test_boot ++ ++ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. ++ ++test_boot test : ++ $(call colorecho, "\n$(QEMU_MISSING_STRING)") ++ ++else # QEMU is supported. ++ ++##------------------------------------------------------------------------------ ++## Run boot test ++##------------------------------------------------------------------------------ ++test_boot: $(KERNEL_BIN) ++ $(call colorecho, "\nBoot test - $(BSP)") ++ @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) ++ ++test: test_boot ++ ++endif diff -uNr 02_runtime_init/src/bsp/raspberrypi/console.rs 03_hacky_hello_world/src/bsp/raspberrypi/console.rs --- 02_runtime_init/src/bsp/raspberrypi/console.rs @@ -245,4 +303,12 @@ diff -uNr 02_runtime_init/src/print.rs 03_hacky_hello_world/src/print.rs + }) +} +diff -uNr 02_runtime_init/tests/boot_test_string.rb 03_hacky_hello_world/tests/boot_test_string.rb +--- 02_runtime_init/tests/boot_test_string.rb ++++ 03_hacky_hello_world/tests/boot_test_string.rb +@@ -0,0 +1,3 @@ ++# frozen_string_literal: true ++ ++EXPECTED_PRINT = 'Stopping here' + ``` diff --git a/03_hacky_hello_world/tests/boot_test_string.rb b/03_hacky_hello_world/tests/boot_test_string.rb new file mode 100644 index 00000000..b0c59536 --- /dev/null +++ b/03_hacky_hello_world/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Stopping here' diff --git a/04_safe_globals/Makefile b/04_safe_globals/Makefile index 2ed82a5f..19e50b33 100644 --- a/04_safe_globals/Makefile +++ b/04_safe_globals/Makefile @@ -2,12 +2,22 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 -# BSP-specific arguments + + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -32,11 +42,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -53,51 +70,87 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -106,10 +159,40 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/04_safe_globals/tests/boot_test_string.rb b/04_safe_globals/tests/boot_test_string.rb new file mode 100644 index 00000000..b0c59536 --- /dev/null +++ b/04_safe_globals/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Stopping here' diff --git a/05_drivers_gpio_uart/Makefile b/05_drivers_gpio_uart/Makefile index 1dd6cc2b..c38e049e 100644 --- a/05_drivers_gpio_uart/Makefile +++ b/05_drivers_gpio_uart/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -38,11 +45,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -59,64 +73,102 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINITERM = ruby ../common/serial/miniterm.rb -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DEV = --privileged -v /dev:/dev +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) + DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINITERM = ruby ../utils/miniterm.rb + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu miniterm clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +##------------------------------------------------------------------------------ +## Connect to the target's serial +##------------------------------------------------------------------------------ miniterm: @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -125,10 +177,40 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/05_drivers_gpio_uart/README.md b/05_drivers_gpio_uart/README.md index 7f0bbda5..2ee89aa6 100644 --- a/05_drivers_gpio_uart/README.md +++ b/05_drivers_gpio_uart/README.md @@ -145,55 +145,64 @@ diff -uNr 04_safe_globals/Cargo.toml 05_drivers_gpio_uart/Cargo.toml diff -uNr 04_safe_globals/Makefile 05_drivers_gpio_uart/Makefile --- 04_safe_globals/Makefile +++ 05_drivers_gpio_uart/Makefile -@@ -7,6 +7,12 @@ - # Default to the RPi3 +@@ -11,6 +11,9 @@ + # Default to the RPi3. BSP ?= rpi3 +# Default to a serial device name that is common in Linux. +DEV_SERIAL ?= /dev/ttyUSB0 + -+# Query the host system's kernel name -+UNAME_S = $(shell uname -s) -+ - # BSP-specific arguments - ifeq ($(BSP),rpi3) - TARGET = aarch64-unknown-none-softfloat -@@ -58,13 +64,23 @@ - DOCKER_IMAGE = rustembedded/osdev-utils - DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial - DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -+DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -+DOCKER_ARG_DEV = --privileged -v /dev:/dev + + + ##-------------------------------------------------------------------------------------------------- +@@ -72,6 +75,7 @@ + + EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) + EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb ++EXEC_MINITERM = ruby ../common/serial/miniterm.rb + + ##------------------------------------------------------------------------------ + ## Dockerization +@@ -80,17 +84,25 @@ + DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial + DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i + DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common ++DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) + DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) --EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -+# Dockerize commands that require USB device passthrough only on Linux -+ifeq ($(UNAME_S),Linux) ++# Dockerize commands, which require USB device passthrough, only on Linux. ++ifeq ($(shell uname -s),Linux) + DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) + -+ DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) ++ DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +endif + -+EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -+EXEC_MINITERM = ruby ../utils/miniterm.rb + + ##-------------------------------------------------------------------------------------------------- + ## Targets + ##-------------------------------------------------------------------------------------------------- -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu miniterm clippy clean readelf objdump nm check all: $(KERNEL_BIN) -@@ -88,6 +104,9 @@ - @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) +@@ -130,6 +142,12 @@ endif + ##------------------------------------------------------------------------------ ++## Connect to the target's serial ++##------------------------------------------------------------------------------ +miniterm: + @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) + ++##------------------------------------------------------------------------------ + ## Run clippy + ##------------------------------------------------------------------------------ clippy: - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) - diff -uNr 04_safe_globals/src/_arch/aarch64/cpu.rs 05_drivers_gpio_uart/src/_arch/aarch64/cpu.rs --- 04_safe_globals/src/_arch/aarch64/cpu.rs @@ -1451,4 +1460,13 @@ diff -uNr 04_safe_globals/src/panic_wait.rs 05_drivers_gpio_uart/src/panic_wait. cpu::wait_forever() +diff -uNr 04_safe_globals/tests/boot_test_string.rb 05_drivers_gpio_uart/tests/boot_test_string.rb +--- 04_safe_globals/tests/boot_test_string.rb ++++ 05_drivers_gpio_uart/tests/boot_test_string.rb +@@ -1,3 +1,3 @@ + # frozen_string_literal: true + +-EXPECTED_PRINT = 'Stopping here' ++EXPECTED_PRINT = 'Echoing input now' + ``` diff --git a/05_drivers_gpio_uart/tests/boot_test_string.rb b/05_drivers_gpio_uart/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/05_drivers_gpio_uart/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/06_uart_chainloader/Makefile b/06_uart_chainloader/Makefile index a8d38ffc..3064e67a 100644 --- a/06_uart_chainloader/Makefile +++ b/06_uart_chainloader/Makefile @@ -2,49 +2,63 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) - TARGET = aarch64-unknown-none-softfloat - KERNEL_BIN = kernel8.img - QEMU_BINARY = qemu-system-aarch64 - QEMU_MACHINE_TYPE = raspi3 - QEMU_RELEASE_ARGS = -serial stdio -display none - OBJDUMP_BINARY = aarch64-none-elf-objdump - NM_BINARY = aarch64-none-elf-nm - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 + TARGET = aarch64-unknown-none-softfloat + KERNEL_BIN = kernel8.img + QEMU_BINARY = qemu-system-aarch64 + QEMU_MACHINE_TYPE = raspi3 + QEMU_RELEASE_ARGS = -serial stdio -display none + OBJDUMP_BINARY = aarch64-none-elf-objdump + NM_BINARY = aarch64-none-elf-nm + READELF_BINARY = aarch64-none-elf-readelf + LINKER_FILE = src/bsp/raspberrypi/link.ld + RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img else ifeq ($(BSP),rpi4) - TARGET = aarch64-unknown-none-softfloat - KERNEL_BIN = kernel8.img - QEMU_BINARY = qemu-system-aarch64 - QEMU_MACHINE_TYPE = - QEMU_RELEASE_ARGS = -serial stdio -display none - OBJDUMP_BINARY = aarch64-none-elf-objdump - NM_BINARY = aarch64-none-elf-nm - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 + TARGET = aarch64-unknown-none-softfloat + KERNEL_BIN = kernel8.img + QEMU_BINARY = qemu-system-aarch64 + QEMU_MACHINE_TYPE = + QEMU_RELEASE_ARGS = -serial stdio -display none + OBJDUMP_BINARY = aarch64-none-elf-objdump + NM_BINARY = aarch64-none-elf-nm + READELF_BINARY = aarch64-none-elf-readelf + LINKER_FILE = src/bsp/raspberrypi/link.ld + RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -61,49 +75,69 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DEV = --privileged -v /dev:/dev +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) -t $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -EXEC_QEMU_MINIPUSH = ruby tests/qemu_minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu qemuasm chainboot clippy clean readelf objdump nm \ - check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu qemuasm: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) @@ -112,26 +146,36 @@ qemuasm: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU with ASM output") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm -test: $(KERNEL_BIN) - $(call colorecho, "\nTesting chainloading - $(BSP)") - @$(DOCKER_TEST) $(EXEC_QEMU_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ - -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) - endif -chainboot: +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ +chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD) +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -140,10 +184,41 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ + -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) + +test: test_boot + +endif diff --git a/06_uart_chainloader/README.md b/06_uart_chainloader/README.md index 0f9e4aa6..d606f536 100644 --- a/06_uart_chainloader/README.md +++ b/06_uart_chainloader/README.md @@ -137,57 +137,95 @@ Binary files 05_drivers_gpio_uart/demo_payload_rpi4.img and 06_uart_chainloader/ diff -uNr 05_drivers_gpio_uart/Makefile 06_uart_chainloader/Makefile --- 05_drivers_gpio_uart/Makefile +++ 06_uart_chainloader/Makefile -@@ -25,6 +25,7 @@ - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 +@@ -22,27 +22,29 @@ + + # BSP-specific arguments. + ifeq ($(BSP),rpi3) +- TARGET = aarch64-unknown-none-softfloat +- KERNEL_BIN = kernel8.img +- QEMU_BINARY = qemu-system-aarch64 +- QEMU_MACHINE_TYPE = raspi3 +- QEMU_RELEASE_ARGS = -serial stdio -display none +- OBJDUMP_BINARY = aarch64-none-elf-objdump +- NM_BINARY = aarch64-none-elf-nm +- READELF_BINARY = aarch64-none-elf-readelf +- LINKER_FILE = src/bsp/raspberrypi/link.ld +- RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 ++ TARGET = aarch64-unknown-none-softfloat ++ KERNEL_BIN = kernel8.img ++ QEMU_BINARY = qemu-system-aarch64 ++ QEMU_MACHINE_TYPE = raspi3 ++ QEMU_RELEASE_ARGS = -serial stdio -display none ++ OBJDUMP_BINARY = aarch64-none-elf-objdump ++ NM_BINARY = aarch64-none-elf-nm ++ READELF_BINARY = aarch64-none-elf-readelf ++ LINKER_FILE = src/bsp/raspberrypi/link.ld ++ RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 + CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img else ifeq ($(BSP),rpi4) - TARGET = aarch64-unknown-none-softfloat - KERNEL_BIN = kernel8.img -@@ -36,6 +37,7 @@ - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 +- TARGET = aarch64-unknown-none-softfloat +- KERNEL_BIN = kernel8.img +- QEMU_BINARY = qemu-system-aarch64 +- QEMU_MACHINE_TYPE = +- QEMU_RELEASE_ARGS = -serial stdio -display none +- OBJDUMP_BINARY = aarch64-none-elf-objdump +- NM_BINARY = aarch64-none-elf-nm +- READELF_BINARY = aarch64-none-elf-readelf +- LINKER_FILE = src/bsp/raspberrypi/link.ld +- RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 ++ TARGET = aarch64-unknown-none-softfloat ++ KERNEL_BIN = kernel8.img ++ QEMU_BINARY = qemu-system-aarch64 ++ QEMU_MACHINE_TYPE = ++ QEMU_RELEASE_ARGS = -serial stdio -display none ++ OBJDUMP_BINARY = aarch64-none-elf-objdump ++ NM_BINARY = aarch64-none-elf-nm ++ READELF_BINARY = aarch64-none-elf-readelf ++ LINKER_FILE = src/bsp/raspberrypi/link.ld ++ RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 + CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img endif - # Export for build.rs -@@ -68,19 +70,22 @@ - DOCKER_ARG_DEV = --privileged -v /dev:/dev + QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +@@ -74,8 +76,8 @@ + -O binary - DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -+DOCKER_TEST = $(DOCKER_CMD) -t $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) + EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +-EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +-EXEC_MINITERM = ruby ../common/serial/miniterm.rb ++EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb ++EXEC_MINIPUSH = ruby ../common/serial/minipush.rb - # Dockerize commands that require USB device passthrough only on Linux - ifeq ($(UNAME_S),Linux) + ##------------------------------------------------------------------------------ + ## Dockerization +@@ -94,7 +96,7 @@ + ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) -- DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) -+ DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) +- DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) ++ DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) endif --EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) --EXEC_MINITERM = ruby ../utils/miniterm.rb -+EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -+EXEC_MINIPUSH = ruby ../utils/minipush.rb -+EXEC_QEMU_MINIPUSH = ruby tests/qemu_minipush.rb +@@ -102,7 +104,7 @@ + ##-------------------------------------------------------------------------------------------------- + ## Targets + ##-------------------------------------------------------------------------------------------------- -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu miniterm clippy clean readelf objdump nm check -+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu qemuasm chainboot clippy clean readelf objdump nm \ -+ check ++.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) -@@ -96,16 +101,26 @@ - @$(DOC_CMD) --document-private-items --open +@@ -131,7 +133,7 @@ + ##------------------------------------------------------------------------------ + ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. - ifeq ($(QEMU_MACHINE_TYPE),) -qemu: -+qemu test: ++qemu qemuasm: $(call colorecho, "\n$(QEMU_MISSING_STRING)") - else + + else # QEMU is supported. +@@ -139,13 +141,18 @@ qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) @@ -195,21 +233,30 @@ diff -uNr 05_drivers_gpio_uart/Makefile 06_uart_chainloader/Makefile +qemuasm: $(KERNEL_BIN) + $(call colorecho, "\nLaunching QEMU with ASM output") + @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm -+ -+test: $(KERNEL_BIN) -+ $(call colorecho, "\nTesting chainloading - $(BSP)") -+ @$(DOCKER_TEST) $(EXEC_QEMU_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ -+ -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) + endif + ##------------------------------------------------------------------------------ +-## Connect to the target's serial ++## Push the kernel to the real HW target + ##------------------------------------------------------------------------------ -miniterm: - @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) -+chainboot: ++chainboot: $(KERNEL_BIN) + @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD) - clippy: - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) + ##------------------------------------------------------------------------------ + ## Run clippy +@@ -209,7 +216,8 @@ + ##------------------------------------------------------------------------------ + test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") +- @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) ++ @$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ ++ -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) + + test: test_boot + diff -uNr 05_drivers_gpio_uart/src/_arch/aarch64/cpu/boot.s 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s --- 05_drivers_gpio_uart/src/_arch/aarch64/cpu/boot.s @@ -492,9 +539,17 @@ diff -uNr 05_drivers_gpio_uart/src/main.rs 06_uart_chainloader/src/main.rs + kernel() } -diff -uNr 05_drivers_gpio_uart/tests/qemu_minipush.rb 06_uart_chainloader/tests/qemu_minipush.rb ---- 05_drivers_gpio_uart/tests/qemu_minipush.rb -+++ 06_uart_chainloader/tests/qemu_minipush.rb +diff -uNr 05_drivers_gpio_uart/tests/boot_test_string.rb 06_uart_chainloader/tests/boot_test_string.rb +--- 05_drivers_gpio_uart/tests/boot_test_string.rb ++++ 06_uart_chainloader/tests/boot_test_string.rb +@@ -1,3 +0,0 @@ +-# frozen_string_literal: true +- +-EXPECTED_PRINT = 'Echoing input now' + +diff -uNr 05_drivers_gpio_uart/tests/chainboot_test.rb 06_uart_chainloader/tests/chainboot_test.rb +--- 05_drivers_gpio_uart/tests/chainboot_test.rb ++++ 06_uart_chainloader/tests/chainboot_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + @@ -502,80 +557,80 @@ diff -uNr 05_drivers_gpio_uart/tests/qemu_minipush.rb 06_uart_chainloader/tests/ +# +# Copyright (c) 2020-2021 Andre Richter + -+require_relative '../../utils/minipush' -+require 'expect' -+require 'timeout' ++require_relative '../../common/serial/minipush' ++require_relative '../../common/tests/boot_test' ++require 'pty' + +# Match for the last print that 'demo_payload_rpiX.img' produces. +EXPECTED_PRINT = 'Echoing input now' + -+# The main class -+class QEMUMiniPush < MiniPush -+ TIMEOUT_SECS = 3 ++# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected ++# to a QEMU instance instead of a real HW. ++class ChainbootTest < BootTest ++ MINIPUSH = '../common/serial/minipush.rb' ++ MINIPUSH_POWER_TARGET_REQUEST = 'Please power the target now' + -+ # override -+ def initialize(qemu_cmd, binary_image_path) -+ super(nil, binary_image_path) ++ def initialize(qemu_cmd, payload_path) ++ super(qemu_cmd, EXPECTED_PRINT) ++ ++ @test_name = 'Boot test using Minipush' + -+ @qemu_cmd = qemu_cmd ++ @payload_path = payload_path + end + + private + -+ def quit_qemu_graceful -+ Timeout.timeout(5) do -+ pid = @target_serial.pid -+ Process.kill('TERM', pid) -+ Process.wait(pid) -+ end -+ end -+ + # override -+ def open_serial -+ @target_serial = IO.popen(@qemu_cmd, 'r+', err: '/dev/null') ++ def post_process_and_add_output(output) ++ temp = output.join.split("\r\n") + -+ # Ensure all output is immediately flushed to the device. -+ @target_serial.sync = true ++ # Should a line have solo carriage returns, remove any overridden parts of the string. ++ temp.map! { |x| x.gsub(/.*\r/, '') } + -+ puts "[#{@name_short}] ✅ Serial connected" ++ @test_output += temp + end + -+ # override -+ def terminal -+ result = @target_serial.expect(EXPECTED_PRINT, TIMEOUT_SECS) -+ exit(1) if result.nil? -+ -+ puts result -+ -+ quit_qemu_graceful ++ def wait_for_minipush_power_request(mp_out) ++ output = [] ++ Timeout.timeout(MAX_WAIT_SECS) do ++ loop do ++ output << mp_out.gets ++ break if output.last.include?(MINIPUSH_POWER_TARGET_REQUEST) ++ end ++ end ++ rescue Timeout::Error ++ @test_error = 'Timed out waiting for power request' ++ rescue StandardError => e ++ @test_error = e.message ++ ensure ++ post_process_and_add_output(output) + end + + # override -+ def connetion_reset; end ++ def setup ++ pty_main, pty_secondary = PTY.open ++ mp_out, _mp_in = PTY.spawn("ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}") + -+ # override -+ def handle_reconnect(error) -+ handle_unexpected(error) ++ # Wait until MiniPush asks for powering the target. ++ wait_for_minipush_power_request(mp_out) ++ ++ # Now is the time to start QEMU with the chainloader binary. QEMU's virtual tty is connected ++ # to the MiniPush instance spawned above, so that the two processes talk to each other. ++ Process.spawn(@qemu_cmd, in: pty_main, out: pty_main) ++ ++ # The remainder of the test is done by the parent class' run_concrete_test, which listens on ++ # @qemu_serial. Hence, point it to MiniPush's output. ++ @qemu_serial = mp_out + end +end + +##-------------------------------------------------------------------------------------------------- +## Execution starts here +##-------------------------------------------------------------------------------------------------- -+puts -+puts 'QEMUMiniPush 1.0'.cyan -+puts -+ -+# CTRL + C handler. Only here to suppress Ruby's default exception print. -+trap('INT') do -+ # The `ensure` block from `QEMUMiniPush::run` will run after exit, restoring console state. -+ exit -+end -+ -+binary_image_path = ARGV.pop ++payload_path = ARGV.pop +qemu_cmd = ARGV.join(' ') + -+QEMUMiniPush.new(qemu_cmd, binary_image_path).run ++ChainbootTest.new(qemu_cmd, payload_path).run diff -uNr 05_drivers_gpio_uart/update.sh 06_uart_chainloader/update.sh --- 05_drivers_gpio_uart/update.sh diff --git a/06_uart_chainloader/tests/chainboot_test.rb b/06_uart_chainloader/tests/chainboot_test.rb new file mode 100644 index 00000000..3dee0f9c --- /dev/null +++ b/06_uart_chainloader/tests/chainboot_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2020-2021 Andre Richter + +require_relative '../../common/serial/minipush' +require_relative '../../common/tests/boot_test' +require 'pty' + +# Match for the last print that 'demo_payload_rpiX.img' produces. +EXPECTED_PRINT = 'Echoing input now' + +# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected +# to a QEMU instance instead of a real HW. +class ChainbootTest < BootTest + MINIPUSH = '../common/serial/minipush.rb' + MINIPUSH_POWER_TARGET_REQUEST = 'Please power the target now' + + def initialize(qemu_cmd, payload_path) + super(qemu_cmd, EXPECTED_PRINT) + + @test_name = 'Boot test using Minipush' + + @payload_path = payload_path + end + + private + + # override + def post_process_and_add_output(output) + temp = output.join.split("\r\n") + + # Should a line have solo carriage returns, remove any overridden parts of the string. + temp.map! { |x| x.gsub(/.*\r/, '') } + + @test_output += temp + end + + def wait_for_minipush_power_request(mp_out) + output = [] + Timeout.timeout(MAX_WAIT_SECS) do + loop do + output << mp_out.gets + break if output.last.include?(MINIPUSH_POWER_TARGET_REQUEST) + end + end + rescue Timeout::Error + @test_error = 'Timed out waiting for power request' + rescue StandardError => e + @test_error = e.message + ensure + post_process_and_add_output(output) + end + + # override + def setup + pty_main, pty_secondary = PTY.open + mp_out, _mp_in = PTY.spawn("ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}") + + # Wait until MiniPush asks for powering the target. + wait_for_minipush_power_request(mp_out) + + # Now is the time to start QEMU with the chainloader binary. QEMU's virtual tty is connected + # to the MiniPush instance spawned above, so that the two processes talk to each other. + Process.spawn(@qemu_cmd, in: pty_main, out: pty_main) + + # The remainder of the test is done by the parent class' run_concrete_test, which listens on + # @qemu_serial. Hence, point it to MiniPush's output. + @qemu_serial = mp_out + end +end + +##-------------------------------------------------------------------------------------------------- +## Execution starts here +##-------------------------------------------------------------------------------------------------- +payload_path = ARGV.pop +qemu_cmd = ARGV.join(' ') + +ChainbootTest.new(qemu_cmd, payload_path).run diff --git a/06_uart_chainloader/tests/qemu_minipush.rb b/06_uart_chainloader/tests/qemu_minipush.rb deleted file mode 100644 index 73857cd6..00000000 --- a/06_uart_chainloader/tests/qemu_minipush.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2020-2021 Andre Richter - -require_relative '../../utils/minipush' -require 'expect' -require 'timeout' - -# Match for the last print that 'demo_payload_rpiX.img' produces. -EXPECTED_PRINT = 'Echoing input now' - -# The main class -class QEMUMiniPush < MiniPush - TIMEOUT_SECS = 3 - - # override - def initialize(qemu_cmd, binary_image_path) - super(nil, binary_image_path) - - @qemu_cmd = qemu_cmd - end - - private - - def quit_qemu_graceful - Timeout.timeout(5) do - pid = @target_serial.pid - Process.kill('TERM', pid) - Process.wait(pid) - end - end - - # override - def open_serial - @target_serial = IO.popen(@qemu_cmd, 'r+', err: '/dev/null') - - # Ensure all output is immediately flushed to the device. - @target_serial.sync = true - - puts "[#{@name_short}] ✅ Serial connected" - end - - # override - def terminal - result = @target_serial.expect(EXPECTED_PRINT, TIMEOUT_SECS) - exit(1) if result.nil? - - puts result - - quit_qemu_graceful - end - - # override - def connetion_reset; end - - # override - def handle_reconnect(error) - handle_unexpected(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Execution starts here -##-------------------------------------------------------------------------------------------------- -puts -puts 'QEMUMiniPush 1.0'.cyan -puts - -# CTRL + C handler. Only here to suppress Ruby's default exception print. -trap('INT') do - # The `ensure` block from `QEMUMiniPush::run` will run after exit, restoring console state. - exit -end - -binary_image_path = ARGV.pop -qemu_cmd = ARGV.join(' ') - -QEMUMiniPush.new(qemu_cmd, binary_image_path).run diff --git a/07_timestamps/Makefile b/07_timestamps/Makefile index b5a56d07..8336ccb7 100644 --- a/07_timestamps/Makefile +++ b/07_timestamps/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -38,11 +45,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -59,64 +73,103 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DEV = --privileged -v /dev:/dev +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -125,10 +178,40 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/07_timestamps/README.md b/07_timestamps/README.md index 4e146bd0..4fb36ddf 100644 --- a/07_timestamps/README.md +++ b/07_timestamps/README.md @@ -62,76 +62,103 @@ Binary files 06_uart_chainloader/demo_payload_rpi4.img and 07_timestamps/demo_pa diff -uNr 06_uart_chainloader/Makefile 07_timestamps/Makefile --- 06_uart_chainloader/Makefile +++ 07_timestamps/Makefile -@@ -25,7 +25,6 @@ - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 +@@ -22,29 +22,27 @@ + + # BSP-specific arguments. + ifeq ($(BSP),rpi3) +- TARGET = aarch64-unknown-none-softfloat +- KERNEL_BIN = kernel8.img +- QEMU_BINARY = qemu-system-aarch64 +- QEMU_MACHINE_TYPE = raspi3 +- QEMU_RELEASE_ARGS = -serial stdio -display none +- OBJDUMP_BINARY = aarch64-none-elf-objdump +- NM_BINARY = aarch64-none-elf-nm +- READELF_BINARY = aarch64-none-elf-readelf +- LINKER_FILE = src/bsp/raspberrypi/link.ld +- RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 - CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img ++ TARGET = aarch64-unknown-none-softfloat ++ KERNEL_BIN = kernel8.img ++ QEMU_BINARY = qemu-system-aarch64 ++ QEMU_MACHINE_TYPE = raspi3 ++ QEMU_RELEASE_ARGS = -serial stdio -display none ++ OBJDUMP_BINARY = aarch64-none-elf-objdump ++ NM_BINARY = aarch64-none-elf-nm ++ READELF_BINARY = aarch64-none-elf-readelf ++ LINKER_FILE = src/bsp/raspberrypi/link.ld ++ RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 else ifeq ($(BSP),rpi4) - TARGET = aarch64-unknown-none-softfloat - KERNEL_BIN = kernel8.img -@@ -37,7 +36,6 @@ - READELF_BINARY = aarch64-none-elf-readelf - LINKER_FILE = src/bsp/raspberrypi/link.ld - RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 +- TARGET = aarch64-unknown-none-softfloat +- KERNEL_BIN = kernel8.img +- QEMU_BINARY = qemu-system-aarch64 +- QEMU_MACHINE_TYPE = +- QEMU_RELEASE_ARGS = -serial stdio -display none +- OBJDUMP_BINARY = aarch64-none-elf-objdump +- NM_BINARY = aarch64-none-elf-nm +- READELF_BINARY = aarch64-none-elf-readelf +- LINKER_FILE = src/bsp/raspberrypi/link.ld +- RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 - CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img ++ TARGET = aarch64-unknown-none-softfloat ++ KERNEL_BIN = kernel8.img ++ QEMU_BINARY = qemu-system-aarch64 ++ QEMU_MACHINE_TYPE = ++ QEMU_RELEASE_ARGS = -serial stdio -display none ++ OBJDUMP_BINARY = aarch64-none-elf-objdump ++ NM_BINARY = aarch64-none-elf-nm ++ READELF_BINARY = aarch64-none-elf-readelf ++ LINKER_FILE = src/bsp/raspberrypi/link.ld ++ RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif - # Export for build.rs -@@ -70,7 +68,6 @@ - DOCKER_ARG_DEV = --privileged -v /dev:/dev + QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +@@ -76,7 +74,7 @@ + -O binary - DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) --DOCKER_TEST = $(DOCKER_CMD) -t $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) + EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +-EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb ++EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb + EXEC_MINIPUSH = ruby ../common/serial/minipush.rb - # Dockerize commands that require USB device passthrough only on Linux -@@ -80,12 +77,10 @@ - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - endif - --EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) --EXEC_MINIPUSH = ruby ../utils/minipush.rb --EXEC_QEMU_MINIPUSH = ruby tests/qemu_minipush.rb -+EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -+EXEC_MINIPUSH = ruby ../utils/minipush.rb - --.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu qemuasm chainboot clippy clean readelf objdump nm \ -- check -+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check - - all: $(KERNEL_BIN) + ##------------------------------------------------------------------------------ +@@ -133,7 +131,7 @@ + ##------------------------------------------------------------------------------ + ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. -@@ -101,26 +96,16 @@ - @$(DOC_CMD) --document-private-items --open - - ifeq ($(QEMU_MACHINE_TYPE),) --qemu test: +-qemu qemuasm: +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") - else - qemu: $(KERNEL_BIN) + + else # QEMU is supported. +@@ -142,17 +140,13 @@ $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -- + -qemuasm: $(KERNEL_BIN) - $(call colorecho, "\nLaunching QEMU with ASM output") - @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm -- --test: $(KERNEL_BIN) -- $(call colorecho, "\nTesting chainloading - $(BSP)") -- @$(DOCKER_TEST) $(EXEC_QEMU_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ -- -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) - endif --chainboot: + ##------------------------------------------------------------------------------ + ## Push the kernel to the real HW target + ##------------------------------------------------------------------------------ + chainboot: $(KERNEL_BIN) - @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD) -+chainboot: $(KERNEL_BIN) + @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) - clippy: - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) + ##------------------------------------------------------------------------------ + ## Run clippy +@@ -216,8 +210,7 @@ + ##------------------------------------------------------------------------------ + test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") +- @$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \ +- -kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD) ++ @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + + test: test_boot + diff -uNr 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s 07_timestamps/src/_arch/aarch64/cpu/boot.s --- 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s @@ -720,9 +747,17 @@ diff -uNr 06_uart_chainloader/src/time.rs 07_timestamps/src/time.rs + } +} -diff -uNr 06_uart_chainloader/tests/qemu_minipush.rb 07_timestamps/tests/qemu_minipush.rb ---- 06_uart_chainloader/tests/qemu_minipush.rb -+++ 07_timestamps/tests/qemu_minipush.rb +diff -uNr 06_uart_chainloader/tests/boot_test_string.rb 07_timestamps/tests/boot_test_string.rb +--- 06_uart_chainloader/tests/boot_test_string.rb ++++ 07_timestamps/tests/boot_test_string.rb +@@ -0,0 +1,3 @@ ++# frozen_string_literal: true ++ ++EXPECTED_PRINT = 'Spinning for 1 second' + +diff -uNr 06_uart_chainloader/tests/chainboot_test.rb 07_timestamps/tests/chainboot_test.rb +--- 06_uart_chainloader/tests/chainboot_test.rb ++++ 07_timestamps/tests/chainboot_test.rb @@ -1,80 +0,0 @@ -# frozen_string_literal: true - @@ -730,80 +765,80 @@ diff -uNr 06_uart_chainloader/tests/qemu_minipush.rb 07_timestamps/tests/qemu_mi -# -# Copyright (c) 2020-2021 Andre Richter - --require_relative '../../utils/minipush' --require 'expect' --require 'timeout' +-require_relative '../../common/serial/minipush' +-require_relative '../../common/tests/boot_test' +-require 'pty' - -# Match for the last print that 'demo_payload_rpiX.img' produces. -EXPECTED_PRINT = 'Echoing input now' - --# The main class --class QEMUMiniPush < MiniPush -- TIMEOUT_SECS = 3 +-# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected +-# to a QEMU instance instead of a real HW. +-class ChainbootTest < BootTest +- MINIPUSH = '../common/serial/minipush.rb' +- MINIPUSH_POWER_TARGET_REQUEST = 'Please power the target now' - -- # override -- def initialize(qemu_cmd, binary_image_path) -- super(nil, binary_image_path) +- def initialize(qemu_cmd, payload_path) +- super(qemu_cmd, EXPECTED_PRINT) - -- @qemu_cmd = qemu_cmd +- @test_name = 'Boot test using Minipush' +- +- @payload_path = payload_path - end - - private - -- def quit_qemu_graceful -- Timeout.timeout(5) do -- pid = @target_serial.pid -- Process.kill('TERM', pid) -- Process.wait(pid) -- end -- end -- - # override -- def open_serial -- @target_serial = IO.popen(@qemu_cmd, 'r+', err: '/dev/null') +- def post_process_and_add_output(output) +- temp = output.join.split("\r\n") - -- # Ensure all output is immediately flushed to the device. -- @target_serial.sync = true +- # Should a line have solo carriage returns, remove any overridden parts of the string. +- temp.map! { |x| x.gsub(/.*\r/, '') } - -- puts "[#{@name_short}] ✅ Serial connected" +- @test_output += temp - end - -- # override -- def terminal -- result = @target_serial.expect(EXPECTED_PRINT, TIMEOUT_SECS) -- exit(1) if result.nil? -- -- puts result -- -- quit_qemu_graceful +- def wait_for_minipush_power_request(mp_out) +- output = [] +- Timeout.timeout(MAX_WAIT_SECS) do +- loop do +- output << mp_out.gets +- break if output.last.include?(MINIPUSH_POWER_TARGET_REQUEST) +- end +- end +- rescue Timeout::Error +- @test_error = 'Timed out waiting for power request' +- rescue StandardError => e +- @test_error = e.message +- ensure +- post_process_and_add_output(output) - end - - # override -- def connetion_reset; end +- def setup +- pty_main, pty_secondary = PTY.open +- mp_out, _mp_in = PTY.spawn("ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}") - -- # override -- def handle_reconnect(error) -- handle_unexpected(error) +- # Wait until MiniPush asks for powering the target. +- wait_for_minipush_power_request(mp_out) +- +- # Now is the time to start QEMU with the chainloader binary. QEMU's virtual tty is connected +- # to the MiniPush instance spawned above, so that the two processes talk to each other. +- Process.spawn(@qemu_cmd, in: pty_main, out: pty_main) +- +- # The remainder of the test is done by the parent class' run_concrete_test, which listens on +- # @qemu_serial. Hence, point it to MiniPush's output. +- @qemu_serial = mp_out - end -end - -##-------------------------------------------------------------------------------------------------- -## Execution starts here -##-------------------------------------------------------------------------------------------------- --puts --puts 'QEMUMiniPush 1.0'.cyan --puts -- --# CTRL + C handler. Only here to suppress Ruby's default exception print. --trap('INT') do -- # The `ensure` block from `QEMUMiniPush::run` will run after exit, restoring console state. -- exit --end -- --binary_image_path = ARGV.pop +-payload_path = ARGV.pop -qemu_cmd = ARGV.join(' ') - --QEMUMiniPush.new(qemu_cmd, binary_image_path).run +-ChainbootTest.new(qemu_cmd, payload_path).run diff -uNr 06_uart_chainloader/update.sh 07_timestamps/update.sh --- 06_uart_chainloader/update.sh diff --git a/07_timestamps/tests/boot_test_string.rb b/07_timestamps/tests/boot_test_string.rb new file mode 100644 index 00000000..02c3c492 --- /dev/null +++ b/07_timestamps/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Spinning for 1 second' diff --git a/08_hw_debug_JTAG/Makefile b/08_hw_debug_JTAG/Makefile index e3dcaae8..0a2443ac 100644 --- a/08_hw_debug_JTAG/Makefile +++ b/08_hw_debug_JTAG/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -42,11 +49,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -63,85 +77,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ - clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -150,10 +189,69 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/08_hw_debug_JTAG/README.md b/08_hw_debug_JTAG/README.md index a30a296e..74a942a8 100644 --- a/08_hw_debug_JTAG/README.md +++ b/08_hw_debug_JTAG/README.md @@ -320,7 +320,7 @@ diff -uNr 07_timestamps/Cargo.toml 08_hw_debug_JTAG/Cargo.toml diff -uNr 07_timestamps/Makefile 08_hw_debug_JTAG/Makefile --- 07_timestamps/Makefile +++ 08_hw_debug_JTAG/Makefile -@@ -23,6 +23,8 @@ +@@ -30,6 +30,8 @@ OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf @@ -329,7 +329,7 @@ diff -uNr 07_timestamps/Makefile 08_hw_debug_JTAG/Makefile LINKER_FILE = src/bsp/raspberrypi/link.ld RUSTC_MISC_ARGS = -C target-cpu=cortex-a53 else ifeq ($(BSP),rpi4) -@@ -34,6 +36,8 @@ +@@ -41,6 +43,8 @@ OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf @@ -338,56 +338,66 @@ diff -uNr 07_timestamps/Makefile 08_hw_debug_JTAG/Makefile LINKER_FILE = src/bsp/raspberrypi/link.ld RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -@@ -65,9 +69,12 @@ - DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial - DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t - DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -+DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot - DOCKER_ARG_DEV = --privileged -v /dev:/dev -+DOCKER_ARG_NET = --network host +@@ -84,17 +88,24 @@ + DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial + DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i + DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common ++DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot + DOCKER_ARG_DEV = --privileged -v /dev:/dev ++DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -+DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) + DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) ++DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) - # Dockerize commands that require USB device passthrough only on Linux -@@ -75,12 +82,17 @@ + # Dockerize commands, which require USB device passthrough, only on Linux. + ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) -+ DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) ++ DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) +else + DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif - EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) - EXEC_MINIPUSH = ruby ../utils/minipush.rb --.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check -+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ -+ clean readelf objdump nm check +@@ -193,6 +204,35 @@ - all: $(KERNEL_BIN) -@@ -107,6 +119,19 @@ - chainboot: $(KERNEL_BIN) - @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) ++##-------------------------------------------------------------------------------------------------- ++## Debugging targets ++##-------------------------------------------------------------------------------------------------- ++.PHONY: jtagboot openocd gdb gdb-opt0 ++ ++##------------------------------------------------------------------------------ ++## Push the JTAG boot image to the real HW target ++##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + ++##------------------------------------------------------------------------------ ++## Start OpenOCD session ++##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + ++##------------------------------------------------------------------------------ ++## Start GDB session ++##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + - clippy: - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) ++ ++ + ##-------------------------------------------------------------------------------------------------- + ## Testing targets + ##-------------------------------------------------------------------------------------------------- ``` diff --git a/08_hw_debug_JTAG/tests/boot_test_string.rb b/08_hw_debug_JTAG/tests/boot_test_string.rb new file mode 100644 index 00000000..02c3c492 --- /dev/null +++ b/08_hw_debug_JTAG/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Spinning for 1 second' diff --git a/09_privilege_level/Makefile b/09_privilege_level/Makefile index e3dcaae8..0a2443ac 100644 --- a/09_privilege_level/Makefile +++ b/09_privilege_level/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -42,11 +49,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -63,85 +77,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ - clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -150,10 +189,69 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/09_privilege_level/README.md b/09_privilege_level/README.md index 4b4c538d..645ec210 100644 --- a/09_privilege_level/README.md +++ b/09_privilege_level/README.md @@ -552,4 +552,13 @@ diff -uNr 08_hw_debug_JTAG/src/main.rs 09_privilege_level/src/main.rs } } +diff -uNr 08_hw_debug_JTAG/tests/boot_test_string.rb 09_privilege_level/tests/boot_test_string.rb +--- 08_hw_debug_JTAG/tests/boot_test_string.rb ++++ 09_privilege_level/tests/boot_test_string.rb +@@ -1,3 +1,3 @@ + # frozen_string_literal: true + +-EXPECTED_PRINT = 'Spinning for 1 second' ++EXPECTED_PRINT = 'Echoing input now' + ``` diff --git a/09_privilege_level/tests/boot_test_string.rb b/09_privilege_level/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/09_privilege_level/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/10_virtual_mem_part1_identity_mapping/Makefile b/10_virtual_mem_part1_identity_mapping/Makefile index e3dcaae8..0a2443ac 100644 --- a/10_virtual_mem_part1_identity_mapping/Makefile +++ b/10_virtual_mem_part1_identity_mapping/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -42,11 +49,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -63,85 +77,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ - clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -150,10 +189,69 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb b/10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/11_exceptions_part1_groundwork/Makefile b/11_exceptions_part1_groundwork/Makefile index e3dcaae8..0a2443ac 100644 --- a/11_exceptions_part1_groundwork/Makefile +++ b/11_exceptions_part1_groundwork/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -42,11 +49,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -63,85 +77,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ - clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -150,10 +189,69 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/11_exceptions_part1_groundwork/README.md b/11_exceptions_part1_groundwork/README.md index df8ecd6f..629f5f09 100644 --- a/11_exceptions_part1_groundwork/README.md +++ b/11_exceptions_part1_groundwork/README.md @@ -1016,4 +1016,13 @@ diff -uNr 10_virtual_mem_part1_identity_mapping/src/main.rs 11_exceptions_part1_ // Discard any spurious received characters before going into echo mode. +diff -uNr 10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb 11_exceptions_part1_groundwork/tests/boot_test_string.rb +--- 10_virtual_mem_part1_identity_mapping/tests/boot_test_string.rb ++++ 11_exceptions_part1_groundwork/tests/boot_test_string.rb +@@ -1,3 +1,3 @@ + # frozen_string_literal: true + +-EXPECTED_PRINT = 'Echoing input now' ++EXPECTED_PRINT = 'lr : 0x' + ``` diff --git a/11_exceptions_part1_groundwork/tests/boot_test_string.rb b/11_exceptions_part1_groundwork/tests/boot_test_string.rb new file mode 100644 index 00000000..200cd971 --- /dev/null +++ b/11_exceptions_part1_groundwork/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'lr : 0x' diff --git a/12_integrated_testing/Makefile b/12_integrated_testing/Makefile index 4c2fb069..08004986 100644 --- a/12_integrated_testing/Makefile +++ b/12_integrated_testing/Makefile @@ -2,18 +2,32 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) +# Optional integration test name. +ifdef TEST + TEST_ARG = --test $(TEST) +else + TEST_ARG = --test '*' +endif + + -# BSP-specific arguments +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -44,20 +58,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -# Testing-specific arguments -ifdef TEST - ifeq ($(TEST),unit) - TEST_ARG = --lib - else - TEST_ARG = --test $(TEST) - endif -endif +KERNEL_ELF = target/$(TARGET)/release/kernel + -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -75,105 +87,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ - clippy clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -define KERNEL_TEST_RUNNER - #!/usr/bin/env bash - - TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') - TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') - - $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY -endef - -export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - $(call colorecho, "\nCompiling test(s) - $(BSP)") - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -182,10 +199,108 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot test_unit test_integration + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + + TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + +##------------------------------------------------------------------------------ +## Run integration test(s) +##------------------------------------------------------------------------------ +test_integration: + $(call colorecho, "\nCompiling integration test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +test: test_boot test_unit test_integration + +endif diff --git a/12_integrated_testing/README.md b/12_integrated_testing/README.md index bb1677e9..c25e3047 100644 --- a/12_integrated_testing/README.md +++ b/12_integrated_testing/README.md @@ -2,12 +2,14 @@ ## tl;dr -- We implement our own test framework using `Rust`'s [custom_test_frameworks] feature by enabling - `Unit Tests` and `Integration Tests` using `QEMU`. -- It is also possible to have test automation for the kernel's `console` (provided over `UART` in - our case): Sending strings/characters to the console and expecting specific answers in return. +- We implement our own integrated test framework using `Rust`'s [custom_test_frameworks] feature by + enabling `Unit Tests` and `Integration Tests` using `QEMU`. +- It is also possible to have test automation for I/O with the kernel's `console` (provided over + `UART` in our case). That is, sending strings/characters to the console and expecting specific + answers in return. +- The already existing basic `boot test` remains unchanged. - + ## Table of Contents @@ -40,13 +42,13 @@ functionality. For example: - Stalling execution during boot to test the kernel's timekeeping code by spinning for 1 second. - Willingly causing exceptions to see the exception handler running. -The feature set of the kernel is now rich enough so that it makes sense to introduce proper testing -modeled after Rust's [native testing framework]. This tutorial extends our kernel with three basic -testing facilities: +The feature set of the kernel is now rich enough so that it makes sense to introduce proper +integrated testing modeled after Rust's [native testing framework]. This tutorial extends our single +existing kernel test with three new testing facilities: - Classic `Unit Tests`. - [Integration Tests] (self-contained tests stored in the `$CRATE/tests/` directory). - - `Console Tests`. These are integration tests acting on external stimuli - aka `console` input. - Sending strings/characters to the console and expecting specific answers in return. + - `Console I/O Tests`. These are integration tests acting on external stimuli - aka `console` + input. Sending strings/characters to the console and expecting specific answers in return. [native testing framework]: https://doc.rust-lang.org/book/ch11-00-testing.html @@ -64,7 +66,7 @@ dependencies on the standard library, but comes at the cost of having a reduced of annotating functions with `#[test]`, the `#[test_case]` attribute must be used. Additionally, we need to write a `test_runner` function, which is supposed to execute all the functions annotated with `#[test_case]`. This is barely enough to get `Unit Tests` running, though. There will be some -more challenges that need solving for getting `Integration Tests` running as well. +more challenges that need be solved for getting `Integration Tests` running as well. Please note that for automation purposes, all testing will be done in `QEMU` and not on real hardware. @@ -82,15 +84,23 @@ additional insights. ## Implementation -We introduce a new `Makefile` target: +We introduce two new `Makefile` targets: ```console -$ make test +$ make test_unit +$ make test_integration ``` -In essence, `make test` will execute `cargo test` instead of `cargo rustc`. The details will be -explained in due course. The rest of the tutorial will explain as chronologically as possible what -happens when `make test` aka `cargo test` runs. +In essence, the `make test_*` targets will execute `cargo test` instead of `cargo rustc`. The +details will be explained in due course. The rest of the tutorial will explain as chronologically as +possible what happens when `make test_*` aka `cargo test` runs. + +Please note that the new targets are added to the existing `make test` target, so this is now your +one-stop target to execute all possible tests for the kernel: + +```Makefile +test: test_boot test_unit test_integration +``` ### Test Organization @@ -166,8 +176,9 @@ that we are supposed to provide. This is the one that will be called by the `car ```rust /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); @@ -255,7 +266,7 @@ opportunity to cut down on setup code. [tutorial 03]: ../03_hacky_hello_world As a matter of fact, for the `Raspberrys`, nothing needs to be done, so the function is empy. But -this might be different for other hardware emulated by QEMU, so it makes sense to introduce the +this might be different for other hardware emulated by `QEMU`, so it makes sense to introduce the function now to make it easier in case new `BSPs` are added to the kernel in the future. Next, the reexported `test_main()` is called, which will call our `test_runner()` which finally @@ -265,9 +276,10 @@ prints the unit test names and executes them. Let's recap where we are right now: -We've enabled `custom_test_frameworks` in `lib.rs` to a point where, when using `make test`, the -code gets compiled to a test kernel binary that eventually executes all the (yet-to-be-defined) -`UnitTest` instances by executing all the way from `_start()` to our `test_runner()` function. +We've enabled `custom_test_frameworks` in `lib.rs` to a point where, when using a `make test_unit` +target, the code gets compiled to a test kernel binary that eventually executes all the +(yet-to-be-defined) `UnitTest` instances by executing all the way from `_start()` to our +`test_runner()` function. Through mechanisms that are explained later, `cargo` will now instantiate a `QEMU` process that exectues this test kernel. The question now is: How is test success/failure communicated to `cargo`? @@ -339,30 +351,30 @@ concludes: #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } } ``` -In case none of the unit tests panicked, `lib.rs`'s `kernel_init()` calls `cpu::qemu_exit_success()` -to successfully conclude the unit test run. +In case _none_ of the unit tests panicked, `lib.rs`'s `kernel_init()` calls +`cpu::qemu_exit_success()` to successfully conclude the unit test run. ### Controlling Test Kernel Execution Now is a good time to catch up on how the test kernel binary is actually being executed. Normally, `cargo test` would try to execute the compiled binary as a normal child process. This would fail -horribly because we build a kernel, and not a userspace process. Also, chances are very high that -you sit in front of an `x86` machine, whereas the RPi kernel is `AArch64`. +horribly because we build a kernel, and not a userspace process. Also, chances are high that you sit +in front of an `x86` machine, whereas the RPi kernel is `AArch64`. Therefore, we need to install some hooks that make sure the test kernel gets executed inside `QEMU`, -quite like it is done for the existing `make qemu` target that is in place since tutorial 1. The +quite like it is done for the existing `make qemu` target that is in place since `tutorial 1`. The first step is to add a new file to the project, `.cargo/config.toml`: ```toml @@ -374,10 +386,13 @@ Instead of executing a compilation result directly, the `runner` flag will instr delegate the execution. Using the setting depicted above, `target/kernel_test_runner.sh` will be executed and given the full path to the compiled test kernel as the first command line argument. -The file `kernel_test_runner.sh` does not exist by default. We generate it on demand throguh the -`make test` target: +The file `kernel_test_runner.sh` does not exist by default. We generate it on demand when one of the +`make test_*` targets is called: ```Makefile +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ define KERNEL_TEST_RUNNER #!/usr/bin/env bash @@ -385,16 +400,26 @@ define KERNEL_TEST_RUNNER TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY endef export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib ``` It first does the standard `objcopy` step to strip the `ELF` down to a raw binary. Just like in all @@ -403,44 +428,76 @@ provided to it by `cargo`, and finally compiles a `docker` command to execute th reference, here it is fully resolved for an `RPi3 BSP`: ```bash -docker run -i --rm -v /opt/rust-raspberrypi-OS-tutorials/12_integrated_testing:/work/tutorial -w /work/tutorial rustembedded/osdev-utils ruby tests/runner.rb qemu-system-aarch64 -M raspi3 -serial stdio -display none -semihosting -kernel $TEST_BINARY +docker run --rm -v /opt/rust-raspberrypi-OS-tutorials/12_integrated_testing:/work/tutorial -w /work/tutorial -v /opt/rust-raspberrypi-OS-tutorials/12_integrated_testing/../common:/work/common rustembedded/osdev-utils ruby ../common/tests/dispatch.rb qemu-system-aarch64 -M raspi3 -serial stdio -display none -semihosting -kernel $TEST_BINARY ``` -We're still not done with all the redirections. Spotted the `ruby tests/runner.rb` part that gets -excuted inside Docker? +This command is quite similar to the one used in the `make test_boot` target that we have since +`tutorial 3`. However, we never bothered explaining it, so lets take a closer look this time. One of +the key ingredients is that we execute this script: `ruby ../common/tests/dispatch.rb`. #### Wrapping QEMU Test Execution -`runner.rb` is a [Ruby] wrapper script around `QEMU` that, for unit tests, catches the case that a -test gets stuck, e.g. in an unintentional busy loop or a crash. If `runner.rb` does not observe any -output of the test kernel for `5 seconds`, it cancels the execution and reports a failure back to -`cargo`. If `QEMU` exited itself by means of `aarch64::exit_success() / aarch64::exit_failure()`, -the respective exit status code is passed through. The essential part happens here in `class -RawTest`: +`dispatch.rb` is a [Ruby] script which first determines what kind of test is due by inspecting the +`QEMU`-command that was given to it. In case of `unit tests`, we are only interested if they all +executed successfully, which can be checked by inspecting `QEMU`'s exit code. So the script takes +the provided qemu command it got from `ARGV`, and creates and runs an instance of `ExitCodeTest`: ```ruby -def exec - error = 'Timed out waiting for test' +require_relative 'boot_test' +require_relative 'console_io_test' +require_relative 'exit_code_test' + +qemu_cmd = ARGV.join(' ') +binary = ARGV.last +test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] + +case test_name +when 'kernel8.img' + load 'tests/boot_test_string.rb' # provides 'EXPECTED_PRINT' + BootTest.new(qemu_cmd, EXPECTED_PRINT).run # Doesn't return + +when 'libkernel' + ExitCodeTest.new(qemu_cmd, 'Kernel library unit tests').run # Doesn't return +``` + +The easy case is `QEMU` existing by itself by means of `aarch64::exit_success()` or +`aarch64::exit_failure()`. But the script can also catch the case of a test that gets stuck, e.g. in +an unintentional busy loop or a crash. If `ExitCodeTest` does not observe any output of the test +kernel for `MAX_WAIT_SECS`, it cancels the execution and marks the test as failed. Test success or +failure is finally reported back to `cargo`. + +Here is the essential part happening in `class ExitCodeTest` (If `QEMU` exits itself, an `EOFError` +is thrown): + +```ruby +def run_concrete_test io = IO.popen(@qemu_cmd) - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end + Timeout.timeout(MAX_WAIT_SECS) do + @test_output << io.read_nonblock(1024) while IO.select([io]) end +rescue EOFError + io.close + @test_error = $CHILD_STATUS.to_i.zero? ? false : 'QEMU exit status != 0' +rescue Timeout::Error + @test_error = 'Timed out waiting for test' +rescue StandardError => e + @test_error = e.message +ensure + post_process_output +end ``` +Please note that `dispatch.rb` and all its dependencies live in the shared folder +`../common/tests/`. + [Ruby]: https://www.ruby-lang.org/ ### Writing Unit Tests -Alright, that's a wrap for the whole chain from `make test` all the way to reporting the test exit -status back to `cargo test`. It is a lot to digest already, but we haven't even learned to write -`Unit Tests` yet. +Alright, that's a wrap for the whole chain from `make test_unit` all the way to reporting the test +exit status back to `cargo test`. It is a lot to digest already, but we haven't even learned to +write `Unit Tests` yet. In essence, it is almost like in `std` environments, with the difference that `#[test]` can't be used, because it is part of the standard library. The `no_std` replacement attribute provided by @@ -485,9 +542,9 @@ Since this is a bit boiler-platy with the const and name definition, let's write macro] named `#[kernel_test]` to simplify this. It should work this way: 1. Must be put before functions that take no arguments and return nothing. - 2. Automatically constructs a `const UnitTest` from attributed functions like shown above by: + 1. Automatically constructs a `const UnitTest` from attributed functions like shown above by: 1. Converting the function name to the `name` member of the `UnitTest` struct. - 2. Populating the `test_func` member with a closure that executes the body of the attributed + 1. Populating the `test_func` member with a closure that executes the body of the attributed function. For the sake of brevity, we're not going to discuss the macro implementation. [The source is in the @@ -609,12 +666,12 @@ function? This marks the function in `lib.rs` as a [weak symbol]. Let's look at #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } @@ -624,7 +681,7 @@ fn _panic_exit() -> ! { [weak symbol]: https://en.wikipedia.org/wiki/Weak_symbol This enables integration tests in `$CRATE/tests/` to override this function according to their -needs. This is useful because depending on the kind of test, a `panic!` could mean success or +needs. This is useful, because depending on the kind of test, a `panic!` could mean success or failure. For example, `tests/02_exception_sync_page_fault.rs` is intentionally causing a page fault, so the wanted outcome is a `panic!`. Here is the whole test (minus some inline comments): @@ -646,10 +703,10 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); - if let Err(string) = memory::mmu::mmu().init() { + if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() { println!("MMU: {}", string); cpu::qemu_exit_failure() } @@ -661,7 +718,6 @@ unsafe fn kernel_init() -> ! { // If execution reaches here, the memory access above did not cause a page fault exception. cpu::qemu_exit_failure() } - ``` The `_panic_exit()` version that makes `QEMU` return `0` (indicating test success) is pulled in by @@ -671,22 +727,21 @@ The `_panic_exit()` version that makes `QEMU` return `0` (indicating test succes As the kernel or OS grows, it will be more and more interesting to test user/kernel interaction through the serial console. That is, sending strings/characters to the console and expecting -specific answers in return. The `runner.rb` wrapper script provides infrastructure to do this with -little overhead. It basically works like this: +specific answers in return. The `dispatch.rb` wrapper script provides infrastructure to recognize +and dispatch console I/O tests with little overhead. It basically works like this: 1. For each integration test, check if a companion file to the `.rs` test file exists. - A companion file has the same name, but ends in `.rb`. - - The companion file contains one or more console subtests. - 2. If it exists, load the file to dynamically import the console subtests. - 3. Spawn `QEMU` and attach to the serial console. - 4. Run the console subtests. + - The companion file contains one or more console I/O subtests. + 1. If it exists, load the file to dynamically import the console subtests. + 1. Create a `ConsoleIOTest` instance and run it. + - This first spawns `QEMU` and attaches to `QEMU`'s serial console emulation. + - Then it runs all console subtests on it. Here is an excerpt from `00_console_sanity.rb` showing a subtest that does a handshake with the kernel over the console: ```ruby -TIMEOUT_SECS = 3 - # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -695,7 +750,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end ``` @@ -727,20 +782,22 @@ unsafe fn kernel_init() -> ! { ## Test it -Believe it or not, that is all. There are three ways you can run tests: +Believe it or not, that is all. There are four ways you can run tests now: - 1. `make test` will run all tests back-to-back. - 2. `TEST=unit make test` will run `libkernel`'s unit tests. - 3. `TEST=TEST_NAME make test` will run a specficic integration test. - - For example, `TEST=01_timer_sanity make test` + 1. `make test` will run all tests back-to-back. That is, the ever existing `boot test` first, then + `unit tests`, then `integration tests`. + 1. `make test_unit` will run `libkernel`'s unit tests. + 1. `make test_integration` will run all integration tests back-to-back. + 1. `TEST=TEST_NAME make test_integration` will run a specficic integration test. + - For example, `TEST=01_timer_sanity make test_integration` ```console $ make test [...] - Running unittests (target/aarch64-unknown-none-softfloat/release/deps/libkernel-836110ac5dd535ba) + Running unittests (target/aarch64-unknown-none-softfloat/release/deps/libkernel-142a8d94bc9c615a) ------------------------------------------------------------------- - 🦀 Running 8 tests + 🦀 Running 6 tests ------------------------------------------------------------------- 1. virt_mem_layout_sections_are_64KiB_aligned................[ok] @@ -749,17 +806,18 @@ $ make test 4. kernel_tables_in_bss......................................[ok] 5. size_of_tabledescriptor_equals_64_bit.....................[ok] 6. size_of_pagedescriptor_equals_64_bit......................[ok] - 7. zero_volatile_works.......................................[ok] - 8. bss_section_is_sane.......................................[ok] ------------------------------------------------------------------- - ✅ Success: libkernel + ✅ Success: Kernel library unit tests ------------------------------------------------------------------- - Running tests/00_console_sanity.rs (target/aarch64-unknown-none-softfloat/release/deps/00_console_sanity-78c12c5472d40df7) + +Compiling integration test(s) - rpi3 + Finished release [optimized] target(s) in 0.00s + Running tests/00_console_sanity.rs (target/aarch64-unknown-none-softfloat/release/deps/00_console_sanity-c06130838f14dbff) ------------------------------------------------------------------- - 🦀 Running 3 console-based tests + 🦀 Running 3 console I/O tests ------------------------------------------------------------------- 1. Transmit and Receive handshake............................[ok] @@ -767,11 +825,11 @@ $ make test 3. Receive statistics........................................[ok] ------------------------------------------------------------------- - ✅ Success: 00_console_sanity + ✅ Success: 00_console_sanity.rs ------------------------------------------------------------------- - Running tests/01_timer_sanity.rs (target/aarch64-unknown-none-softfloat/release/deps/01_timer_sanity-4866734b14c83c9b) + Running tests/01_timer_sanity.rs (target/aarch64-unknown-none-softfloat/release/deps/01_timer_sanity-62a954d22239d1a3) ------------------------------------------------------------------- 🦀 Running 3 tests ------------------------------------------------------------------- @@ -781,11 +839,11 @@ $ make test 3. spin_accuracy_check_1_second..............................[ok] ------------------------------------------------------------------- - ✅ Success: 01_timer_sanity + ✅ Success: 01_timer_sanity.rs ------------------------------------------------------------------- - Running tests/02_exception_sync_page_fault.rs (target/aarch64-unknown-none-softfloat/release/deps/02_exception_sync_page_fault-f2d0885cada1105b) + Running tests/02_exception_sync_page_fault.rs (target/aarch64-unknown-none-softfloat/release/deps/02_exception_sync_page_fault-2d8ec603ef1c4d8e) ------------------------------------------------------------------- 🦀 Testing synchronous exception handling by causing a page fault ------------------------------------------------------------------- @@ -800,7 +858,7 @@ $ make test [...] ------------------------------------------------------------------- - ✅ Success: 02_exception_sync_page_fault + ✅ Success: 02_exception_sync_page_fault.rs ------------------------------------------------------------------- ``` @@ -880,7 +938,21 @@ diff -uNr 11_exceptions_part1_groundwork/Cargo.toml 12_integrated_testing/Cargo. diff -uNr 11_exceptions_part1_groundwork/Makefile 12_integrated_testing/Makefile --- 11_exceptions_part1_groundwork/Makefile +++ 12_integrated_testing/Makefile -@@ -20,6 +20,7 @@ +@@ -14,6 +14,13 @@ + # Default to a serial device name that is common in Linux. + DEV_SERIAL ?= /dev/ttyUSB0 + ++# Optional integration test name. ++ifdef TEST ++ TEST_ARG = --test $(TEST) ++else ++ TEST_ARG = --test '*' ++endif ++ + + + ##-------------------------------------------------------------------------------------------------- +@@ -27,6 +34,7 @@ QEMU_BINARY = qemu-system-aarch64 QEMU_MACHINE_TYPE = raspi3 QEMU_RELEASE_ARGS = -serial stdio -display none @@ -888,7 +960,7 @@ diff -uNr 11_exceptions_part1_groundwork/Makefile 12_integrated_testing/Makefile OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf -@@ -33,6 +34,7 @@ +@@ -40,6 +48,7 @@ QEMU_BINARY = qemu-system-aarch64 QEMU_MACHINE_TYPE = QEMU_RELEASE_ARGS = -serial stdio -display none @@ -896,23 +968,7 @@ diff -uNr 11_exceptions_part1_groundwork/Makefile 12_integrated_testing/Makefile OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm READELF_BINARY = aarch64-none-elf-readelf -@@ -45,6 +47,15 @@ - # Export for build.rs - export LINKER_FILE - -+# Testing-specific arguments -+ifdef TEST -+ ifeq ($(TEST),unit) -+ TEST_ARG = --lib -+ else -+ TEST_ARG = --test $(TEST) -+ endif -+endif -+ - QEMU_MISSING_STRING = "This board is not yet supported for QEMU." - - RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) -@@ -59,6 +70,7 @@ +@@ -73,6 +82,7 @@ DOC_CMD = cargo doc $(COMPILER_ARGS) CLIPPY_CMD = cargo clippy $(COMPILER_ARGS) CHECK_CMD = cargo check $(COMPILER_ARGS) @@ -920,37 +976,28 @@ diff -uNr 11_exceptions_part1_groundwork/Makefile 12_integrated_testing/Makefile OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -@@ -75,6 +87,7 @@ - - DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) - DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -+DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) - DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) - - # Dockerize commands that require USB device passthrough only on Linux -@@ -91,8 +104,8 @@ - EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) - EXEC_MINIPUSH = ruby ../utils/minipush.rb +@@ -236,11 +246,11 @@ + ##-------------------------------------------------------------------------------------------------- + ## Testing targets + ##-------------------------------------------------------------------------------------------------- +-.PHONY: test test_boot ++.PHONY: test test_boot test_unit test_integration --.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot jtagboot openocd gdb gdb-opt0 clippy \ -- clean readelf objdump nm check -+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ -+ clippy clean readelf objdump nm check + ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. - all: $(KERNEL_BIN) +-test_boot test : ++test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") -@@ -108,12 +121,31 @@ - @$(DOC_CMD) --document-private-items --open + else # QEMU is supported. +@@ -252,6 +262,45 @@ + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) - ifeq ($(QEMU_MACHINE_TYPE),) --qemu: -+qemu test: - $(call colorecho, "\n$(QEMU_MISSING_STRING)") - else - qemu: $(KERNEL_BIN) - $(call colorecho, "\nLaunching QEMU") - @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -+ +-test: test_boot ++##------------------------------------------------------------------------------ ++## Helpers for unit and integration test targets ++##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + @@ -958,20 +1005,38 @@ diff -uNr 11_exceptions_part1_groundwork/Makefile 12_integrated_testing/Makefile + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY -+ $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY ++ $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER -+test: FEATURES += --features test_build -+test: -+ $(call colorecho, "\nCompiling test(s) - $(BSP)") -+ @mkdir -p target -+ @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh -+ @chmod +x target/kernel_test_runner.sh ++ ++define test_prepare ++ @mkdir -p target ++ @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh ++ @chmod +x target/kernel_test_runner.sh ++endef ++ ++test_unit test_integration: FEATURES += --features test_build ++ ++##------------------------------------------------------------------------------ ++## Run unit test(s) ++##------------------------------------------------------------------------------ ++test_unit: ++ $(call colorecho, "\nCompiling unit test(s) - $(BSP)") ++ $(call test_prepare) ++ RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib ++ ++##------------------------------------------------------------------------------ ++## Run integration test(s) ++##------------------------------------------------------------------------------ ++test_integration: ++ $(call colorecho, "\nCompiling integration test(s) - $(BSP)") ++ $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) - endif ++ ++test: test_boot test_unit test_integration - chainboot: $(KERNEL_BIN) + endif diff -uNr 11_exceptions_part1_groundwork/src/_arch/aarch64/cpu.rs 12_integrated_testing/src/_arch/aarch64/cpu.rs --- 11_exceptions_part1_groundwork/src/_arch/aarch64/cpu.rs @@ -1216,7 +1281,7 @@ diff -uNr 11_exceptions_part1_groundwork/src/exception.rs 12_integrated_testing/ diff -uNr 11_exceptions_part1_groundwork/src/lib.rs 12_integrated_testing/src/lib.rs --- 11_exceptions_part1_groundwork/src/lib.rs +++ 12_integrated_testing/src/lib.rs -@@ -0,0 +1,186 @@ +@@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright (c) 2018-2021 Andre Richter @@ -1379,8 +1444,9 @@ diff -uNr 11_exceptions_part1_groundwork/src/lib.rs 12_integrated_testing/src/li + +/// The default runner for unit tests. +pub fn test_runner(tests: &[&test_types::UnitTest]) { ++ // This line will be printed as the test header. + println!("Running {} tests", tests.len()); -+ println!("-------------------------------------------------------------------\n"); ++ + for (i, test) in tests.iter().enumerate() { + print!("{:>3}. {:.<58}", i + 1, test.name); + @@ -1630,12 +1696,12 @@ diff -uNr 11_exceptions_part1_groundwork/src/panic_wait.rs 12_integrated_testing +#[linkage = "weak"] +#[no_mangle] +fn _panic_exit() -> ! { -+ #[cfg(not(test_build))] ++ #[cfg(not(feature = "test_build"))] + { + cpu::wait_forever() + } + -+ #[cfg(test_build)] ++ #[cfg(feature = "test_build")] + { + cpu::qemu_exit_failure() + } @@ -1708,7 +1774,7 @@ diff -uNr 11_exceptions_part1_groundwork/test-macros/src/lib.rs 12_integrated_te diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrated_testing/tests/00_console_sanity.rb --- 11_exceptions_part1_groundwork/tests/00_console_sanity.rb +++ 12_integrated_testing/tests/00_console_sanity.rb -@@ -0,0 +1,50 @@ +@@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 @@ -1719,6 +1785,13 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrate + +TIMEOUT_SECS = 3 + ++# Error class for when expect times out. ++class ExpectTimeoutError < StandardError ++ def initialize ++ super('Timeout while expecting string') ++ end ++end ++ +# Verify sending and receiving works as expected. +class TxRxHandshake + def name @@ -1727,7 +1800,7 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrate + + def run(qemu_out, qemu_in) + qemu_in.write_nonblock('ABC') -+ raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? ++ raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + end +end + @@ -1738,7 +1811,7 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrate + end + + def run(qemu_out, _qemu_in) -+ raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? ++ raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? + end +end + @@ -1749,7 +1822,7 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrate + end + + def run(qemu_out, _qemu_in) -+ raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? ++ raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? + end +end + @@ -1763,7 +1836,7 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rb 12_integrate diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rs 12_integrated_testing/tests/00_console_sanity.rs --- 11_exceptions_part1_groundwork/tests/00_console_sanity.rs +++ 12_integrated_testing/tests/00_console_sanity.rs -@@ -0,0 +1,42 @@ +@@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright (c) 2019-2021 Andre Richter @@ -1797,14 +1870,7 @@ diff -uNr 11_exceptions_part1_groundwork/tests/00_console_sanity.rs 12_integrate + print!("{}", console().chars_read()); + + // The QEMU process running this test will be closed by the I/O test harness. -+ // cpu::wait_forever(); -+ -+ // For some reason, in this test, rustc or the linker produces an empty binary when -+ // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time -+ // being, the following lines are just a workaround to fix this compiler/linker weirdness. -+ use libkernel::time::interface::TimeManager; -+ libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); -+ cpu::qemu_exit_success() ++ cpu::wait_forever(); +} diff -uNr 11_exceptions_part1_groundwork/tests/01_timer_sanity.rs 12_integrated_testing/tests/01_timer_sanity.rs @@ -1893,8 +1959,8 @@ diff -uNr 11_exceptions_part1_groundwork/tests/02_exception_sync_page_fault.rs 1 + exception::handling_init(); + bsp::console::qemu_bring_up_console(); + ++ // This line will be printed as the test header. + println!("Testing synchronous exception handling by causing a page fault"); -+ println!("-------------------------------------------------------------------\n"); + + if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() { + println!("MMU: {}", string); @@ -1909,6 +1975,15 @@ diff -uNr 11_exceptions_part1_groundwork/tests/02_exception_sync_page_fault.rs 1 + cpu::qemu_exit_failure() +} +diff -uNr 11_exceptions_part1_groundwork/tests/boot_test_string.rb 12_integrated_testing/tests/boot_test_string.rb +--- 11_exceptions_part1_groundwork/tests/boot_test_string.rb ++++ 12_integrated_testing/tests/boot_test_string.rb +@@ -1,3 +1,3 @@ + # frozen_string_literal: true + +-EXPECTED_PRINT = 'lr : 0x' ++EXPECTED_PRINT = 'Echoing input now' + diff -uNr 11_exceptions_part1_groundwork/tests/panic_exit_success/mod.rs 12_integrated_testing/tests/panic_exit_success/mod.rs --- 11_exceptions_part1_groundwork/tests/panic_exit_success/mod.rs +++ 12_integrated_testing/tests/panic_exit_success/mod.rs @@ -1923,154 +1998,6 @@ diff -uNr 11_exceptions_part1_groundwork/tests/panic_exit_success/mod.rs 12_inte + libkernel::cpu::qemu_exit_success() +} -diff -uNr 11_exceptions_part1_groundwork/tests/runner.rb 12_integrated_testing/tests/runner.rb ---- 11_exceptions_part1_groundwork/tests/runner.rb -+++ 12_integrated_testing/tests/runner.rb -@@ -0,0 +1,143 @@ -+#!/usr/bin/env ruby -+# frozen_string_literal: true -+ -+# SPDX-License-Identifier: MIT OR Apache-2.0 -+# -+# Copyright (c) 2019-2021 Andre Richter -+ -+require 'English' -+require 'pty' -+ -+# Test base class. -+class Test -+ INDENT = ' ' -+ -+ def print_border(status) -+ puts -+ puts "#{INDENT}-------------------------------------------------------------------" -+ puts status -+ puts "#{INDENT}-------------------------------------------------------------------\n\n\n" -+ end -+ -+ def print_error(error) -+ puts -+ print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") -+ end -+ -+ def print_success -+ print_border("#{INDENT}✅ Success: #{@test_name}") -+ end -+ -+ def print_output -+ puts "#{INDENT}-------------------------------------------------------------------" -+ print INDENT -+ print '🦀 ' -+ print @output.join.gsub("\n", "\n#{INDENT}") -+ end -+ -+ def finish(error) -+ print_output -+ -+ exit_code = if error -+ print_error(error) -+ false -+ else -+ print_success -+ true -+ end -+ -+ exit(exit_code) -+ end -+end -+ -+# Executes tests with console I/O. -+class ConsoleTest < Test -+ def initialize(binary, qemu_cmd, test_name, console_subtests) -+ super() -+ -+ @binary = binary -+ @qemu_cmd = qemu_cmd -+ @test_name = test_name -+ @console_subtests = console_subtests -+ @cur_subtest = 1 -+ @output = ["Running #{@console_subtests.length} console-based tests\n", -+ "-------------------------------------------------------------------\n\n"] -+ end -+ -+ def format_test_name(number, name) -+ formatted_name = "#{number.to_s.rjust(3)}. #{name}" -+ formatted_name.ljust(63, '.') -+ end -+ -+ def run_subtest(subtest, qemu_out, qemu_in) -+ @output << format_test_name(@cur_subtest, subtest.name) -+ -+ subtest.run(qemu_out, qemu_in) -+ -+ @output << "[ok]\n" -+ @cur_subtest += 1 -+ end -+ -+ def exec -+ error = false -+ -+ PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| -+ begin -+ @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } -+ rescue StandardError => e -+ error = e.message -+ end -+ -+ finish(error) -+ end -+ end -+end -+ -+# A wrapper around the bare QEMU invocation. -+class RawTest < Test -+ MAX_WAIT_SECS = 5 -+ -+ def initialize(binary, qemu_cmd, test_name) -+ super() -+ -+ @binary = binary -+ @qemu_cmd = qemu_cmd -+ @test_name = test_name -+ @output = [] -+ end -+ -+ def exec -+ error = 'Timed out waiting for test' -+ io = IO.popen(@qemu_cmd) -+ -+ while IO.select([io], nil, nil, MAX_WAIT_SECS) -+ begin -+ @output << io.read_nonblock(1024) -+ rescue EOFError -+ io.close -+ error = $CHILD_STATUS.to_i != 0 -+ break -+ end -+ end -+ -+ finish(error) -+ end -+end -+ -+##-------------------------------------------------------------------------------------------------- -+## Script entry point -+##-------------------------------------------------------------------------------------------------- -+binary = ARGV.last -+test_name = binary.gsub(modulor{.*deps/}, '').split('-')[0] -+console_test_file = "tests/#{test_name}.rb" -+qemu_cmd = ARGV.join(' ') -+ -+test_runner = if File.exist?(console_test_file) -+ load console_test_file -+ # subtest_collection is provided by console_test_file -+ ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) -+ else -+ RawTest.new(binary, qemu_cmd, test_name) -+ end -+ -+test_runner.exec - diff -uNr 11_exceptions_part1_groundwork/test-types/Cargo.toml 12_integrated_testing/test-types/Cargo.toml --- 11_exceptions_part1_groundwork/test-types/Cargo.toml +++ 12_integrated_testing/test-types/Cargo.toml diff --git a/12_integrated_testing/src/lib.rs b/12_integrated_testing/src/lib.rs index 9890351f..b8370a54 100644 --- a/12_integrated_testing/src/lib.rs +++ b/12_integrated_testing/src/lib.rs @@ -160,8 +160,9 @@ extern "Rust" { /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); diff --git a/12_integrated_testing/src/panic_wait.rs b/12_integrated_testing/src/panic_wait.rs index 20493a91..d272a197 100644 --- a/12_integrated_testing/src/panic_wait.rs +++ b/12_integrated_testing/src/panic_wait.rs @@ -23,12 +23,12 @@ fn _panic_print(args: fmt::Arguments) { #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } diff --git a/12_integrated_testing/tests/00_console_sanity.rb b/12_integrated_testing/tests/00_console_sanity.rb index dfd6b16e..16fb6c79 100644 --- a/12_integrated_testing/tests/00_console_sanity.rb +++ b/12_integrated_testing/tests/00_console_sanity.rb @@ -8,6 +8,13 @@ require 'expect' TIMEOUT_SECS = 3 +# Error class for when expect times out. +class ExpectTimeoutError < StandardError + def initialize + super('Timeout while expecting string') + end +end + # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -16,7 +23,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end @@ -27,7 +34,7 @@ class TxStatistics end def run(qemu_out, _qemu_in) - raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? end end @@ -38,7 +45,7 @@ class RxStatistics end def run(qemu_out, _qemu_in) - raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? end end diff --git a/12_integrated_testing/tests/00_console_sanity.rs b/12_integrated_testing/tests/00_console_sanity.rs index 84b74479..03058f5e 100644 --- a/12_integrated_testing/tests/00_console_sanity.rs +++ b/12_integrated_testing/tests/00_console_sanity.rs @@ -31,12 +31,5 @@ unsafe fn kernel_init() -> ! { print!("{}", console().chars_read()); // The QEMU process running this test will be closed by the I/O test harness. - // cpu::wait_forever(); - - // For some reason, in this test, rustc or the linker produces an empty binary when - // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time - // being, the following lines are just a workaround to fix this compiler/linker weirdness. - use libkernel::time::interface::TimeManager; - libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); - cpu::qemu_exit_success() + cpu::wait_forever(); } diff --git a/12_integrated_testing/tests/02_exception_sync_page_fault.rs b/12_integrated_testing/tests/02_exception_sync_page_fault.rs index f1535d34..8febacd1 100644 --- a/12_integrated_testing/tests/02_exception_sync_page_fault.rs +++ b/12_integrated_testing/tests/02_exception_sync_page_fault.rs @@ -26,8 +26,8 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() { println!("MMU: {}", string); diff --git a/12_integrated_testing/tests/boot_test_string.rb b/12_integrated_testing/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/12_integrated_testing/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/12_integrated_testing/tests/runner.rb b/12_integrated_testing/tests/runner.rb deleted file mode 100755 index 53116e08..00000000 --- a/12_integrated_testing/tests/runner.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2019-2021 Andre Richter - -require 'English' -require 'pty' - -# Test base class. -class Test - INDENT = ' ' - - def print_border(status) - puts - puts "#{INDENT}-------------------------------------------------------------------" - puts status - puts "#{INDENT}-------------------------------------------------------------------\n\n\n" - end - - def print_error(error) - puts - print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") - end - - def print_success - print_border("#{INDENT}✅ Success: #{@test_name}") - end - - def print_output - puts "#{INDENT}-------------------------------------------------------------------" - print INDENT - print '🦀 ' - print @output.join.gsub("\n", "\n#{INDENT}") - end - - def finish(error) - print_output - - exit_code = if error - print_error(error) - false - else - print_success - true - end - - exit(exit_code) - end -end - -# Executes tests with console I/O. -class ConsoleTest < Test - def initialize(binary, qemu_cmd, test_name, console_subtests) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @console_subtests = console_subtests - @cur_subtest = 1 - @output = ["Running #{@console_subtests.length} console-based tests\n", - "-------------------------------------------------------------------\n\n"] - end - - def format_test_name(number, name) - formatted_name = "#{number.to_s.rjust(3)}. #{name}" - formatted_name.ljust(63, '.') - end - - def run_subtest(subtest, qemu_out, qemu_in) - @output << format_test_name(@cur_subtest, subtest.name) - - subtest.run(qemu_out, qemu_in) - - @output << "[ok]\n" - @cur_subtest += 1 - end - - def exec - error = false - - PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| - begin - @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } - rescue StandardError => e - error = e.message - end - - finish(error) - end - end -end - -# A wrapper around the bare QEMU invocation. -class RawTest < Test - MAX_WAIT_SECS = 5 - - def initialize(binary, qemu_cmd, test_name) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @output = [] - end - - def exec - error = 'Timed out waiting for test' - io = IO.popen(@qemu_cmd) - - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end - end - - finish(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Script entry point -##-------------------------------------------------------------------------------------------------- -binary = ARGV.last -test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] -console_test_file = "tests/#{test_name}.rb" -qemu_cmd = ARGV.join(' ') - -test_runner = if File.exist?(console_test_file) - load console_test_file - # subtest_collection is provided by console_test_file - ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) - else - RawTest.new(binary, qemu_cmd, test_name) - end - -test_runner.exec diff --git a/13_exceptions_part2_peripheral_IRQs/Makefile b/13_exceptions_part2_peripheral_IRQs/Makefile index 4c2fb069..e860f00d 100644 --- a/13_exceptions_part2_peripheral_IRQs/Makefile +++ b/13_exceptions_part2_peripheral_IRQs/Makefile @@ -2,18 +2,32 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) +# Optional integration test name. +ifdef TEST + TEST_ARG = --test $(TEST) +else + TEST_ARG = --test '*' +endif + + -# BSP-specific arguments +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -44,20 +58,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -# Testing-specific arguments -ifdef TEST - ifeq ($(TEST),unit) - TEST_ARG = --lib - else - TEST_ARG = --test $(TEST) - endif -endif +KERNEL_ELF = target/$(TARGET)/release/kernel + -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -75,105 +87,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ - clippy clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -define KERNEL_TEST_RUNNER - #!/usr/bin/env bash - - TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') - TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') - - $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY -endef - -export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - $(call colorecho, "\nCompiling test(s) - $(BSP)") - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -182,10 +199,108 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot test_unit test_integration + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + + TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + +##------------------------------------------------------------------------------ +## Run integration test(s) +##------------------------------------------------------------------------------ +test_integration: + $(call colorecho, "\nCompiling integration test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +test: test_boot test_unit test_integration + +endif diff --git a/13_exceptions_part2_peripheral_IRQs/README.md b/13_exceptions_part2_peripheral_IRQs/README.md index 02341a12..b8cf351c 100644 --- a/13_exceptions_part2_peripheral_IRQs/README.md +++ b/13_exceptions_part2_peripheral_IRQs/README.md @@ -758,6 +758,19 @@ diff -uNr 12_integrated_testing/Cargo.toml 13_exceptions_part2_peripheral_IRQs/C edition = "2018" +diff -uNr 12_integrated_testing/Makefile 13_exceptions_part2_peripheral_IRQs/Makefile +--- 12_integrated_testing/Makefile ++++ 13_exceptions_part2_peripheral_IRQs/Makefile +@@ -291,7 +291,7 @@ + test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) +- RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib ++ @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + + ##------------------------------------------------------------------------------ + ## Run integration test(s) + diff -uNr 12_integrated_testing/src/_arch/aarch64/cpu/smp.rs 13_exceptions_part2_peripheral_IRQs/src/_arch/aarch64/cpu/smp.rs --- 12_integrated_testing/src/_arch/aarch64/cpu/smp.rs +++ 13_exceptions_part2_peripheral_IRQs/src/_arch/aarch64/cpu/smp.rs diff --git a/13_exceptions_part2_peripheral_IRQs/src/lib.rs b/13_exceptions_part2_peripheral_IRQs/src/lib.rs index 4ca9f03e..9c67f1ed 100644 --- a/13_exceptions_part2_peripheral_IRQs/src/lib.rs +++ b/13_exceptions_part2_peripheral_IRQs/src/lib.rs @@ -163,8 +163,9 @@ extern "Rust" { /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); diff --git a/13_exceptions_part2_peripheral_IRQs/src/panic_wait.rs b/13_exceptions_part2_peripheral_IRQs/src/panic_wait.rs index e3a9ed8a..130e952b 100644 --- a/13_exceptions_part2_peripheral_IRQs/src/panic_wait.rs +++ b/13_exceptions_part2_peripheral_IRQs/src/panic_wait.rs @@ -23,12 +23,12 @@ fn _panic_print(args: fmt::Arguments) { #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } diff --git a/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rb b/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rb index dfd6b16e..16fb6c79 100644 --- a/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rb +++ b/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rb @@ -8,6 +8,13 @@ require 'expect' TIMEOUT_SECS = 3 +# Error class for when expect times out. +class ExpectTimeoutError < StandardError + def initialize + super('Timeout while expecting string') + end +end + # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -16,7 +23,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end @@ -27,7 +34,7 @@ class TxStatistics end def run(qemu_out, _qemu_in) - raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? end end @@ -38,7 +45,7 @@ class RxStatistics end def run(qemu_out, _qemu_in) - raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? end end diff --git a/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rs b/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rs index 84b74479..03058f5e 100644 --- a/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rs +++ b/13_exceptions_part2_peripheral_IRQs/tests/00_console_sanity.rs @@ -31,12 +31,5 @@ unsafe fn kernel_init() -> ! { print!("{}", console().chars_read()); // The QEMU process running this test will be closed by the I/O test harness. - // cpu::wait_forever(); - - // For some reason, in this test, rustc or the linker produces an empty binary when - // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time - // being, the following lines are just a workaround to fix this compiler/linker weirdness. - use libkernel::time::interface::TimeManager; - libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); - cpu::qemu_exit_success() + cpu::wait_forever(); } diff --git a/13_exceptions_part2_peripheral_IRQs/tests/02_exception_sync_page_fault.rs b/13_exceptions_part2_peripheral_IRQs/tests/02_exception_sync_page_fault.rs index f1535d34..8febacd1 100644 --- a/13_exceptions_part2_peripheral_IRQs/tests/02_exception_sync_page_fault.rs +++ b/13_exceptions_part2_peripheral_IRQs/tests/02_exception_sync_page_fault.rs @@ -26,8 +26,8 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() { println!("MMU: {}", string); diff --git a/13_exceptions_part2_peripheral_IRQs/tests/boot_test_string.rb b/13_exceptions_part2_peripheral_IRQs/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/13_exceptions_part2_peripheral_IRQs/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/13_exceptions_part2_peripheral_IRQs/tests/runner.rb b/13_exceptions_part2_peripheral_IRQs/tests/runner.rb deleted file mode 100755 index 53116e08..00000000 --- a/13_exceptions_part2_peripheral_IRQs/tests/runner.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2019-2021 Andre Richter - -require 'English' -require 'pty' - -# Test base class. -class Test - INDENT = ' ' - - def print_border(status) - puts - puts "#{INDENT}-------------------------------------------------------------------" - puts status - puts "#{INDENT}-------------------------------------------------------------------\n\n\n" - end - - def print_error(error) - puts - print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") - end - - def print_success - print_border("#{INDENT}✅ Success: #{@test_name}") - end - - def print_output - puts "#{INDENT}-------------------------------------------------------------------" - print INDENT - print '🦀 ' - print @output.join.gsub("\n", "\n#{INDENT}") - end - - def finish(error) - print_output - - exit_code = if error - print_error(error) - false - else - print_success - true - end - - exit(exit_code) - end -end - -# Executes tests with console I/O. -class ConsoleTest < Test - def initialize(binary, qemu_cmd, test_name, console_subtests) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @console_subtests = console_subtests - @cur_subtest = 1 - @output = ["Running #{@console_subtests.length} console-based tests\n", - "-------------------------------------------------------------------\n\n"] - end - - def format_test_name(number, name) - formatted_name = "#{number.to_s.rjust(3)}. #{name}" - formatted_name.ljust(63, '.') - end - - def run_subtest(subtest, qemu_out, qemu_in) - @output << format_test_name(@cur_subtest, subtest.name) - - subtest.run(qemu_out, qemu_in) - - @output << "[ok]\n" - @cur_subtest += 1 - end - - def exec - error = false - - PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| - begin - @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } - rescue StandardError => e - error = e.message - end - - finish(error) - end - end -end - -# A wrapper around the bare QEMU invocation. -class RawTest < Test - MAX_WAIT_SECS = 5 - - def initialize(binary, qemu_cmd, test_name) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @output = [] - end - - def exec - error = 'Timed out waiting for test' - io = IO.popen(@qemu_cmd) - - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end - end - - finish(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Script entry point -##-------------------------------------------------------------------------------------------------- -binary = ARGV.last -test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] -console_test_file = "tests/#{test_name}.rb" -qemu_cmd = ARGV.join(' ') - -test_runner = if File.exist?(console_test_file) - load console_test_file - # subtest_collection is provided by console_test_file - ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) - else - RawTest.new(binary, qemu_cmd, test_name) - end - -test_runner.exec diff --git a/14_virtual_mem_part2_mmio_remap/Makefile b/14_virtual_mem_part2_mmio_remap/Makefile index 4c2fb069..e860f00d 100644 --- a/14_virtual_mem_part2_mmio_remap/Makefile +++ b/14_virtual_mem_part2_mmio_remap/Makefile @@ -2,18 +2,32 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) +# Optional integration test name. +ifdef TEST + TEST_ARG = --test $(TEST) +else + TEST_ARG = --test '*' +endif + + -# BSP-specific arguments +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -44,20 +58,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -# Testing-specific arguments -ifdef TEST - ifeq ($(TEST),unit) - TEST_ARG = --lib - else - TEST_ARG = --test $(TEST) - endif -endif +KERNEL_ELF = target/$(TARGET)/release/kernel + -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -75,105 +87,110 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ - clippy clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -define KERNEL_TEST_RUNNER - #!/usr/bin/env bash - - TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') - TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') - - $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY -endef - -export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - $(call colorecho, "\nCompiling test(s) - $(BSP)") - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -182,10 +199,108 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot test_unit test_integration + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + + TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + +##------------------------------------------------------------------------------ +## Run integration test(s) +##------------------------------------------------------------------------------ +test_integration: + $(call colorecho, "\nCompiling integration test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +test: test_boot test_unit test_integration + +endif diff --git a/14_virtual_mem_part2_mmio_remap/README.md b/14_virtual_mem_part2_mmio_remap/README.md index 6cb7e16f..d178e9c1 100644 --- a/14_virtual_mem_part2_mmio_remap/README.md +++ b/14_virtual_mem_part2_mmio_remap/README.md @@ -3296,8 +3296,8 @@ diff -uNr 13_exceptions_part2_peripheral_IRQs/tests/02_exception_sync_page_fault exception::handling_init(); bsp::console::qemu_bring_up_console(); @@ -29,10 +29,30 @@ + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); - if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() { - println!("MMU: {}", string); diff --git a/14_virtual_mem_part2_mmio_remap/src/lib.rs b/14_virtual_mem_part2_mmio_remap/src/lib.rs index 8a029738..002e14b9 100644 --- a/14_virtual_mem_part2_mmio_remap/src/lib.rs +++ b/14_virtual_mem_part2_mmio_remap/src/lib.rs @@ -165,8 +165,9 @@ extern "Rust" { /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); diff --git a/14_virtual_mem_part2_mmio_remap/src/panic_wait.rs b/14_virtual_mem_part2_mmio_remap/src/panic_wait.rs index e3a9ed8a..130e952b 100644 --- a/14_virtual_mem_part2_mmio_remap/src/panic_wait.rs +++ b/14_virtual_mem_part2_mmio_remap/src/panic_wait.rs @@ -23,12 +23,12 @@ fn _panic_print(args: fmt::Arguments) { #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } diff --git a/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rb b/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rb index dfd6b16e..16fb6c79 100644 --- a/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rb +++ b/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rb @@ -8,6 +8,13 @@ require 'expect' TIMEOUT_SECS = 3 +# Error class for when expect times out. +class ExpectTimeoutError < StandardError + def initialize + super('Timeout while expecting string') + end +end + # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -16,7 +23,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end @@ -27,7 +34,7 @@ class TxStatistics end def run(qemu_out, _qemu_in) - raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? end end @@ -38,7 +45,7 @@ class RxStatistics end def run(qemu_out, _qemu_in) - raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? end end diff --git a/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rs b/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rs index 84b74479..03058f5e 100644 --- a/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rs +++ b/14_virtual_mem_part2_mmio_remap/tests/00_console_sanity.rs @@ -31,12 +31,5 @@ unsafe fn kernel_init() -> ! { print!("{}", console().chars_read()); // The QEMU process running this test will be closed by the I/O test harness. - // cpu::wait_forever(); - - // For some reason, in this test, rustc or the linker produces an empty binary when - // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time - // being, the following lines are just a workaround to fix this compiler/linker weirdness. - use libkernel::time::interface::TimeManager; - libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); - cpu::qemu_exit_success() + cpu::wait_forever(); } diff --git a/14_virtual_mem_part2_mmio_remap/tests/02_exception_sync_page_fault.rs b/14_virtual_mem_part2_mmio_remap/tests/02_exception_sync_page_fault.rs index 940866a0..7b8bba35 100644 --- a/14_virtual_mem_part2_mmio_remap/tests/02_exception_sync_page_fault.rs +++ b/14_virtual_mem_part2_mmio_remap/tests/02_exception_sync_page_fault.rs @@ -26,8 +26,8 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); let phys_kernel_tables_base_addr = match memory::mmu::kernel_map_binary() { Err(string) => { diff --git a/14_virtual_mem_part2_mmio_remap/tests/boot_test_string.rb b/14_virtual_mem_part2_mmio_remap/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/14_virtual_mem_part2_mmio_remap/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/14_virtual_mem_part2_mmio_remap/tests/runner.rb b/14_virtual_mem_part2_mmio_remap/tests/runner.rb deleted file mode 100755 index 53116e08..00000000 --- a/14_virtual_mem_part2_mmio_remap/tests/runner.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2019-2021 Andre Richter - -require 'English' -require 'pty' - -# Test base class. -class Test - INDENT = ' ' - - def print_border(status) - puts - puts "#{INDENT}-------------------------------------------------------------------" - puts status - puts "#{INDENT}-------------------------------------------------------------------\n\n\n" - end - - def print_error(error) - puts - print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") - end - - def print_success - print_border("#{INDENT}✅ Success: #{@test_name}") - end - - def print_output - puts "#{INDENT}-------------------------------------------------------------------" - print INDENT - print '🦀 ' - print @output.join.gsub("\n", "\n#{INDENT}") - end - - def finish(error) - print_output - - exit_code = if error - print_error(error) - false - else - print_success - true - end - - exit(exit_code) - end -end - -# Executes tests with console I/O. -class ConsoleTest < Test - def initialize(binary, qemu_cmd, test_name, console_subtests) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @console_subtests = console_subtests - @cur_subtest = 1 - @output = ["Running #{@console_subtests.length} console-based tests\n", - "-------------------------------------------------------------------\n\n"] - end - - def format_test_name(number, name) - formatted_name = "#{number.to_s.rjust(3)}. #{name}" - formatted_name.ljust(63, '.') - end - - def run_subtest(subtest, qemu_out, qemu_in) - @output << format_test_name(@cur_subtest, subtest.name) - - subtest.run(qemu_out, qemu_in) - - @output << "[ok]\n" - @cur_subtest += 1 - end - - def exec - error = false - - PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| - begin - @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } - rescue StandardError => e - error = e.message - end - - finish(error) - end - end -end - -# A wrapper around the bare QEMU invocation. -class RawTest < Test - MAX_WAIT_SECS = 5 - - def initialize(binary, qemu_cmd, test_name) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @output = [] - end - - def exec - error = 'Timed out waiting for test' - io = IO.popen(@qemu_cmd) - - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end - end - - finish(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Script entry point -##-------------------------------------------------------------------------------------------------- -binary = ARGV.last -test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] -console_test_file = "tests/#{test_name}.rb" -qemu_cmd = ARGV.join(' ') - -test_runner = if File.exist?(console_test_file) - load console_test_file - # subtest_collection is provided by console_test_file - ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) - else - RawTest.new(binary, qemu_cmd, test_name) - end - -test_runner.exec diff --git a/15_virtual_mem_part3_precomputed_tables/Makefile b/15_virtual_mem_part3_precomputed_tables/Makefile index feb65cef..86a64ee5 100644 --- a/15_virtual_mem_part3_precomputed_tables/Makefile +++ b/15_virtual_mem_part3_precomputed_tables/Makefile @@ -2,18 +2,32 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) +# Optional integration test name. +ifdef TEST + TEST_ARG = --test $(TEST) +else + TEST_ARG = --test '*' +endif + + -# BSP-specific arguments +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -44,20 +58,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -# Testing-specific arguments -ifdef TEST - ifeq ($(TEST),unit) - TEST_ARG = --lib - else - TEST_ARG = --test $(TEST) - endif -endif +KERNEL_ELF = target/$(TARGET)/release/kernel + -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -75,107 +87,112 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TT_TOOL = ruby translation_table_tool/main.rb +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ - clippy clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) - @$(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $(KERNEL_ELF) + @$(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -define KERNEL_TEST_RUNNER - #!/usr/bin/env bash - - TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') - TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') - - $(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $$TEST_ELF > /dev/null - $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY -endef - -export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - $(call colorecho, "\nCompiling test(s) - $(BSP)") - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -184,10 +201,109 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot test_unit test_integration + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + + TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $$TEST_ELF > /dev/null + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + +##------------------------------------------------------------------------------ +## Run integration test(s) +##------------------------------------------------------------------------------ +test_integration: + $(call colorecho, "\nCompiling integration test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +test: test_boot test_unit test_integration + +endif diff --git a/15_virtual_mem_part3_precomputed_tables/README.md b/15_virtual_mem_part3_precomputed_tables/README.md index 581dcefc..b6a2b5b6 100644 --- a/15_virtual_mem_part3_precomputed_tables/README.md +++ b/15_virtual_mem_part3_precomputed_tables/README.md @@ -776,21 +776,29 @@ diff -uNr 14_virtual_mem_part2_mmio_remap/Cargo.toml 15_virtual_mem_part3_precom diff -uNr 14_virtual_mem_part2_mmio_remap/Makefile 15_virtual_mem_part3_precomputed_tables/Makefile --- 14_virtual_mem_part2_mmio_remap/Makefile +++ 15_virtual_mem_part3_precomputed_tables/Makefile -@@ -112,6 +112,7 @@ +@@ -88,6 +88,7 @@ + -O binary + + EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) ++EXEC_TT_TOOL = ruby translation_table_tool/main.rb + EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb + EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +@@ -133,6 +134,7 @@ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) -+ @$(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $(KERNEL_ELF) ++ @$(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $(KERNEL_ELF) - $(KERNEL_BIN): $(KERNEL_ELF) - @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) -@@ -134,6 +135,7 @@ + ##------------------------------------------------------------------------------ + ## Build the stripped kernel binary +@@ -271,6 +273,7 @@ TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') -+ $(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $$TEST_ELF > /dev/null ++ $(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $$TEST_ELF > /dev/null $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY endef diff -uNr 14_virtual_mem_part2_mmio_remap/src/_arch/aarch64/cpu/boot.rs 15_virtual_mem_part3_precomputed_tables/src/_arch/aarch64/cpu/boot.rs @@ -1555,8 +1563,8 @@ diff -uNr 14_virtual_mem_part2_mmio_remap/tests/02_exception_sync_page_fault.rs exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); - let phys_kernel_tables_base_addr = match memory::mmu::kernel_map_binary() { - Err(string) => { diff --git a/15_virtual_mem_part3_precomputed_tables/src/lib.rs b/15_virtual_mem_part3_precomputed_tables/src/lib.rs index 8a029738..002e14b9 100644 --- a/15_virtual_mem_part3_precomputed_tables/src/lib.rs +++ b/15_virtual_mem_part3_precomputed_tables/src/lib.rs @@ -165,8 +165,9 @@ extern "Rust" { /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); diff --git a/15_virtual_mem_part3_precomputed_tables/src/panic_wait.rs b/15_virtual_mem_part3_precomputed_tables/src/panic_wait.rs index e3a9ed8a..130e952b 100644 --- a/15_virtual_mem_part3_precomputed_tables/src/panic_wait.rs +++ b/15_virtual_mem_part3_precomputed_tables/src/panic_wait.rs @@ -23,12 +23,12 @@ fn _panic_print(args: fmt::Arguments) { #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } diff --git a/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rb b/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rb index dfd6b16e..16fb6c79 100644 --- a/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rb +++ b/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rb @@ -8,6 +8,13 @@ require 'expect' TIMEOUT_SECS = 3 +# Error class for when expect times out. +class ExpectTimeoutError < StandardError + def initialize + super('Timeout while expecting string') + end +end + # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -16,7 +23,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end @@ -27,7 +34,7 @@ class TxStatistics end def run(qemu_out, _qemu_in) - raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? end end @@ -38,7 +45,7 @@ class RxStatistics end def run(qemu_out, _qemu_in) - raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? end end diff --git a/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rs b/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rs index 84b74479..03058f5e 100644 --- a/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rs +++ b/15_virtual_mem_part3_precomputed_tables/tests/00_console_sanity.rs @@ -31,12 +31,5 @@ unsafe fn kernel_init() -> ! { print!("{}", console().chars_read()); // The QEMU process running this test will be closed by the I/O test harness. - // cpu::wait_forever(); - - // For some reason, in this test, rustc or the linker produces an empty binary when - // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time - // being, the following lines are just a workaround to fix this compiler/linker weirdness. - use libkernel::time::interface::TimeManager; - libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); - cpu::qemu_exit_success() + cpu::wait_forever(); } diff --git a/15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_fault.rs b/15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_fault.rs index 6a0c11f3..e7fa8800 100644 --- a/15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_fault.rs +++ b/15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_fault.rs @@ -24,8 +24,8 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); println!("Writing beyond mapped area to address 9 GiB..."); let big_addr: u64 = 9 * 1024 * 1024 * 1024; diff --git a/15_virtual_mem_part3_precomputed_tables/tests/boot_test_string.rb b/15_virtual_mem_part3_precomputed_tables/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/15_virtual_mem_part3_precomputed_tables/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/15_virtual_mem_part3_precomputed_tables/tests/runner.rb b/15_virtual_mem_part3_precomputed_tables/tests/runner.rb deleted file mode 100755 index 53116e08..00000000 --- a/15_virtual_mem_part3_precomputed_tables/tests/runner.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2019-2021 Andre Richter - -require 'English' -require 'pty' - -# Test base class. -class Test - INDENT = ' ' - - def print_border(status) - puts - puts "#{INDENT}-------------------------------------------------------------------" - puts status - puts "#{INDENT}-------------------------------------------------------------------\n\n\n" - end - - def print_error(error) - puts - print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") - end - - def print_success - print_border("#{INDENT}✅ Success: #{@test_name}") - end - - def print_output - puts "#{INDENT}-------------------------------------------------------------------" - print INDENT - print '🦀 ' - print @output.join.gsub("\n", "\n#{INDENT}") - end - - def finish(error) - print_output - - exit_code = if error - print_error(error) - false - else - print_success - true - end - - exit(exit_code) - end -end - -# Executes tests with console I/O. -class ConsoleTest < Test - def initialize(binary, qemu_cmd, test_name, console_subtests) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @console_subtests = console_subtests - @cur_subtest = 1 - @output = ["Running #{@console_subtests.length} console-based tests\n", - "-------------------------------------------------------------------\n\n"] - end - - def format_test_name(number, name) - formatted_name = "#{number.to_s.rjust(3)}. #{name}" - formatted_name.ljust(63, '.') - end - - def run_subtest(subtest, qemu_out, qemu_in) - @output << format_test_name(@cur_subtest, subtest.name) - - subtest.run(qemu_out, qemu_in) - - @output << "[ok]\n" - @cur_subtest += 1 - end - - def exec - error = false - - PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| - begin - @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } - rescue StandardError => e - error = e.message - end - - finish(error) - end - end -end - -# A wrapper around the bare QEMU invocation. -class RawTest < Test - MAX_WAIT_SECS = 5 - - def initialize(binary, qemu_cmd, test_name) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @output = [] - end - - def exec - error = 'Timed out waiting for test' - io = IO.popen(@qemu_cmd) - - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end - end - - finish(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Script entry point -##-------------------------------------------------------------------------------------------------- -binary = ARGV.last -test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] -console_test_file = "tests/#{test_name}.rb" -qemu_cmd = ARGV.join(' ') - -test_runner = if File.exist?(console_test_file) - load console_test_file - # subtest_collection is provided by console_test_file - ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) - else - RawTest.new(binary, qemu_cmd, test_name) - end - -test_runner.exec diff --git a/16_virtual_mem_part4_higher_half_kernel/Makefile b/16_virtual_mem_part4_higher_half_kernel/Makefile index feb65cef..86a64ee5 100644 --- a/16_virtual_mem_part4_higher_half_kernel/Makefile +++ b/16_virtual_mem_part4_higher_half_kernel/Makefile @@ -2,18 +2,32 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) +# Optional integration test name. +ifdef TEST + TEST_ARG = --test $(TEST) +else + TEST_ARG = --test '*' +endif + + -# BSP-specific arguments +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -44,20 +58,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -# Testing-specific arguments -ifdef TEST - ifeq ($(TEST),unit) - TEST_ARG = --lib - else - TEST_ARG = --test $(TEST) - endif -endif +KERNEL_ELF = target/$(TARGET)/release/kernel + -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -75,107 +87,112 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel - -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot -DOCKER_ARG_DEV = --privileged -v /dev:/dev -DOCKER_ARG_NET = --network host +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TT_TOOL = ruby translation_table_tool/main.rb +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb + +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DIR_JTAG = -v $(shell pwd)/../X1_JTAG_boot:/work/X1_JTAG_boot +DOCKER_ARG_DEV = --privileged -v /dev:/dev +DOCKER_ARG_NET = --network host DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) -DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) +DOCKER_GDB = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) - DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) + DOCKER_JTAGBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_ARG_DIR_JTAG) $(DOCKER_IMAGE) DOCKER_OPENOCD = $(DOCKER_CMD_DEV) $(DOCKER_ARG_NET) $(DOCKER_IMAGE) else DOCKER_OPENOCD = echo "Not yet supported on non-Linux systems."; \# endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu test chainboot jtagboot openocd gdb gdb-opt0 \ - clippy clean readelf objdump nm check + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) - @$(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $(KERNEL_ELF) + @$(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) -qemu test: +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -define KERNEL_TEST_RUNNER - #!/usr/bin/env bash - - TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') - TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') - - $(DOCKER_TOOLS) ruby translation_table_tool/main.rb $(TARGET) $(BSP) $$TEST_ELF > /dev/null - $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY - $(DOCKER_TEST) ruby tests/runner.rb $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY -endef - -export KERNEL_TEST_RUNNER -test: FEATURES += --features test_build -test: - $(call colorecho, "\nCompiling test(s) - $(BSP)") - @mkdir -p target - @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh - @chmod +x target/kernel_test_runner.sh - @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) -jtagboot: - @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) - -openocd: - $(call colorecho, "\nLaunching OpenOCD") - @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) - -gdb: RUSTC_MISC_ARGS += -C debuginfo=2 -gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 -gdb gdb-opt0: $(KERNEL_ELF) - $(call colorecho, "\nLaunching GDB") - @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) - +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -184,10 +201,109 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Debugging targets +##-------------------------------------------------------------------------------------------------- +.PHONY: jtagboot openocd gdb gdb-opt0 + +##------------------------------------------------------------------------------ +## Push the JTAG boot image to the real HW target +##------------------------------------------------------------------------------ +jtagboot: + @$(DOCKER_JTAGBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(JTAG_BOOT_IMAGE) + +##------------------------------------------------------------------------------ +## Start OpenOCD session +##------------------------------------------------------------------------------ +openocd: + $(call colorecho, "\nLaunching OpenOCD") + @$(DOCKER_OPENOCD) openocd $(OPENOCD_ARG) + +##------------------------------------------------------------------------------ +## Start GDB session +##------------------------------------------------------------------------------ +gdb: RUSTC_MISC_ARGS += -C debuginfo=2 +gdb-opt0: RUSTC_MISC_ARGS += -C debuginfo=2 -C opt-level=0 +gdb gdb-opt0: $(KERNEL_ELF) + $(call colorecho, "\nLaunching GDB") + @$(DOCKER_GDB) gdb-multiarch -q $(KERNEL_ELF) + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot test_unit test_integration + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test_unit test_integration test: + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +##------------------------------------------------------------------------------ +## Helpers for unit and integration test targets +##------------------------------------------------------------------------------ +define KERNEL_TEST_RUNNER + #!/usr/bin/env bash + + TEST_ELF=$$(echo $$1 | sed -e 's/.*target/target/g') + TEST_BINARY=$$(echo $$1.img | sed -e 's/.*target/target/g') + + $(DOCKER_TOOLS) $(EXEC_TT_TOOL) $(TARGET) $(BSP) $$TEST_ELF > /dev/null + $(OBJCOPY_CMD) $$TEST_ELF $$TEST_BINARY + $(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_TEST_ARGS) -kernel $$TEST_BINARY +endef + +export KERNEL_TEST_RUNNER + +define test_prepare + @mkdir -p target + @echo "$$KERNEL_TEST_RUNNER" > target/kernel_test_runner.sh + @chmod +x target/kernel_test_runner.sh +endef + +test_unit test_integration: FEATURES += --features test_build + +##------------------------------------------------------------------------------ +## Run unit test(s) +##------------------------------------------------------------------------------ +test_unit: + $(call colorecho, "\nCompiling unit test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) --lib + +##------------------------------------------------------------------------------ +## Run integration test(s) +##------------------------------------------------------------------------------ +test_integration: + $(call colorecho, "\nCompiling integration test(s) - $(BSP)") + $(call test_prepare) + @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(TEST_CMD) $(TEST_ARG) + +test: test_boot test_unit test_integration + +endif diff --git a/16_virtual_mem_part4_higher_half_kernel/README.md b/16_virtual_mem_part4_higher_half_kernel/README.md index fa594703..15629a2a 100644 --- a/16_virtual_mem_part4_higher_half_kernel/README.md +++ b/16_virtual_mem_part4_higher_half_kernel/README.md @@ -617,8 +617,8 @@ diff -uNr 15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_f --- 15_virtual_mem_part3_precomputed_tables/tests/02_exception_sync_page_fault.rs +++ 16_virtual_mem_part4_higher_half_kernel/tests/02_exception_sync_page_fault.rs @@ -27,8 +27,8 @@ + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); - println!("Writing beyond mapped area to address 9 GiB..."); - let big_addr: u64 = 9 * 1024 * 1024 * 1024; diff --git a/16_virtual_mem_part4_higher_half_kernel/src/lib.rs b/16_virtual_mem_part4_higher_half_kernel/src/lib.rs index 7b5cdb09..c75a96ea 100644 --- a/16_virtual_mem_part4_higher_half_kernel/src/lib.rs +++ b/16_virtual_mem_part4_higher_half_kernel/src/lib.rs @@ -160,8 +160,9 @@ pub fn version() -> &'static str { /// The default runner for unit tests. pub fn test_runner(tests: &[&test_types::UnitTest]) { + // This line will be printed as the test header. println!("Running {} tests", tests.len()); - println!("-------------------------------------------------------------------\n"); + for (i, test) in tests.iter().enumerate() { print!("{:>3}. {:.<58}", i + 1, test.name); diff --git a/16_virtual_mem_part4_higher_half_kernel/src/panic_wait.rs b/16_virtual_mem_part4_higher_half_kernel/src/panic_wait.rs index e3a9ed8a..130e952b 100644 --- a/16_virtual_mem_part4_higher_half_kernel/src/panic_wait.rs +++ b/16_virtual_mem_part4_higher_half_kernel/src/panic_wait.rs @@ -23,12 +23,12 @@ fn _panic_print(args: fmt::Arguments) { #[linkage = "weak"] #[no_mangle] fn _panic_exit() -> ! { - #[cfg(not(test_build))] + #[cfg(not(feature = "test_build"))] { cpu::wait_forever() } - #[cfg(test_build)] + #[cfg(feature = "test_build")] { cpu::qemu_exit_failure() } diff --git a/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rb b/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rb index dfd6b16e..16fb6c79 100644 --- a/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rb +++ b/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rb @@ -8,6 +8,13 @@ require 'expect' TIMEOUT_SECS = 3 +# Error class for when expect times out. +class ExpectTimeoutError < StandardError + def initialize + super('Timeout while expecting string') + end +end + # Verify sending and receiving works as expected. class TxRxHandshake def name @@ -16,7 +23,7 @@ class TxRxHandshake def run(qemu_out, qemu_in) qemu_in.write_nonblock('ABC') - raise('TX/RX test failed') if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('OK1234', TIMEOUT_SECS).nil? end end @@ -27,7 +34,7 @@ class TxStatistics end def run(qemu_out, _qemu_in) - raise('chars_written reported wrong') if qemu_out.expect('6', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('6', TIMEOUT_SECS).nil? end end @@ -38,7 +45,7 @@ class RxStatistics end def run(qemu_out, _qemu_in) - raise('chars_read reported wrong') if qemu_out.expect('3', TIMEOUT_SECS).nil? + raise ExpectTimeoutError if qemu_out.expect('3', TIMEOUT_SECS).nil? end end diff --git a/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rs b/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rs index 84b74479..03058f5e 100644 --- a/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rs +++ b/16_virtual_mem_part4_higher_half_kernel/tests/00_console_sanity.rs @@ -31,12 +31,5 @@ unsafe fn kernel_init() -> ! { print!("{}", console().chars_read()); // The QEMU process running this test will be closed by the I/O test harness. - // cpu::wait_forever(); - - // For some reason, in this test, rustc or the linker produces an empty binary when - // wait_forever() is used. Calling qemu_exit_success() fixes this behavior. So for the time - // being, the following lines are just a workaround to fix this compiler/linker weirdness. - use libkernel::time::interface::TimeManager; - libkernel::time::time_manager().spin_for(core::time::Duration::from_secs(3600)); - cpu::qemu_exit_success() + cpu::wait_forever(); } diff --git a/16_virtual_mem_part4_higher_half_kernel/tests/02_exception_sync_page_fault.rs b/16_virtual_mem_part4_higher_half_kernel/tests/02_exception_sync_page_fault.rs index 30d420a7..47ef31fa 100644 --- a/16_virtual_mem_part4_higher_half_kernel/tests/02_exception_sync_page_fault.rs +++ b/16_virtual_mem_part4_higher_half_kernel/tests/02_exception_sync_page_fault.rs @@ -24,8 +24,8 @@ unsafe fn kernel_init() -> ! { exception::handling_init(); bsp::console::qemu_bring_up_console(); + // This line will be printed as the test header. println!("Testing synchronous exception handling by causing a page fault"); - println!("-------------------------------------------------------------------\n"); println!("Writing to bottom of address space to address 1 GiB..."); let big_addr: u64 = 1 * 1024 * 1024 * 1024; diff --git a/16_virtual_mem_part4_higher_half_kernel/tests/boot_test_string.rb b/16_virtual_mem_part4_higher_half_kernel/tests/boot_test_string.rb new file mode 100644 index 00000000..f778b3d8 --- /dev/null +++ b/16_virtual_mem_part4_higher_half_kernel/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Echoing input now' diff --git a/16_virtual_mem_part4_higher_half_kernel/tests/runner.rb b/16_virtual_mem_part4_higher_half_kernel/tests/runner.rb deleted file mode 100755 index 53116e08..00000000 --- a/16_virtual_mem_part4_higher_half_kernel/tests/runner.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# SPDX-License-Identifier: MIT OR Apache-2.0 -# -# Copyright (c) 2019-2021 Andre Richter - -require 'English' -require 'pty' - -# Test base class. -class Test - INDENT = ' ' - - def print_border(status) - puts - puts "#{INDENT}-------------------------------------------------------------------" - puts status - puts "#{INDENT}-------------------------------------------------------------------\n\n\n" - end - - def print_error(error) - puts - print_border("#{INDENT}❌ Failure: #{error}: #{@test_name}") - end - - def print_success - print_border("#{INDENT}✅ Success: #{@test_name}") - end - - def print_output - puts "#{INDENT}-------------------------------------------------------------------" - print INDENT - print '🦀 ' - print @output.join.gsub("\n", "\n#{INDENT}") - end - - def finish(error) - print_output - - exit_code = if error - print_error(error) - false - else - print_success - true - end - - exit(exit_code) - end -end - -# Executes tests with console I/O. -class ConsoleTest < Test - def initialize(binary, qemu_cmd, test_name, console_subtests) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @console_subtests = console_subtests - @cur_subtest = 1 - @output = ["Running #{@console_subtests.length} console-based tests\n", - "-------------------------------------------------------------------\n\n"] - end - - def format_test_name(number, name) - formatted_name = "#{number.to_s.rjust(3)}. #{name}" - formatted_name.ljust(63, '.') - end - - def run_subtest(subtest, qemu_out, qemu_in) - @output << format_test_name(@cur_subtest, subtest.name) - - subtest.run(qemu_out, qemu_in) - - @output << "[ok]\n" - @cur_subtest += 1 - end - - def exec - error = false - - PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| - begin - @console_subtests.each { |t| run_subtest(t, qemu_out, qemu_in) } - rescue StandardError => e - error = e.message - end - - finish(error) - end - end -end - -# A wrapper around the bare QEMU invocation. -class RawTest < Test - MAX_WAIT_SECS = 5 - - def initialize(binary, qemu_cmd, test_name) - super() - - @binary = binary - @qemu_cmd = qemu_cmd - @test_name = test_name - @output = [] - end - - def exec - error = 'Timed out waiting for test' - io = IO.popen(@qemu_cmd) - - while IO.select([io], nil, nil, MAX_WAIT_SECS) - begin - @output << io.read_nonblock(1024) - rescue EOFError - io.close - error = $CHILD_STATUS.to_i != 0 - break - end - end - - finish(error) - end -end - -##-------------------------------------------------------------------------------------------------- -## Script entry point -##-------------------------------------------------------------------------------------------------- -binary = ARGV.last -test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] -console_test_file = "tests/#{test_name}.rb" -qemu_cmd = ARGV.join(' ') - -test_runner = if File.exist?(console_test_file) - load console_test_file - # subtest_collection is provided by console_test_file - ConsoleTest.new(binary, qemu_cmd, test_name, subtest_collection) - else - RawTest.new(binary, qemu_cmd, test_name) - end - -test_runner.exec diff --git a/X1_JTAG_boot/Makefile b/X1_JTAG_boot/Makefile index b5a56d07..8336ccb7 100644 --- a/X1_JTAG_boot/Makefile +++ b/X1_JTAG_boot/Makefile @@ -2,18 +2,25 @@ ## ## Copyright (c) 2018-2021 Andre Richter -include ../utils/color.mk.in +include ../common/color.mk.in -# Default to the RPi3 +##-------------------------------------------------------------------------------------------------- +## Optional, user-provided configuration values +##-------------------------------------------------------------------------------------------------- + +# Default to the RPi3. BSP ?= rpi3 # Default to a serial device name that is common in Linux. DEV_SERIAL ?= /dev/ttyUSB0 -# Query the host system's kernel name -UNAME_S = $(shell uname -s) -# BSP-specific arguments + +##-------------------------------------------------------------------------------------------------- +## Hardcoded configuration values +##-------------------------------------------------------------------------------------------------- + +# BSP-specific arguments. ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img @@ -38,11 +45,18 @@ else ifeq ($(BSP),rpi4) RUSTC_MISC_ARGS = -C target-cpu=cortex-a72 endif -# Export for build.rs +QEMU_MISSING_STRING = "This board is not yet supported for QEMU." + +# Export for build.rs. export LINKER_FILE -QEMU_MISSING_STRING = "This board is not yet supported for QEMU." +KERNEL_ELF = target/$(TARGET)/release/kernel + + +##-------------------------------------------------------------------------------------------------- +## Command building blocks +##-------------------------------------------------------------------------------------------------- RUSTFLAGS = -C link-arg=-T$(LINKER_FILE) $(RUSTC_MISC_ARGS) RUSTFLAGS_PEDANTIC = $(RUSTFLAGS) -D warnings -D missing_docs @@ -59,64 +73,103 @@ OBJCOPY_CMD = rust-objcopy \ --strip-all \ -O binary -KERNEL_ELF = target/$(TARGET)/release/kernel +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb +EXEC_MINIPUSH = ruby ../common/serial/minipush.rb -DOCKER_IMAGE = rustembedded/osdev-utils -DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial -DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -DOCKER_ARG_DEV = --privileged -v /dev:/dev +##------------------------------------------------------------------------------ +## Dockerization +##------------------------------------------------------------------------------ +DOCKER_IMAGE = rustembedded/osdev-utils +DOCKER_CMD = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial +DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i +DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common +DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) +DOCKER_TEST = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) -# Dockerize commands that require USB device passthrough only on Linux -ifeq ($(UNAME_S),Linux) +# Dockerize commands, which require USB device passthrough, only on Linux. +ifeq ($(shell uname -s),Linux) DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) - DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE) endif -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -EXEC_MINIPUSH = ruby ../utils/minipush.rb + +##-------------------------------------------------------------------------------------------------- +## Targets +##-------------------------------------------------------------------------------------------------- .PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the kernel ELF +##------------------------------------------------------------------------------ $(KERNEL_ELF): $(call colorecho, "\nCompiling kernel - $(BSP)") @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(RUSTC_CMD) +##------------------------------------------------------------------------------ +## Build the stripped kernel binary +##------------------------------------------------------------------------------ $(KERNEL_BIN): $(KERNEL_ELF) @$(OBJCOPY_CMD) $(KERNEL_ELF) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Build the documentation +##------------------------------------------------------------------------------ doc: $(call colorecho, "\nGenerating docs") @$(DOC_CMD) --document-private-items --open -ifeq ($(QEMU_MACHINE_TYPE),) +##------------------------------------------------------------------------------ +## Run the kernel in QEMU +##------------------------------------------------------------------------------ +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + qemu: $(call colorecho, "\n$(QEMU_MISSING_STRING)") -else + +else # QEMU is supported. + qemu: $(KERNEL_BIN) $(call colorecho, "\nLaunching QEMU") @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif +##------------------------------------------------------------------------------ +## Push the kernel to the real HW target +##------------------------------------------------------------------------------ chainboot: $(KERNEL_BIN) @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run clippy +##------------------------------------------------------------------------------ clippy: @RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) +##------------------------------------------------------------------------------ +## Clean +##------------------------------------------------------------------------------ clean: rm -rf target $(KERNEL_BIN) +##------------------------------------------------------------------------------ +## Run readelf +##------------------------------------------------------------------------------ readelf: $(KERNEL_ELF) $(call colorecho, "\nLaunching readelf") @$(DOCKER_TOOLS) $(READELF_BINARY) --headers $(KERNEL_ELF) +##------------------------------------------------------------------------------ +## Run objdump +##------------------------------------------------------------------------------ objdump: $(KERNEL_ELF) $(call colorecho, "\nLaunching objdump") @$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \ @@ -125,10 +178,40 @@ objdump: $(KERNEL_ELF) --section .got \ $(KERNEL_ELF) | rustfilt +##------------------------------------------------------------------------------ +## Run nm +##------------------------------------------------------------------------------ nm: $(KERNEL_ELF) $(call colorecho, "\nLaunching nm") @$(DOCKER_TOOLS) $(NM_BINARY) --demangle --print-size $(KERNEL_ELF) | sort | rustfilt -# For rust-analyzer +##------------------------------------------------------------------------------ +## Helper target for rust-analyzer +##------------------------------------------------------------------------------ check: @RUSTFLAGS="$(RUSTFLAGS)" $(CHECK_CMD) --message-format=json + + + +##-------------------------------------------------------------------------------------------------- +## Testing targets +##-------------------------------------------------------------------------------------------------- +.PHONY: test test_boot + +ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board. + +test_boot test : + $(call colorecho, "\n$(QEMU_MISSING_STRING)") + +else # QEMU is supported. + +##------------------------------------------------------------------------------ +## Run boot test +##------------------------------------------------------------------------------ +test_boot: $(KERNEL_BIN) + $(call colorecho, "\nBoot test - $(BSP)") + @$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + +test: test_boot + +endif diff --git a/X1_JTAG_boot/tests/boot_test_string.rb b/X1_JTAG_boot/tests/boot_test_string.rb new file mode 100644 index 00000000..029dbd06 --- /dev/null +++ b/X1_JTAG_boot/tests/boot_test_string.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +EXPECTED_PRINT = 'Please connect over JTAG now' diff --git a/utils/color.mk.in b/common/color.mk.in similarity index 100% rename from utils/color.mk.in rename to common/color.mk.in diff --git a/utils/minipush.rb b/common/serial/minipush.rb similarity index 83% rename from utils/minipush.rb rename to common/serial/minipush.rb index a9ee9bb2..b18c4983 100755 --- a/utils/minipush.rb +++ b/common/serial/minipush.rb @@ -14,19 +14,19 @@ class ProtocolError < StandardError; end # The main class class MiniPush < MiniTerm - def initialize(serial_name, binary_image_path) + def initialize(serial_name, payload_path) super(serial_name) @name_short = 'MP' # override - @binary_image_path = binary_image_path - @binary_size = nil - @binary_image = nil + @payload_path = payload_path + @payload_size = nil + @payload_data = nil end private # The three characters signaling the request token form the consecutive sequence "\x03\x03\x03". - def wait_for_binary_request + def wait_for_payload_request puts "[#{@name_short}] 🔌 Please power the target now" # Timeout for the request token starts after the first sign of life was received. @@ -54,27 +54,28 @@ class MiniPush < MiniTerm end end - def load_binary - @binary_size = File.size(@binary_image_path) - @binary_image = File.binread(@binary_image_path) + def load_payload + @payload_size = File.size(@payload_path) + @payload_data = File.binread(@payload_path) end def send_size - @target_serial.print([@binary_size].pack('L<')) + @target_serial.print([@payload_size].pack('L<')) raise ProtocolError if @target_serial.read(2) != 'OK' end - def send_binary + def send_payload pb = ProgressBar.create( - total: @binary_size, + total: @payload_size, format: "[#{@name_short}] ⏩ Pushing %k KiB %b🦀%i %p%% %r KiB/s %a", rate_scale: ->(rate) { rate / 1024 }, - length: 92 + length: 92, + output: $stdout ) # Send in 512 byte chunks. while pb.progress < pb.total - part = @binary_image.slice(pb.progress, 512) + part = @payload_data.slice(pb.progress, 512) pb.progress += @target_serial.write(part) end end @@ -95,10 +96,10 @@ class MiniPush < MiniTerm # override def run open_serial - wait_for_binary_request - load_binary + wait_for_payload_request + load_payload send_size - send_binary + send_payload terminal rescue ConnectionError, EOFError, Errno::EIO, ProtocolError, Timeout::Error => e handle_reconnect(e) diff --git a/utils/minipush/progressbar_patch.rb b/common/serial/minipush/progressbar_patch.rb similarity index 100% rename from utils/minipush/progressbar_patch.rb rename to common/serial/minipush/progressbar_patch.rb diff --git a/utils/miniterm.rb b/common/serial/miniterm.rb similarity index 99% rename from utils/miniterm.rb rename to common/serial/miniterm.rb index 10cc4bc5..06e7efd1 100755 --- a/utils/miniterm.rb +++ b/common/serial/miniterm.rb @@ -7,8 +7,9 @@ require 'rubygems' require 'bundler/setup' -require 'io/console' + require 'colorize' +require 'io/console' require 'serialport' SERIAL_BAUD = 921_600 diff --git a/common/tests/boot_test.rb b/common/tests/boot_test.rb new file mode 100644 index 00000000..1f5301f9 --- /dev/null +++ b/common/tests/boot_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2021 Andre Richter + +require_relative 'test' +require 'timeout' + +# Check for an expected string when booting the kernel in QEMU. +class BootTest < Test + MAX_WAIT_SECS = 5 + + def initialize(qemu_cmd, expected_print) + super() + + @qemu_cmd = qemu_cmd + @expected_print = expected_print + + @test_name = 'Boot test' + @test_description = "Checking for the string: '#{@expected_print}'" + @test_output = [] + @test_error = nil + end + + private + + def expected_string_observed?(qemu_output) + qemu_output.join.include?(@expected_print) + end + + # Convert the recorded output to an array of lines. + def post_process_and_add_output(qemu_output) + @test_output += qemu_output.join.split("\n") + end + + # override + def setup + @qemu_serial = IO.popen(@qemu_cmd, err: '/dev/null') + @qemu_pid = @qemu_serial.pid + end + + # override + def cleanup + Timeout.timeout(MAX_WAIT_SECS) do + Process.kill('TERM', @qemu_pid) + Process.wait + end + rescue StandardError => e + puts 'QEMU graceful shutdown didn\'t work. Skipping it.' + puts e + end + + def run_concrete_test + qemu_output = [] + Timeout.timeout(MAX_WAIT_SECS) do + while IO.select([@qemu_serial]) + qemu_output << @qemu_serial.read_nonblock(1024) + + if expected_string_observed?(qemu_output) + @test_error = false + break + end + end + end + rescue EOFError + @test_error = 'QEMU quit unexpectedly' + rescue Timeout::Error + @test_error = 'Timed out waiting for magic string' + rescue StandardError => e + @test_error = e.message + ensure + post_process_and_add_output(qemu_output) + end +end diff --git a/common/tests/console_io_test.rb b/common/tests/console_io_test.rb new file mode 100644 index 00000000..66822a4e --- /dev/null +++ b/common/tests/console_io_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2019-2021 Andre Richter + +require 'pty' +require_relative 'test' + +# A test doing console I/O with the QEMU binary. +class ConsoleIOTest < Test + def initialize(qemu_cmd, test_name, console_subtests) + super() + + @qemu_cmd = qemu_cmd + @console_subtests = console_subtests + + @test_name = test_name + @test_description = "Running #{@console_subtests.length} console I/O tests" + @test_output = [] + @test_error = nil + end + + private + + def format_test_name(number, name) + formatted_name = "#{number.to_s.rjust(3)}. #{name}" + formatted_name.ljust(63, '.') + end + + def run_subtest(subtest, test_id, qemu_out, qemu_in) + @test_output << format_test_name(test_id, subtest.name) + subtest.run(qemu_out, qemu_in) + @test_output.last.concat('[ok]') + end + + def run_concrete_test + @test_error = false + + PTY.spawn(@qemu_cmd) do |qemu_out, qemu_in| + @console_subtests.each_with_index do |t, i| + run_subtest(t, i + 1, qemu_out, qemu_in) + end + rescue StandardError => e + @test_error = e.message + end + end +end diff --git a/common/tests/dispatch.rb b/common/tests/dispatch.rb new file mode 100755 index 00000000..14827265 --- /dev/null +++ b/common/tests/dispatch.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2019-2021 Andre Richter + +require_relative 'boot_test' +require_relative 'console_io_test' +require_relative 'exit_code_test' + +qemu_cmd = ARGV.join(' ') +binary = ARGV.last +test_name = binary.gsub(%r{.*deps/}, '').split('-')[0] + +case test_name +when 'kernel8.img' + load 'tests/boot_test_string.rb' # provides 'EXPECTED_PRINT' + BootTest.new(qemu_cmd, EXPECTED_PRINT).run # Doesn't return + +when 'libkernel' + ExitCodeTest.new(qemu_cmd, 'Kernel library unit tests').run # Doesn't return + +else + console_test_file = "tests/#{test_name}.rb" + test_name.concat('.rs') + test = if File.exist?(console_test_file) + load console_test_file # provides 'subtest_collection' + ConsoleIOTest.new(qemu_cmd, test_name, subtest_collection) + else + ExitCodeTest.new(qemu_cmd, test_name) + end + + test.run # Doesn't return +end diff --git a/common/tests/exit_code_test.rb b/common/tests/exit_code_test.rb new file mode 100644 index 00000000..2f14ab78 --- /dev/null +++ b/common/tests/exit_code_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2019-2021 Andre Richter + +require 'English' +require_relative 'test' + +# A test that only inspects the exit code of the QEMU binary. +class ExitCodeTest < Test + MAX_WAIT_SECS = 5 + + def initialize(qemu_cmd, test_name) + super() + + @qemu_cmd = qemu_cmd + + @test_name = test_name + @test_description = nil + @test_output = [] + @test_error = nil + end + + private + + # Convert the recorded output to an array of lines, and extract the test description. + def post_process_output + @test_output = @test_output.join.split("\n") + @test_description = @test_output.shift + end + + # override + def setup + @qemu_serial = IO.popen(@qemu_cmd) + end + + def run_concrete_test + Timeout.timeout(MAX_WAIT_SECS) do + @test_output << @qemu_serial.read_nonblock(1024) while IO.select([@qemu_serial]) + end + rescue EOFError + @qemu_serial.close + @test_error = $CHILD_STATUS.to_i.zero? ? false : 'QEMU exit status != 0' + rescue Timeout::Error + @test_error = 'Timed out waiting for test' + rescue StandardError => e + @test_error = e.message + ensure + post_process_output + end +end diff --git a/common/tests/test.rb b/common/tests/test.rb new file mode 100644 index 00000000..b0f67f3a --- /dev/null +++ b/common/tests/test.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2019-2021 Andre Richter + +# Test base class. +class Test + INDENT = ' ' + + def initialize + # Template instance variables. + # @test_name + # @test_description + # @test_output + # @test_error + end + + private + + def print_border(content) + puts "#{INDENT}-------------------------------------------------------------------" + puts content + puts "#{INDENT}-------------------------------------------------------------------" + end + + def print_header + print_border("#{INDENT}🦀 #{@test_description}") + puts + end + + def print_footer_error(error) + puts + print_border("#{INDENT}❌ Failure: #{@test_name}: #{error}") + puts + puts + end + + def print_footer_success + puts + print_border("#{INDENT}✅ Success: #{@test_name}") + puts + puts + end + + # Expects @test_output the be an array of lines, without '\n' + def print_output + @test_output.each { |x| print "#{INDENT}#{x}\n" } + end + + # Template method. + def setup; end + + # Template method. + def cleanup; end + + # Template method. + def run_concrete_test + raise('Not implemented') + end + + public + + def run + setup + run_concrete_test + cleanup + + print_header + print_output + + exit_code = if @test_error + print_footer_error(@test_error) + false + else + print_footer_success + true + end + + exit(exit_code) + end +end diff --git a/devtool_completion.bash b/devtool_completion.bash index 3831425c..ce4c67b0 100755 --- a/devtool_completion.bash +++ b/devtool_completion.bash @@ -1,3 +1,3 @@ #!/usr/bin/env bash -complete -W "clean clippy copyright diff fmt fmt_check make make_xtra misspell ready_for_publish ready_for_publish_no_rust rubocop test_integration test_unit test_xtra update" devtool +complete -W "clean clippy copyright diff fmt fmt_check make make_xtra misspell ready_for_publish ready_for_publish_no_rust rubocop test test_boot test_integration test_unit test_xtra update" devtool diff --git a/doc/12_demo.gif b/doc/12_demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..8afa23c12f22988cedfbcae2712baf481f841221 GIT binary patch literal 224703 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

vTBgv@j3O&h&QCegG zrC!5s(*AChgOi|y1mCZ-bTofg47wZQ{W;fEMyzEeb~A!)(~4L)61SL49HjNXez18x zr3XKSn;M=P{PPy*t4qi}r92e4e3}la+sx>ZA?iNzg0OQV+TAA=x~jIlE&O%#9AeD7 z{vP0Wi@kFfG2?ya&Lyz6=m-fp*u{8F2UmFbRv|c+CfPvPfXg6%Z(Q+#K*t+N7&GEy|#F&=Y46wvK*TF=A*#5Eh;uNn}tBtvgaifM8ny+;T9>WAM0(6CXQ^NEo?X zJvHGCv^%DvK%Y7f4nZ-P!hR1<*g}E-kz7cJ$zGC{AwiC05PC$?Vip`4${tDu2UwVg z(*OWS%!&K&Br({C`ajOjlbnq*wpX}t>V&dpyblS$AHTw|OrTpefng({`w@^6 zT|^DkCwCj5rwtn!r6d=ntK}9CMBDEU%RvxQ%EM>^2elI6f*9$M5kWX>4P=KF-QsyM zLBUF+ask*psUSxjdGrm3GqNE?+eEvrP~L_xlg(55!hDAJw28S5AZy~SyJc5oLw}nw zhLwo%c{J(>hKf;F4q%JQKa6n2iwqeJFEMBQ@hjXs5V?O@Cc6pT6bWi*kHRvEYzcyj zvSCaw2)Zi?w?7KeT8@I$1AA5A=94~euEeJ5M5M1U{Xqo$eFP;I13N_dp7tU`p`wy_ zb@V>eF;NMuVjf)0Sfp)_cQ&*IF~$vU58PRv_dawTHt5eF)O{6X)&i)QtM8=+V1nO? zBuGgVa!E8%Ao|rrQv4)J-J}<%Bt!^%f{|82DL4uWF+>zUMO=>TQzWKRnF7x!NC7WQ zB$8ZW*ur97DzQ1Gd&v3EV8EvXMRi05PYT6RD0L$zA_?knv3(dqTmo&UNW^V&2Md=w zDo)U>X#*QIVK58^9s(LvVvUEXi>-qD6Yx)&akCZ>Pzky?LX*0po(!h?CYMfml^&EC z^^J$~{2KVGV);7=O7aGHrUDm{W`mhYgp~m0IRnf&&P17w2N$L%7RTi9qQ8O?DElZ} zaOAw@=yhkJxzq{cj?+D&KpYg}-l#Ot&GDDs-1%tm3>kFZj7p9D@VoYQco)Vmke;q7L_kKGcAb@EJ01Q>CYfyzVf;$Kt!i60XNa6dX*3rMrCj$ z_|?;fm<69BFd)%%L?WG!g5rcGo1r);kZfB8snUoGUr{TJm3~H+(j?&Z z3k-SD^hXo(9it~9fRL9CWPlXpd3jAr=iF@oL+k;1C9O9lc4HNwe&VuugcS8s%+OK} zZ1DY3CV78e!Ev6QwXC$7X_iLd2Sj!m1oW#ndR9TfXRm%!t8UAx&TOeh1e>x!*F00z zkPlTOidAW6m0}ar&hgjAI~Q^%I-yugYBqy3RzY-gaLSdb{aAGjBhC!oEK+4o)(h(T zUJ9iA#By3h4iw*v=!l8TgdO=xBsicI`&j>|AOZ;M!<-OX-I8Ui(qB)0Koi!(uJcpK z*^1ZGh0W0EZGEZZ_d-Yw9Q=R;lj|W@KTrhf;a=*K=w((Plpj>`Q5%e2RZ!qotNI4) zyE*P2d6d6?mpKQhoOCAof$sdVKQsP`h?iWU1(E-LM$A2!%!o0e4r0+KlXu zECt*pJxq3ZNxn#CYCWb)dE(OXTB7B`%`tOzIt|&Sx*q~{41H<5(iBZ7T@CAL_}48d zX7Fk_*6DMNQv+GOrv~Y2R4oaa<^YuAKGrL67BUocZh^q76IyKo{ER%*uWH>9#%^Q& z%pOP#XELVV%RX?xvn<6b=KzJtzAKAIU;OKr0zn@tEEPEqWG1n$k1vMGH5r0WQO{2W z`_XFXL=FM->V$!Rd1Jr$F0jBqe|1lxkj$V|evM+^w(BbuZw1(Q_h)zKez8IChpL-F ze|-SWDfBGG^pro_kHMAV7q(%(X8LqlU(k^-h(do)Kn@^-k{`&&R~=Mx`p-*np^X9n zU`V`XQ1FaII2J@3E4b*>Gt=3jVem^U7CTn~GhYGx=cJGnqwcSK9bo1cx|mW;3^@f4 zVL4-%C?2e&Mi=QP9($xZ+29{t`u!4=43k!wEl%~9FGks`_RQ7p_*;Hi*n_w&Q0@>A zYRflzmu&d>qLJW@k-v70DdJQIFUegM2^UelnIpf?*87g0M)F?+0%mze@0;2(ej;we zM%)I-GWsK|=w4+4&Y6_KI2xC{IZ!j>brQ@+lgLDOi`OLqxzyEa=RB z>QB&=59MT>J#ewxT6<&q{dM|N;xCSdUzqHjMjOLrF zE>8v+08)ReSUOINamXjA_qyJlP+5Fcm1M@i^m9~ipwP#dOI}nrGC1J^7&8dSI8rcH zb{}zWtNRLslnvcSX*dV=2AdGf?Nm$d`}DG*j5eB&zmASyoHn=P<=w0|J;apL{Q;2$ z#tE7i(Z_Kzo`Y`b=5`4aEwUYdBgv{L*|PKi*_h`!9p(*D<}O=!@$8q{71 z{Z?C_S4-7alndqs&*#@rXKMbe)%oF?4ocW$rUJ!=<Ko6h!|b0Djy)lJUVe@Z5I)3kW&D)5#OhBQ z9fp*Wt*Q#+0@w>jA z+^-;0UKj2KR;I0#o6QKCRUSj|1yLa7)(%)0%txXp==e!wlTGa64?W4|^U%(Zo;}6Q zy}uaiO`Pi>&osSu>N%J^pOJ1x#WyO`N>R^SM{T$6(UQ#>Wxk5JH@bC0WMYsI0R08wm=U zYWI+@V;h6L0s;M}h7dHA&d*lwpD01c7cEg2h(H~A`iP78Cs<(Wb2IP)^zj_U^*hM# zeF_cc$i?9pU976c(UKtN1e5&~`~-KiZ%oE<)E}8}4ibGOI*CX=rk31=HQ(Jb01udr z!x=20Z0n>2_VC)#KLnn;3-vBfiSMHCx<;ObUF@B0UR)S~p?h!_T*5H74(ID*?1ix_fnCucY({~%lDu}0 z1rJPlQy&AnF-6YH3RcuIDe{fb*ePl2KqyA%i_EL{5Vl@Ak()^LUi4YnMh>(fXpYPN z#EU+tON;EB^w-0drxY1NkiY$>O&FgdT>LW9~ zs~!R|#V50{-lf+4$bUvcK1MsVS{liuBL8}eMg9P0hbHJi*PE;U^y6GJSyzelK|Vea zOn<&fbfl>O#plE;6L05y6pd;cF^cK1So;_&@{p<#T?+Ln##lMyYju`!i(mZ3rO|U4 zWbK*+6P6i_ueXmiS%k?vy-=)h&=_)~M>IzD&EEdU`#Y|wPDuYAoPyOQH}xUb^>xlA z&l?EM3qTeQuI~^3nR5l2AvQou zb|nNPzD5kaQ5?KETmLpY|DLb&_hCD#RD}>ACZ^C8uubs<001QL9UdOG)e==G0OJMiqXG&Dr2*MB8Kk$=HLi9*Ho_@-0%m#FNd3Ar#B1!Kv5A`ef-&|JHXxIhhy z$CObSEj%~=Y2zhNv$qm@6bTkhIcc)mrw-$Oct+~d#S%r(hHU17v$bj^6Y;pj&k;>v z;RPWl&I%Hov)2}Ct)(XW6q;iT$3qp~w5^lv z>=)J=4_EXA4NIXF{B*Q+HqCb{+wrsz_ z2kQRC8)7#wK;CZGm^^0pC+7vfV);D}UA*VO1hV9I4lUM-45TRuJo!G~pZ^`nH-`AX zzr8^a*hHZasOm*wfRb!taJUxrVhE(6paB$`l6r9z`dB9(q?D_oLB@)rcsLe*ss;&` zSkG-ZJlV5>h-|I1dZ{%@roi$s12xSVg;%ldYlx4 z#zE%1PGzMS@0_wMo63^@hve`#M!MN?b%q>CGBHxC4yH(#2vhrD7aPTf=I&Y}-c1wrzKu z9e3<>tWMIgI?0}VGY4}t>;I2yz3-~CT2;?|UspZ0446P+5iBv}z zJStD){mD_h%-Kdkp#k%k>OD750Ay254e`1lOI!b-p3ey-Xx}_R(`(;4FD>ZMzGl_y z(D^&#OuU;}(@z3Jgvu2_A-#<5L@CUYI!ZaV={JE29vt$Qy7NYjg3QuAPYRcMQw)CW z9}IgjRd-k4DE(vWqREbC5gDQ_T**q|A7#W9C{4U^ltRE8)sFk}U#$<))jv$c*`+SX z55`A+j2|vA2BrL#{{MyPho4s0o_^any=1dN&1Rqo|WbW_gF5N%(LW#GKar= zSH*E!i=aghG5226P6gMY=RsB&@H-8Q;@C)iAz@vcusl>%LQoZs9*9_oFoJMD%S{nU zGjEU-wGaB`$(@+jIq-v*Cjig=s@kzVdUFq$=(JFf-olp}Z&+^m0i)soMfZ}|B0L-> zK(j?kwAnk9jS%VZY`nFkIl)SoG*8Duf>XFT5i%FAfN_x{(Vgmvcv(2Jvm>IsEJQR3 zZ};F>bh4BwIOzu*aOP?dPCi(i)aOBl0j)k+41(wYDpPJ2ojjFta~L}s4cI3~m13|S zira-Yr53nnXA5sSEschbx^Aj|Q8gh{RE#L6Xc|sQ7D5?coamkwhSxuX$Lv6lY$&t^>3==fNDiMfgq z9nK{FP$_-a$IRV4s-QOSCPauIgtT7|r<7HSJ4?e3an0#}{(6u>Nik*X4Z98!%s zab~f5i&AxENO6jE%R6?*5?FanK|-Q1n1vhGf4x_WjZG_qeBCp4##w)_BFn=17-v|- zXTY{FL0JSwfmp*47Dg~Qo8!vs*qCeA*{-4T4yY>mj~QeZGgjLwr5wI%_3E#7WQcj> zZEbdx%6zC=*0*uX)1UjKJw!xwLNJs=skgQ6Z5{P2xyUoM5$SJ*=0NCgHg@%^>Jk1p(hGgh@~&n=`j7C%`z6Q!Fi7K;yp`ua=r9x zYGPX(u-&MJ28a5Ve74eKKrZhol4^K=J@yl?=?_4jeJzd4ix+AjUia=4VSF%lV7vZ@ z7rkMg;MfQ_ItWSTt^}_ySZXd65$iA5&PtyZ zPv^$0lku+PBHu4ig#;cKGL(Tyq-*1$7XYl#`OW>QP7iX zXpMe>vx74uL<-Dlt2L2QM}4AVkGOFR5#otxygA7*%SVJEdbp3e*WTrM>o*9-dk~uC z2=v|DW)C*W{2{h8vGYh~<#z{H_e8WKpZXqrWKo5{rka{U5%dA5cu?H`tEOT_hSumE z;)p)rn$fv^%o_D-hv+zwwYGi2TbLB&vAvvx8Vx}?K9Ber=T?Hdb0#C>yO=)UUctL_ zuJRRfDhGJf=W^;;hUc(!EjTw2TcZLSP>c69DsIYs$x=Q4xi z;E}_-lICjPex4xfhWz$}_1}YBe*TGQe94gXcA^oq`J2pcf2{?iv`3>5ej z0cwNA2#M>H_ks1G4Mk5agwiQsmtoojcZ`~WR5mFk3<3+h!AwbzG9ZAmOpzfy;)7tW z3Tu+biNpY1SmbK?B&45P!`l0KOajgRTe*70b8|$y0)B8XWoCwUh$oi`YDdHfw zMNPvV?6hbp6xPphI?+h}!S*7+pOFD{0=UP;><7W0OF5#3)uKv%*)aaXzMG=kxgC;z z>GZ&XrBpCFb|Okqp|)m19CGRhP69t0Sf@Rz_M%`^(e^gDVe;@~3NFHZji?}Clx{RT zl{+(C8!DM@Ji2>n)M1~q!OwqH1uj5hebX2Y2_*8oB$oGKKlqWXE@%*A2dF7!_^n7h zkz$aWYmmpip07=u!a@{Qh*F5D1FbJnh$%<(e27%21XU7=S(91x9ahYq@XJ&sQ3RzZ z8a{cKgpLNmlO?GD3>#(?13bjg{tA|vDD(k2Q1WC6^tiFv$W62br zAw-FlCZqvRtAI*fvXN;!+g$lS?JQa6@1`R5B)9V-y>gI$#GS)}Yd~YW@9pv{L*Z>fGnw|Tlo)H2qW_> z;86nk4RH`(LL${{X4*tM7&0yu22wJF@}1&Nm6G4cEO|By2wN5*9xHd}k`v`e{sNSE zKbms6=;RW_?9p;#64Y2{jPk+*{0Y(AsnE=*g&dVW%LY^Z-nMeiWfD4GXs+bwgQ-%H zw^E_WgfCiA;2x~!AjJA|0s>s*UW&>+d8-|i_{B`k6A|SNy3B+9?6Z(0wvY-0GYKSg z_tJLSA62DfS(O_}B+lUJl;r8RkzjwfXk9vvfg^MZwgtvuh=Ki3lKq(`IVv@ZZmRn) zVJq~QJj$UaB2fYwaRORyUEj>|3`?!*(t+Ae6G9LhtkaLu#bspFBFF$|Y_@C$Y9&O> zuE{^nGVk$dH2F&HO&GpsVR<>V6d%#7$V*8@O?OPnU+HJ1<`1K2ao#l|A#cvgv0no`95Lo-K1dLMoOzF`*HBo_ZunJX>UuHW=IIMuI zgby_M3U4GNZwOskdFVck#ytr%@BF~C$O&C=LFbY{lV9N`noRu!&X}5DVk_mkD>ODM zIteRp&gygynDp&Xf|j+svdfSD*#USkPWw12z%U{M3U2%rI4ll~EA+!fDooqdaFdWD z1%Sev_OV1t(o1j;*gd2CLdp;vPn2sy7jt&R5r^nikbD8_&7^g3IX`dR;tUQMU zT8Jgu)SsyDO_I%I>wNm_V#`s6ztp?-7u(PPpi3a^m;kPKb+l&|EOt*Zu3>V_)nbXs z%BdS4Sk{VqRoz@iwcdF2MqAcK+u0gz@5%+4=JK1~ z#|w>`Wx+S4=!7fXouT3wFmT<$2(w&7q3Fa`unlxoq#%kr0*f2XAOHHh{SE$G~ zP(ggU?NA){?GW%}7{>2JVBP@MuI+H(_OviHrdl~(Hv|wmcplkkU`PsBKSpe-*37np zF1E9>R{4QgH1J_>d z0W*eb#_0NB$2blhmy1TmPhnYyupBYO*w}Vh*-iz+AOz?k(gx-ug8cbn;RA%K7)ma*TBBC zhSMh}A)7#I<5k#A(Yz`Y@C_U?zQT#oSQrX0NLsaV??;)-N=pqK3I!(Gici~dRLIjo zj6!Njm_(Qj!pP25Xm^n!$cFeaRN50j%QZnXC({6)%tZ^}6f?|~q~pt)V1KNbb2reb zYV(&|9GRk6rge9z11u#xVYIGkN!XzDykX2RqKqi43_Ogrt4_=oBV>OyQ3gbD*({_I zMCqKPKx3Q3P?;x58pP;=bKzL=oKQ8kDS8J(d1suOS6GjJX`Q-gon`BUUZTrXS3}PB zIF-!SO9itxcak?({y`n#tw%29!lXTlv729829EJw1^P^(*r z@vCQAWM{|ihl1Cz=&P0_Q-?V$1=$&>pC6~eyEghK7D=B+7FM-ca~4cs;@@CSj*YA| zq>rru4f-Uu)-UE7``;eW&H)wQ4N*?aNxqMAo_}4EOOn6WH(VU6e@~x1X74KZ(1Wf^ zm|6pVPYpyBhKn#Wp~-QD8LJs;@;qs#8E!L^9v7#NsT!=XK+Y66?BcK|pYN zAH6+tZLO4*#S@N;F$-~3ae83#vchY$SR5JZ#Z?;Dg^~3480XvHAueOv3Ft*+J|-?` zg)X@bF7Ra%$TG0JK>FNmRb4HDiB0Z%(@tNoF-#Eggwbs#*{#%xWtL4?98eF;WYmxU zE)U_pNc0Z0xvoNDcGpA}BIoT&2ReQ{BjH2ELMe0tBjcrEYQ=d}yGh}b@!&&bJ@o3( z0^htr5iv!=<`iCwL$c12kb&57t=qBU<;jLMu;C*DOT&|iPxn@mQ6WP~a{mB(Tt@eh z>-;*pAA-6bA{pJ0pQALQ;Fqdhu~DN)*OCZ%JN@<_!n549SWC1?JIUtHm1gfsGj63; zZJSMgfBWWP3X0|+bxVeGVJ5w^xpjS2b>)Udx-RxOY4$K&b2xw+c2c=zOnRaSdax~o zGm3IG{$}a+Gt;@TfrVGB8EwF5tK&2NPg=4U<4=$4jX`#Tt9bWkii0aJjZQz1{XeBR zR9bydC>~`180mcUgcNB0;Kj>V=AQcRlTOfsu8ezv%&m`%mz9@y@-##q0r+ENCO{-i zq;aJkEF?R4FDChk^EYB?wS~e#oTsj54DeaL6E?6j9{rcQA0Oh*FiyOhH0|BJl&DF` zc|lC$ONq@ruU8>oW?b2~x6K>kN|oq3)%80-z}JUQTVEto2v|xQW+F)XO#0z`07M}Tr@Ta zAvs{PGho{(V8@z~1sowHEzSN0Hju43w~A3A>8FHRgz3db+S$k0OTZp(;I~dj?Vv#a zw9jY<@AbySi^h+wiNHz0Z@CZ=!E#fcO)xeP#Y1?j#(1*Y)zCp&(cz|;E@YWzV9_q1 zH+T*3Ju(=M!^_uy6$+TO_Mb0fZsurU&^&2$O8R(nOXJC@$wW-HsbB4!7pw#}A7?07 z%{BKB2<)jW0l%v{C3biX8l_x5k307mh>f2?Zv&5u*z(xSw(V3|Tp42sH$U!a=kPUcpDwSarHZM8(K)I<1lOP%ru)EzxLRocCdDGfUDo>-KRX+P5F9RV zr#)_q%>1B65@&n3>xIjQZ(kUh-Ry#MSU<59vG7iT!3jvl= zC|}0V`EAF3tQ)lJQZvbm={oL1sratevqY5+M+?L6dWe=Z;E8;h(nqv_mnt4aU%$L9cKNEY z%G=jOF>4QrJ$mRZl0!>nY0+Papt6^u_vAzrVw5ngOihcSR9%D zGUEMR@Z=l@?PVT-vuRVV2Pc|pG`C?P$IUR7reS*(t>(Je{zQ-`?<+@}g3nMx(f})m z7XCxc8-~El{}npRB%HqF*iD9MR2iER!ty|s-nH^lBuKboh<5zIk|yVSOZKt_?24=k z(RsqJA?Cox{nVya90T173#IRh<(WMe*xI{3ZT5{+AR-{}PEOE1a zuQvZQp691wv~~gTmt!QWqjg~dj@{r}x?_bnCaF&uz;O+|hoG@Rg~NN$GsbB9kYE^S z==V5WnNVR<#z+3YrrE;KN8y;8f3@Q3U?GeCGDhluSYT(oI)Y-g{@9LOHqD89yA2iErpM= zA{)Q#$7FJ43cvP=-@{?)TB-;Kz8Nr^FhBN(Vn1m)k{?D>YL~Vb6vFrd9ZQEQ&7%_n zRnHR{KSOPuO;x2MNeLsC1#R&phgQLacp%X5uMZWl8IXRiZ-;AOz8?91E9ky&RdDJ$ zr@*~N!9mtZ{AU1n`JH5%$( z2tXx~&9$IA)md2^}#Ei;;AK1K$e=lJ^JjO3D3S{LYOP zYX6F@M$tjO@E&^1)EHs}b+^Ip6HeyQhdF77C-hiD-1soT-IJ zu^R?z0GuS4g=n-SlhOi?Oy+p{5p)SA)iY6L|0Z^e2Q2uu>EOBq``tp4WuMXY%v!Gk zU7~x~rE$XmE`VUj$@`vjlAu5y- zu*LW_`E-^XhGp;`x;|UXxU)sq?Ol>avVNgIS5wItwzo_OwU+M^swl9rC}#Z!KWlnI zoloXI2j_o0{GWb{KT$mFPyS|-@nP!2=_BTaiOJN_Gt&KFC|tz#%20|Q3Dfo!@JZ1C zsZphhm$R^pL_IYWvxhJcr>)>`{CScdXqLy`Z=X+I$)9?%6(n`ljGCcKVMN#ga>PU$ zRyWLkmTDS$>KYSF?Oq%=l}&lV z%+sE2t1iS7tYcI^qq&ZFW6KOzd@`%4;5_9VAn$?~HGKEOvpnBvZ3!2dro)C$G!_eW zSi%X*v=HLbfdajC`y}V6+h)$9;`Nq16h_9_9CV%`xNJAo~6=TO*Z20(f4*Zkq+ zD#mAYs{UG{zRE6Mx6O17qa*wzHIs`zvPGxz%8sY%7W^T&_tx<`fVA+}^t+}>q6hwf zNrjWdG`-<70YP_kU}7+M-8K??=FGV*tIN;JGueIIUo1lz>WR&5Qwc@Lj%H59+``{* zXkUB1&t#I;kb>EDI%i_e9|BJe8D-%sC(|h*^>$rgIHhOC*fN^s*shKpmQVQ7oHva44@*F`-I{_dOzMErw_v6#J_9IBljXzOYhl#&r1y!S z^&Xm0_KaXQ%s_I|^)%Nyf=_PFm@&B~tY6nH0L9f(_>8P>4e-ycw63+>5?M(P8qGSM z=>K3^wHEk8upm$2UP(=S?ydK>WO%nxhdocFtm8WqIo_5QqTNP=E`YnK&4Kab*C?|h zN7W6PC8n|Lg5#$J-9Cv;S@aThXm)jgKAW%)otbkNL%!R?b>@KA(}~EyxzmtOS5#s- z>4$HJPkz{!TBzc(tv|X5B$I9b&oXUxn-IThhqq;1#8|jYMdjqm*K49ysbTTSOPGrI z$EqPEHkqD=o@~0OVBtAxEwze+5^@`OcVg0oR;c!qqNusxW&aMR9 zdk0nR-RA#pUwK^l{o%8F@R7gWzjJyV+c5d9-K0>#=67+Hu^^xE0ayVrd{;I_-DgAV zBxQ2P!+6VJis0soGRg`<`gcFU2T?1?kRb*! z91S{;e?Qz7bX6A_;}M=FBO$&agEehI59{keC8_Kcp#;hwKbX9Spc^1b5OaaUz=0Ds zL2$o}kX#fJc%lu%n;uJ=zSS9XwiR509RT1c$O$MY7!FX>OQ^J6nJgJY)1Msohz=0>aL z$r)mNhjVPD%a4jS2^m}vvzF~RHY;0ekMZk1IydR-$Jm7`Ss~VG#vMBddCWy_HYNt_ z;Ck*7@LN)k*IhY+V&7JR;a#h z$bB&}t~3Tm{Rqjdc2_)@&}W#?>Y>msr9qlFu#2(*uAr%U0*>oQLi$LOW=>v!N^&(s zauZT&(dbAD1DZj+0ghanO*L>MYOoG!_7Zi#L`CihjQb|6YG=Lc7#h}jm_w0Ti)I8r z`Vh{x?5=npE~`{Tr~pS#f^s=b@I~GrHC(|YGi}mxm_J5;{F_W5m29j9pdeiH^O!@od=$w- zOy5F~2y|k=LX(|!B*0=}OS3oAoiBr5VL(G;$WfuvbfmaSwF#bS3$y!BcC;N(bSX(A z8%YI7rNXQ!{qb_*q+a1XUZL1irUZboelw@SKed3Vq%1zducFXHJM|rla9^c#Jt=Ry zMaMdav_Eq&?b5HwMsnjUPH~QhK0N z@^^=SGpn$QUGbwiR_C$&8>34&fM+TiI@=#^&q2ExB1+o?&Azv*KdL({l;?>-D&B{u z%?1@|IbI{I9_FYJEnL!;AnKpE26tHo>@rm^I@70$z*?b0zoTkEDyP6Iw8Fmaw6ivuBL56A&-B>~J6`Et-~QcZ{wS z(=Vk=F54=N)K&9*CtijIZ(-Wu9oFX4-`##GQ=zf+6UY7DK`EGu1{sqVQEzAnh>Sd; z6ET!vEE_^EQ7=A`D!ra8mA)toMT6}U7X*uc*8Fak2+mX%)~Aa{pk3w+W(gK!-o=z0 zOYyy&O`2Ku%VZH1L;?eoTvJWKKydxNy5J&Whsuc}a&Jt+q-WPpzA)*;gvds2g>D+$ zs509$m$D;KF@~sWPFox0Hy;lT|Ke;N#VRBC_Zj^z-HN5VED*r^v4JGe46V4OIL8<-JY3^9DK$2hAQXDC^$ z%g?WCw6dY)s~JTp8k(~oUS$e8W`N&c`}zI&23S{j!PGSXHJL#&YIQM6fVViMSJZOq zd&l@&@oF%2Y%1l?EXidZ0~6?&N`KdGNWftZ$KfEY_y_sLfU803TVeatGOsBZLHr2m ztLT0R!hEdZXR)NPY~sCrF!A7U!60OEXd$qNVM}fB`d3`U>XV#~IQ$>_AG`?5z-N83a5q^u=vHB)9 zp6w|eT{dHhD(jLC_a`xLu>L-AH`oRDRP4I>1&e zlN4t#c~^%g_CFb_U{E$wv)H2!LjPtlTgQIRhfJyrD{8ifFO&Nsc-X`a$D7N=z zi`1(M%_{-aocp8tL(D|hoJ>YA9P&`Uxdkf)UBt!Vgi(9B9&`EzjWZm}59>(Z1+UZv z#*Phk4G%4|8SNkP=S8D1p0h4fBs8Qw_8JXXZ=-k%(^{xK0jPNoio>tCsjlYo39rRC zm<^aW2PFcVH0i?Jme??M`#yIICvihrnh%C~^NAt>dUAoEz97nlqV7!GCOY@@ZP{?W zDeQ1bO>ZH&zcuZecB8aXTj2m#9)NPq?M)#x}*7PjXTUn#OlV(>jqD8H@tX7 zJl`-CijL~Et3eSHb0Wb+mjx&2I{&r1{ah}!K?EvsJ%3#$1<;IVBrA+ih%k;gu`akO z1R`&1ewWx8^C522j1-~<}m|939(bDk7#D_)5A~& zO9qC=pqJZl+GBGQTmG}XrDPxd$BHU}j2Qk@GZE>6K1i2U`Vkt_gMg5mi?X@m%X!2$ zT0VHEHo{)JseeW=%EA+7!bS}VzlGZt_Dl@?F41B)We%jXIjA##wd#A0_3EAP~{Ef|n)DsoH!wlE+?;W=fx#}JdPs3-nw3ZDm z;j=F-5+>P`32Vm`2d|dFqg$AVdu^xnGxv>XNAc?8>%pU+-xZn$ua`S_APCakugC}F z>gnaMOyl~~CdKwtGR;g3h~^zs`i!W+qYk19B8BnmWNo109j&Uf5@DY6?C>s9Van_D zYJBvMdzKmvZ&la(BmUbH*Tn-H1Nwcv_nIWm4dmIn-9j4Y=m_E6AOv#_=R+fU+@9P-ljJTLF*|w6e?tQ!j-cdW)%!px0UXp@F?RJoDfuF zsiql|Ka<*5_5E>((&Q?Tyqo@0Ot0!GtLz`?K=;xOQ5F<#?T#|MUZdTliq5eZfcPTwTYY?tH=r79UKU}z(ut`3nfRAb<5V~c%M}13qkz&nKafJmM{;#tRku$oKeKl8TP2giT-_$|fuidM| z#zHE-karrfw^NKv&?Hu6dnfBmXnyO_No^3_Le@3AA4X@UP`qD^>0k8KM=S)`evq?# zuuB^$ndco35`~UQ$H%x6e|ZjRY$>Swq!&ugAR4NOYsL`V0NV&Rt*s%RHCNi`f;V4T z#yqD-QX%uR(tl72SiJ;qzWV;KUNkV-R4%~c+xiC@Wn8!+z7Mv6&EjlPLJQ^Fa@Zm9 z&&?Br`=sOhLu}{=0gV>)7mrlaC4c-_vj#!UV03=&Ydn}qMv8&9XJ5S>>tk??rC$Hy;oO^dT zlE!M6PCL|iHCvt8xEKAk-U{iAlHcg%eS>>_dHy2&QWa+kF_azz zELH5E71**HII|y7;$78WM~Qp8_O%Tk8NHZd4NujtsjjTLVC_AB*Ffs-3%^q)d2exJ z8^8t6f*;Hir?92^p$bxup^k=@ZVxMp%^k@%;(979{k1PPuV^e}l` zN#Gy^HIb(M-Bu!#g-9u-f$L?IBp{I$3=vfb`GqOv^*2bnHE74F%XA_5~ zE^xqJgloT`r2Q!3Qd7gqW;88LBoek~+?rX*U_E_rwnC6$%$+Rotx+>=aVJgBH1?w; zCXW(b%PwJAy#XE9&Ya@;$Hd?pXe0_u9b8TjQY|mXlsIm9;2K9DB;qm#9mGH$sGud< zn)ahqpjQ0Arf%ja8V(KreZf#x!ycl^XOtenaGJ`ik>Xd`GzKfeItv?_=u0d6t z@I3rvSkv`UZ8cce;yaER>Jps`U)|$dlnef=2)&}hMT-pStm%~0v#Pa=$o6Ftnv?l5 z`(L3|8zAM9`zH~DUC4~I!BzBTprB*(5H{VR7j=Z%Z(mG?_o9&vPr~a-D-C3$rizZb z`%T@Yv%=);R-vm*4xK*NR4wf+(?l_yHFFD5oNkUxdOxD?A&N3aL!FxueJyF2+R>KY zw5My&m(rjYzN6jW7nM0B)qxl#r{Ao$<2}6rVeKbFX~xfstr)u;E{_&0&Jp@iTUGVF zjtKS>F>C8QyYzYeF0XgieV0&9Dk+CcmsqZCWn$Ejiqla`U^kc_UDF&u&W(6cXGFhk zYb$mEa9x2#j8k1yzt;t0OK)e4pdtx>RyC9VP8j-3Q{O@o&fJi<{-&pjKr!79GFu($ zKeD$L{)Z18%F~9C0UMIRt|0ubmXMK#>^X@EykjuOfiNV=k_}=@y&x zi4g*NWNm!-Hz&cQ+B5J&ES|;I8Z@<|bHnp#uX+^H8%!{`XJzOw1k}OOG)ZZ%rtG-| zb-gj#Sfz-Gyj2i4@W^$#JsnueEXw5KzhhD2&s15wFX;tBj2GhJL18VVfFd)$U|)*a zy79UO8f(LzWLkevP>IBl{no!CSE_$+ICAhRjl+9Z#~e+Oa!|vKaRQOFSfR<7Oc4zD zgs22<#1*zx!DJtNJ*sUc7fY2+l8eyNk8Y^O*-0+Pt8xmU>q^Ybdc1<+;*~t_j7Pnz z#Rs5Zobp`pmd<3gyAkyIFI>e3;$;oTq_q-{#PPqOSs5`akTC(j=(qIzG*BBP2YV3= zn$2-;U+xe_sTO$9P?FtED9&OQiDEJwAjto4t_?U|>~<$y!KRO$2xvoK6OGJ=&RJ~x zN*VxA`){25qloSfD=lcFlpvr@v4j@+k3`ZQ z7bXrYyL2r%MqTrfHbw4kTx<#iB?>0*nOU6ES|Ev zc4VO5R7XmtApxxl|7;<_ouX6DR>BicmnqjD^+JJ_+Hof+k89La%$`%tw{8QqL;h8; ze=F%Uyo$T>m80h)_%&w{#U8Y-)%V+{3{joU8%lp^TmwMc$>YDfi9MWfQ3AGh=3uJK z7X?~!iLdw$1uGLipPec-cJ99LT??fT`n<$zD)T%GA1K}tS%dmtaSJq zTl41klf|CzGzk*QK33wsH}L(J%E$Wr2O9+3xG8B-$iajCuxyMOTgmOg9Ox4%&~upd zm9Svnxo-BGVq2L%n+63w- zIdPjU)KfIMjcLvpYAI`KvnFBQ`Fj2d4GIvl$%e-M6bdOYvLeq?aj*jUP3DF`- zFi2bZK1Av&&lCvpfVQx?M~<<}TEKOSVeLq!%8sp*fK6$QdJjd)5&}7Lh+Uh2DMEqB zV}`jizFWX7Ty>5d*}VA#PN>|7DZ-AG9f}1)596m>zzAZ;!KP3VhdD5zz{|YFjdNib zL??7ZfJ{JVCV|-m3x*XCBd$}rkY>lhy`=z@AW>bCiYLj_MRU(243LeX*~Q{uFZ4Y? z9ERHS0ZT|~FVN~8Nc_J9l@twd7C>({fAuM{#+wtaMd}v>baHiyR@NWCDDlLaH>I1g z$j{X1OmJ*nuz&xhB;}yUro3c2B;Zve#2%($9)YTW!uc1RV9~NEzDOd>PBiaARKI{b zuSVNxN7;kkyu6^Ms7|2KLhw3F>&8J>IF6~Jj#a&YRg6yms78-FOs#=VV+3TZ=U~!t zB`m(%Vi-Yd9m1fz#ixvBR$F9lkHi5%&eA8sF{7(7n}!k|a|pI!(4{n>lQuJHxdx_k zVB8PkC^pTH(~z?c#n-s9YAsSXHNKKuu<~Fs334(9aS(nXGc1Za=?+-j1&5)dmp9mBj%q|>^ zoS~by#D~qKC+y(R;{0lNggN)a7{kQ-=+xayq;0n(_b#MHb_m#b` zb>oXf#7idyu3@@mh|tTy127sC`nxIger20A6(`#O zaF6m=%U0o5EV__t*y(C64{DSv5!z&Gg{EqRTH4f)ctKb2zpw~5#YhK3G%H0V1k`D4 z(1v5l3E%eDDTdg1?q zBiq0BO{If>%LJQ^;o zhWy6DvintmhFzF?LP_xT^_*G zjTtA7bt#Tb#Df(mDb4-MPJD+%IG|~#WBOBC9sK7?v`L4ppSJxsGxgI)yL=v#pbkFi zLC4n)*EI{C_f#H&>;2&b5%dMpvtrc1ajl393+qQ}AuzNYP2v}D8Lu>sIvl$Q54&ET z0#$P20dPERaP+LnZGbBICyz7iGXX*-ZtbH^Ylg0vy_es!7q69%gO@IJ2OAsY7?G)7 zZaQ8;I|~0AYrAsYI2o2_i1Z`4%nx1*2W%blb{lM}lb2$A3wzNk9d%`Qaz&nyz2jqT zMEu)o6n6386t(^*v|wyAvFBt6Ja}CcKIx6K7P`IuK^yb5adGe4(3EyS!-Whr7zK+R z6jv69^h-Ku;5IB@CHxt8z!&ORvw=e%kF&=-G^m6{bCB@1Ce9`q=Ht({Vg`&+G`AUE zOPdX;89Bhl*(&%Y(poph0WYdBGW$g*wl>U`+FOYZ&zS;`vt?Z+T{?0|R-v~l?(Z|s zWR`x|dR!KC_;XV@@=-jwWpu86jLW#%=*yaoH+ex;cxFcQ{BX1bWz;vQXw0({`>sTO zz6AdD7)rP(HNMbI?}$g;lXh+cH-OH*65p1IMTzq1i1 zNiC^z46P20u=q;@=^_8or||VP?uiXib>wilqmcBNkuN15wbUKnw-~>tnE0)jT%d&7 zwE`oe?*&VG1qU(BC>psyHCnV>S=w>rD0I{&TuqfI4Ipt{(%FoB@@ zAAi-bZcTGfP5)c%kU-r}-*UABJpbUDF@bvWF}>wq8lqD5`DKRU}#mopDyWl@cP5ICUn7OY{F*P-X`4MLc*=4Q(gN(x)#!ZE$o7=+=4g2 zy}S&S7GA;zYEiU(CU{Z7HdMccQxo_tyAozAf*lx0o#xD;GsbsvE>*!!55G>It$YW< zPQP5lpkCgVoEEV+9LL3e^dE2n7B2*x+8r!VSN@{M5+v!xUo`5()I@cW3E&?xR3G^j<7T%glU zN@z^We@w4$%;{)3c2 zc#%3_k-mSC`D1bEZGk;tiNAkI_+v>-c$r*kNveNY>0?<7UwE@TV6(b^ zlaY9{UU;iLV5_@-Ym#WIUwC^gV0*HE+nQ*5R(NM6U}vL$CzEhzM|k%*VE4R#*M(sB zO8ECGP=8oRC~z?FFE%I?7&sKv|I$Rkz^K77z^K0#82<+v zS~L{)|AK~ckp1uX|L2eY&xKMjKJXT(WAOiAO2Ofd25qy4!Y1dc(16Y+Lt!wOEcRz; zW|9$DjQVL==wF4_X+Ep>F7!iS6q=MeRrcbMd>Wh)j|INlfkHBa^>Ck!2CZ^7lI`6k z9T$~q0<#~?j;$vBVvc0?iQ(H)wM?~4qjo~iqs3Y-bv;i0n|sw-li@;vQclK|ewEW~ zsD~h~qfs|tW!GVI)zP>o=tF&l5bv+aU=(!bF=6Jdaf?1YEl(fb-FjW-nAysEdz1Nm z2Jdq@5x$G%ObKDswa}~EpA}!ht~Z%po_;=U%e9V#-qPzGkmG3qvSF9gL2rWfdw$=u z>rr16B!i zp`U=@f1luZh2D@YyoG*OLzzYX+Y35-fuP4{lVA)M*`f#-p6I<01Xm!L4~iLFaWI8C zUP%;gNfvoD;rI38IMNd@ig^0Pu9A4>mY0%5_B&q6ByI?wFA(WLS80l{^b2LG7zck@ zsuYG#SsGGm)j>42v8`E}s`7?qs`h5|ajGVd-pLPB1b&)KTY>J1OoxJ3nk>E8Zt6Ub z8Xwwxi$L3oLYtHGlY*de__MqS1P0pTkSE)-(pZuyx{`zzeEPCP;~LwFJpE1jg2KD= z^Qxj$hVRu?G#kkX+8$JBwGBNW1iL!?S@}OT^;0=^G99~b7Y*$Y3Tll57=E?QZ4`vH zEj@3swXI0()3r_W;)ua*L;8Nq?F+8|YCG0Z{#vW=)Tmiz3#luVRY&} zpCe@LpBqrP?tfp6b83Ei+j1HJ$9(_W3=Qe;+yYP8chhqCo$+=QOUM3p49~jmcAUuT z@^*qOis=qWm1%!BNmp5SH^tO>c{j~A!E`^vwPt@m%XeCLKPU8jc|R`-%lz<564&A3 zzsvQDio913OR6%=kIR}m4v#Ck*7cA7!IVC(nM5%^ty^R|JZ;!i)<139cV0bhIZrS@ zZ@aHKJnwj))<5t1KZCBGe+R>|yzGVJI=<{j(>A;u#PeRi945=KydI_LIKCcdSvS0% z$KtDb>H*#zrTa9tnW9Y zxK8i4K-$LlJ5E?~Sjw_wiV4X8$$22DnC#&D)3(>&k5dLkF$DgjZHYOa_;|8kOvk^? z0hD6?tf2RYHI)~{__n{FJOP@`XwX{g<2>7^1K)sR5Liux5V*iWm;xo}c(S5btA(id z$$jX9KVe|hvWfK339xyC(!8I^qOdtP;dD#|L{w)3dNafR8p)!Vz}q-IB-PNL(ZAQ| zg1_pqrNp~%p>Lo$=@8VHU@IvhGlWdwD2;kN7E|8@PoN8`^k6L3&n|LA5LrkxxF}>I z9SqO1X;NU>A>nB9C{`q}72U!F{i*1`0NFq$zl}Sv;Ki9y;0cDH#WS)I#1dZY1hUA+ zI-0;nmjP0cgmj`J7s*J01fhJ>ke~o_$dDp1LW?-87zl3B4&cP`AK{3^3+kvc3ZNm5 z{-}n&?C1npjAJ*0EM&!GK#hOIGG?^2Wg;8N%U(M0kx`@}7FJaRDh88c6zIS$?2!*$ zfC3b%aAh3xafuiB%mabw#VXQ3G!M{0A92*jH+V4$OEiI$4awy#VW!Sq`tqId{N4-n zM?i8gf)P8gCIHfK|2=xTj~srq76nSMOyQ_c1w8149O?M87ie#29!SJLn7Pe`tTI;Z zlqJhD8qbb;RCyABA`VGWi(33*2NJLXIchw({jy)mby z7%Ioys9NII#<=;-f;9T;kACzc9Dh&>2|A%++GyAx-hdT}O?={EGGxXzo+v7FjLzs9 z`N)C`pa2z71PX+d9J#0k3sj&|mO?;#5=cXJtAK%bm6fJv&2Jn2(T`)a^{w=E?_2NN z*8Tot{~p)ig(_&V;(zEv7zB1RoaIdCaN?OE9v}oI20e*aI=CYResi4XT<1F%5(9KF zusHZ!j90R`Vk5}qIo!E13b29D7!HRg22E;7AOWppz_33Is|i+Q!yo*JhZ}5?fkDJ^ z9?S(rC{)4QT*7~lw!v*%oG}stXGRDI>`QA66 z6XEaPjDP|qR6!23@sC4iB;foexIO+okfEM8b z|7mBgXvxf19P!{LK!rW((TrK402%*~M~u~n@)cM?p9e3Y%42o&obMa}J^wkhm(Fy- zrQsLkn}rW-Sj86vL8a3m2NmvFK6~DiebwE@OUZG-fFhcpV4f8Pj==DLs~N8Ke*2}7 zzF&R!L^ttvZ{4SH_u1sVBMWbMV-dagzW?LzUt9#4r=dhK)0LF9UadcT2;*j3Z0H@y zKqKt2k61K5y*R+R+XW)pe|V$GxM~&VJ5u`72b5JsZo2GeKej(~z1)|W;w-S}&wt+D z+~$^|So7CGXSG`eJ_x}bCXtQ9$HLxlmn+BfTlj{gl^^)7K3egh)(%r7@D8U)|G;~4 z{SM=R8urk+^dDa_4#?cj*GR>vSs{&o_(RsQ2S7WCj`KNISn>Iw2>R3l=qC?~fO5{V zf3ZS<>34u;M|%($fp9ZJ=yL|_b3U^Gby2i|1XM!;#0XVj1%2=bau7qe@@2bW4`g(B zxX^e=M{nylcjMO(BVY#Y;14xNeK*K=G-!i3Xcgc$cn_z20#SoEn1dq_1+TOY>jr&u zH!(AX4~lR^2uA@i_YX9ee+rlpBftcm)D49YhGJLQi1*+^yJt*M^-#AmQ>zsYkYIN6{}p$RCxb*t zcRSc-l~`Ys$PhvJV3Rn59%m1!)GPMoecCbtICUF4H3Bbi1Q_-XXHZl~Wpar2aVF+N za8(O70Rjp&QT%WXt&j!?*b#8p5xQt&z4(j4h={QV6$PNkgOq4@^`(yPm=T*uc<7j4@W_Mj)eM$5 zillUk z%=nQY88s?(J{wd%8F&xVNR1L?UM|!JXFzUMw=3<|WEgNt_D~H${}538XpWW`kM_8Z zJ;{zg>5~mHk3(3GNOzC_b4sBYgQU1#%QTR-LIE7`3I8C4mNzj5nO`3VUs<^>^+f>~ z@C1$^4W0B4@DK@zl};FWf?`RQW=R`qxt1YWmv%`sK|n>M&;)1D2QFl8hNzPDM2PqF ziYh>Ev2uJ1|6y^(>0?D01^g9$JK2sx znUti-o^sWh6hVOH8Je8=o(u>NF=i9%IWcKfW<4F&_?{F|0t5;nJUW!@$eLy34GJb^WcHNq!ixX!4sPH_9ybm+ z)&vhIF*sGDvGQU5U=6orF-bUcs-zTIYNHbqru8tU9J;1#x-#IIm-}Nw(TJTLn1OvD z2exNEW#=p6$q?sf4^Ub{BLJEHzzrW7iI?gxt1V5Las1UQFy7Xz96pbWAI02?3%r*?3G*ALN<2QE0P zs45U9APRNhTmY~FjX-8*HWF6(s_ex6 zctgK11u*#w&eEdpmR5I0gG_*7_AqIxpbERte3B@di`t3u34Ll6XLL4tm)e;s0BQWy z53BWr5_52(MR}>P39hDA)ldYqS*|u{f8#V~*)V$lnraC-v9u7cs5lXs6&nW(@v$L0vL!2*C~K`Y|9i6<^GNmtL6Q`h)JP6gS3nYgm>$HZ zW@Rfcz;dH^Ef_Ed3^%Ze8lUgkuRh8U=>-CxAh0zyobdUM@0ng0@CF3B8~cC_qiQiK zKmlcdenJ*bUdk~XKx3Z7wbw!cDt4Ck=MT);R;OfQbLlE^8(|a0wWcJvyYaU;o4ATQ zF@F$M*+xFP2bkD73-{!gFeCvH#0XbfUybQ*FS>d9;A`%6N0T58>R?8jWeS$SO*ksG zJ8G|0yDBRX2)vaJ<&Z|QNVS+b0v^Byn_vp%lm>Xv4rxX)LB|6k*AL=A45IX~0+j)F z5L@=359Oc>mC&mrun-<#2lUrl^8gLH|DZ>23+H47=5tOyfCbgldr`Cv(^CX0ptIZ=qKtq*0^=&OL;)VD z0ta_dA~UTISB0s?dGe+)LuFA1_c1SEc_WNyuO}Mj*wsatTf{c8z~Uoz^#rsB z)DtSu1O~Lid$R&{AY(LU59d$?Pn^bTJUFv51=(X2-Dw49UEvOLSQT+6n6%eb7&y1dK0+{?cF%fKAW z!aU5xT+GIN%*dR~%Dl|X+|17W%+MUo(mc)7T+P;e&DfmH+Pux&+|Azn&EOo);ylje zT+Zfv&gh)Z>b%bE+|KTNIGHgTu5lWiAsO^6&#++{rJ)%6Od9d58mPe<_YBbWtk3`a z7y)h1{XEYLozMJy(4djf2AvuRUC<9r(GhLY{*2MDA<-8t&<*X;8x7JB-O(D&&?P<4 z6^+j#jnW*g(izRtC+*N9|EmDjEz>|v&_TV@LS597G9gG^B1x?! zOKm4jttU@CC{evAQ+?D*ebr2j)lRL|P|ej+?bTEb)>fU>WWCj8-PLFP)oC5pYdzLx z{nls=*J>@-Y)#j09oKk0*Lhvndwth>{nvaA*nS<@ggw}XjVUdH*r}2zp~Bdv;@FE_ zDUnSnlszeyUDb+xDw+-1n=RR$UD=quDUa>hoL$-N~)p%T3+U|Lxt=&E3xp-r^$Nc<2>Hu zKK|oC9^^tkqp5|)4=4{^PZvN(Q9_MmC=X74@c7Er0p67bL=X~Dhe*Wix9_WHT z=!9PAhJNUX|DNcIzUYkJ=#KvAkRIuhKIxQR>6U)!n4amDya86QxtiX~BY;P$Pzl1C zxU9KasA(~FYYgqsp0x5thUB__Fw(c{yUK_fe5y0;2 zKQmuUinFIa9jRV1`W3rd3P;l+>u|$EJ_JN`$uUVpHp5OE#SS&yel_DxE7<-+LFBx| zbMD%%?lAN1f_H*FgznouG|EoM%y{bXUNZ6l!>u&NB!RKdJqgx`id-b z0>RMg_&zdjMD728pdNEbNd-15KnR=A@sqHdLMSyD&uLLJ@^v`zpJef#v&nkZN3=2m zs+v=Z|6mgx%&G&CtjZvSD8Eo*5k_M)b7!Q!teY|$-|?I9@$nujJ74h?YBc@1xTpSa z*^6|H%&1DwDnXY_vtjKiGXg{K@Q%7Og=>Fm+A&5IHVCM23>WTAbM{i>_K4QEP1!jV z&`PgV8yQa#UQeL@fR$)vYc{_Dcp$K@C|7b{_CLyCDfmhG;BqOG_D1<&ho8wX^Yn^) z>JS3$1n(>(pc`C&GGWWMVZSrjq)kI__Oa;hSrhqD1NTtl`WaFB+vGXk1WtA~PUVD~ zu3`cYPa6TaQ#D@-@AyjaAb_!-O|$PRIF%0xt83Ead@jTKaftk<@AT8BznDMV1CNyJ z|F!iDZ!*dW3lS+{uI@7q6*90NHq!qz>EHIL4=v~4IT1Bc`A|{&0=S>h4)b8P_;3jT zK>@%(41?_LD>V?n2q(x6#(FVA;Y0v0Bv7PS;?}=-aZ~_Mp@K)JfB)W{k%&>{N|r4L z=Fs-fToIQFE!M=DQ|Hc!6x!J9C+s6nqD74!MVeIUQl?FvBGvHeA3cvvDQIcuZ`x3z z9bB0_^OU6tSnBNk3peSXR<2^*zMUw8+P`&8Qo_ZH7p6>_dHvRuOV{pS!Yl*Vy<1rE zV#X>l;Hl@YTOSIESz@4buvH^_`4n+2crf8W6R7_2Q-;LyPL2LLP8@M!BR)lq{{ifs zX4~}T_EAX5oyx7<`o}QS^*GQ zu&D_RXQPXLd2Dv7 zSSV$<<7N};^jP{j?0@Vo`|KRC^0_2`{Mjcp%jmXSX}tBuHz&u}n!0L*Le3n$lK19~ zXP^D~duXJB&8L*gatiq$ZqC@QGevI2&mdS+uE(Bt+=yr*$Dein7T4nFX={1<7}fNS z^^=dZCL@sDc*KsKrHi!-f?OEF7LtVGt#OgrZOY4L6I1CmiuXVWq+m z*$~!e2ttN|`J+#oU`00k!H;;j;Uh*wq7s>y4<|y=Gycm7_%Jd85?(_Wpa4ZEq-2kF zSYrw~YQtJcH<1^1BuR8o$6Ct4iyLMPRV5lykk}KlCAt#9-2xnyg_bMd-3NzA=t-|E%L3`6wf0%8-UO#GwxH6G|)U z0*hMYq8E)ag20?(FgWo5XXG)o;IYb8#w*1J0Qrx3HqU-3>|qNtBE8q-NMLyb)KS=o z3OB-0j%LZ`_hPck69N>Ho)fb=nvL`mdLQBxKKosSHFmu#pfHmhL8qkqS0u1RF%7TaJUZ#Ef7s zClLq*s-^-pV&sLP`r^q3LNu)opyAI*z%f)g@hp4Fz7H)s$eM3^6gG|3dasP=zpoquwZjKsptZeb@pCABmhl zBq)(Yu8olkDJuZZYFM1SaGqi<>k6zOLb-GXuN2U0J@{%I?g*#a>~z6AG9CD8q0>F;rxsR#+}y0uwBB7a=_2y{Z&w;Ap{CgWRzyw@n276$Yd* zhDmTV9+W{!co|EO@~NbFhA9#Fc=0hnX8|3R=wwACd|Wz18t!e++9}rTBFOof?PQM$j5>1c3sodWg;zG6EdP=rv|WvzpoL=A<~7C2t-?J@k=_ ziFli%V zfIB6VGjG_Y;wdYn*Q0^1sAECu|7V}tYRWvutJ^w)Jns0%4~KZf>n)XgL5@4Mh8+fQ zjW}Iz1p{*!Siy1v<;6&O*NO?u^<3Qn)6gbReg?n`m5r0LD(8s9I+>@CGy)K8#2g`! zITkE~yW#mY=i1nL&t)>-qQIObSSc))Im*GU(P$yA#oj^&?!RnRt0OWBVcXKaROqjTgE&t?|55m#19++fb8-xJGtQkb-HFZ?fwh;(7EFo zF4u&FD7{81fQYWOyp$k{el52L%=m9d4IO6?{Dyw*G-1x_Tc#w}Sh={wZjh&FYngFruu!jn)5tYe6(lRR8FoUdE zJrXn~6U?3rE5HLxzy-92+mpbRNWo%a!4`bM{40w8qm2gvE>zMjuE+su;D>~RsR6Sm zf-neuAdr&i203WF|A{cSjerMHID|vkgiwHmdQhfnXaeff2?8`g1XMr<1i#mlH9~Z? z8mqvsqzfy<@RfC=sV=d(4#7+bbIJ?5q9z zh+yn1V*I&HEH^{Ez%hV@bSNh7xxKCNH5wDbMzSw|sD}A!t2}zdFY-DM>^p_%#ialN zl9(KRxQ146mBcfD9Y4pUYq)NX4 z#e5-1awNWUoUX|ko8{pNS1b%zluD+E5fI9{)}ROB6Fs0bykx_I@e{kH_{IMFjsKg8 z-dLQ|s0IByzmKrXkHE_Sq)1zw3~Uv-02K?;iOmd3ijqvq1f$olGVGk53I`m^iRUL%0+rl zil|K6T#kRRDO8yPKBNe-Bn+~&jhKQrlN-l?%*B{1PQVC3^TRn{L^@%FMq|vFV`xJZ zV@3~EzYr};H*C*^FqP&ShB}Y}AJ73Y$c&Q%0NFUT7kCcJ8p?vq7jsO=eTzzrfK8;( zs`XHiY=nnMNT{JCQI^;nvRfVo&9n`jyY~E1rrXMdM9Ng7&wQZ(C6xvybkjb9PA zs8~stc~Lf%Q5v<;Fm1~Vl`JhiBB;#Fq&ztq3sWd9(J#u%BK^-(MOC~A&>Sh%AVtV8 z`j=_|q>Mq-k+VM6e8udu4e_W4cfuWF3CK}IJGhifc0en-JW&$}1yhKGix^A*P(e)P zjbvTcW(9y}?X#E=*4{%nA3!m0#8Dd&ESS&=k&J>s+E#8;8LT1IjM2~djElK2))jRL z6<92aED5WNL}`7nt>cNnnAa?&3kxLBa~;_7Syzu#NZiEHvAS1l%-0z#ieSM>|8M;w zaE()96|!h0%zBv)M4Jc_^4Bz3oI%AW)M|+C(8JK%i*&^mf{HenQW?mcSnjCUZ`D=y zJXjmlR)U2Mh$+8LmCrAUf-v3Jj&(K5@`pqe+EjJgr&WqoT`q%lS;b-3>1x6efFN1{ zJ)CgRzkpDe*p`N|(w%rb+F;G^V+r762R0ejnX8G6L{erP$(~SA?Ls4?tJ@q=(WC%a zj>Cw@^RTH^2OQ<9xp1c}quIB-9=n?jZ<8_rY}&VkTeoPdc&dlF`VzdY2}i|AGzBw_ ztqHbu+t6*?=JU3NozI3%EX>thqYzwRJ;cJz*vicp&V33S@eYGL0f)F3{|Xp`ITT*u zr3Cz21x!E$(=@OzDV2fQ2VB4x(q$Hy#fam;hf65iz>TlL)gapSlE1|X=~W4`gNLEn zQzG@$#VlHYp#a?dvll8~Q^km}br_tW-?F`5`_%)o)nERtiTwTF{jJ{s_Fu6TVEjGc z{vBWgF5s2`U@LdcJUE6({jZisc{YwgJzuX8Gf3PF}`9-GsVWj09X!DZe@U@9h zTA%cQ#gl|tsnUtifJRu0X~;LsIE%I&3gFw}{anX^Fpn_Ci>KNj{~!(GA$Exa#Ro{Z z)9K}>J}{jcUScfPiY?9zNsG&&1yj~7*ffsg&fPwMtE_(0MbTY}HQp4bPyz8U3GW%m zambrJ94mW$flO{Y&Nw5GC?3&J2o=z{9wDg^ zmR3mWyO1zGW9rjThKj!EHR8sFCJ2WftDiQTj}U6kD(dt#lJ(`Fsjg~@!0PV!j{lHp znpPc~Mm8LFHII%7kQV8QAnEK?iN_Pil1|wswz%gs<>h5+bn{{YeQSo6>j#@2UP{^5 zk;<4>w3)7H6ke{CZfP_T1ExFb=M`;4p@5QHw>oG7{~dUONq{1e0_Hz%4hL-Pu7>Qd z)-2}I2Wv=bF$NDK3h2p<6M&u^fv)c9CSmEmZtDIs>jnz%{%7sZ?(W`h@W$@%CU5mN z@A3}s^Iq@qZg2K}Z}(1b`L6GYgJ&tF>!2QLgh+{n{Rr04hiY&Xmf&Z%-~mF|ga+>f zRc!E0kOZsd4LtCfJz5IY<{!SR3B8oZph`)(jguf;i*G=tY-nY*I#DB*+;JL@(=c%x z+J$Tg$z|+e7Ux&>*kgTghE|e>f?}r+vIlV>hEdprYeM8umJYkYJ7L3&tmp?|>Kjz? zS)3knR#Jr-FTS22@tLwA6pvY+;MGA&1yH~u|7j?R`27e`rlOMIhZv`Y8L#oNVjUPV zsW=Do>~LW(rv)%4l}P*W8!9MK-YD9Ua+FF9V)|6R=87@rg)%?$G&kS>sI4I{@*|g{ zBo}dtVDlG$b7Ye9H~>zUp#V=An{Y^lO~|6tFmN;t9?ru~%pM4`d*0{<^i>jcRBH`* z7zIqALpd6mDyDQHCvqc4a@OTyW$N)>`0+5mw9~-#W*@632MUr|MD?}QPIq%S4|U)a z>^t6jV{JDUzjpYw}}0soj>WG zR|-^;8R#_4zRCiJzuy!x8`PRvKowv__Xwrvf zdZ5=FqoED1_=u6PDb4AJ&5`+#z4@ePk;80 zfBnv7mbm)B*ZMsTet|hZe*y>q0ta?*_3z!8feIHgZ0L}o3~K)-QmnYH2}2bb=19xu zs|kgNB1K{tq^@5x6(a?zSP{sWx^(4;Ny0b~1+stcu&7)qkU|DKvas3HS58+YkUkeu zKw~f8rJf5r+_Gn`h`tDE7QdW$hZa>~gfz*j5dFZ{NRx2Nza|Qzy@nxOIOGYy7u+Sy4a% z4~`U*O+Vrg6Uht6t(3tcPbtz~N%rhhN<$#HQj{>4we(U<)7iwqB*kFUjy!M8XoQNlS@w&a7Ug(;f?p*K>Kyo zUvFhK!~-D05qFI)|DipnkVYJNlp2p4b@b7g4VAejnr^CjW}6w!$)-qe(wXO-aoTAn zo)766XqtffxzM18lG$gSh%(Bjp^YvYDWq^pDv$<=(G$mu)kQX0WsN!69hG@ufX77d zxN&Jg2PG6Btg#Lx!GHtG>X3u7b_#$BD)s8#3pN5U!liHhnq08}*qRrQFD`p4vb?h7 zECt8fs)DczdCyW%vvda>< z_d@F~yd&fbEoRn+ge{`JiW@Dn1V@RRx%+0caKcNyD1iiIsx%1&S~V2$A0^*7!bJ$@4fr}dnM#1uKVxB124SVL+}Dh zD6*iKPbpJJPQ3Be2Ol@?lT%-P@z)Q(JNL(9kNfuF_wIeGLd2mkg&3Tm2_4eH6s|CF6llXl{NWGzrRX2V1TP!s22w+M3%aArB9;gL)8XT70NWHKK&{Fss;~B7|;)OKpsVJ zinRj%?+$thhO6wxgGpqg9{;chismtlGi1zjPqZg~1o@B?$RZmTQK=?ZK#1N=|Dqij z?CAo@YSt#8wPi{LYTDGwR=462uYUdO=&I%|+*C`f%DU^;dPS@O*=}x^Lq6UID2t+SdAsVx(maecgEozBa-MYGGR0zE- z*|1An@RGN@b<6G8yo*!S{8nxtfCVnJID%7I6(RrJ+Y7o%U5TvLmEwh~{x35;-TxoILu#T{a5%l?c`@f33^f?=Bct{~s5* zP0`N5t<L)221wZU}I7gJM76))4iDmWex^<47=|M^=~!ES_n_uk3iioxo}YT-wDm z)FBRCK%zj>n9M6`s*n``Vkq?hhchfO1*tf>OHrP3l^t>AYh+n$CvFIr`BGK8RH6uz zypN3+a;ksyqeD?D&~?Bu#ylja4fa?Pcnsa=o+#OZOipTdy1|Ml%S0EEa7dh=G3S}o zS*06b11n%6A7`Wj5ipF^3iO}}IsfGwy2wLH2M6eM2%3!Popr4Vtq>gRm?1pAh+4Sp zWiX5RMM5?Nk+EUqz##cDZ)QlQH%*LAFC^8WD8^Ta;|%Gd_0yqV|2C;jt>SSzv%&Mi z^`MjU;PAu`9+3E&LGYyQ%!L$Vd=$9K#XTr57i zw`lN3MeH$;Y$UNL&CoIhvLFpc`lBDj{X$P+Rsnqq6-_|UG79tiu+K8omYMf3;M(5bwXVa zC(=WIc*C*1KL{a#Q}5=5`3%|9Sq8wD0!Ccb5qCk`?2- zQ9yn6UusJ2M`Xk1ieNMu;^z-nlQSFs;72^%urcaeAN%sl z$M(CQkM9#~@L7R9m_Wfim|)c!zq#RNNuGV=gtssn9jL=*sYmgRhdw|Z+!-385uH6O z+D|ASCFGtxG~e^xUi8JvsB9MdETDzt0tMvC2;iSC=wB%CUpx5Ut_?!Zks9-;0!-ur zW{t!L&cikE0`YB}?+L>sVZZ?hIpT(g|aK&IM z%wP@PVCDVcE?pi&EFlpZAritJVr3lYMUUvkn?~VH%`L{=#N5o?26ow(vc#4e0>FK| zLmF(1x$w)dEK7AYS5rL2B@l~KEQYzX4NCMxL_9^C5sL|+f<4qjN~pjoLWL??2xE+Z zE2c#K%@%+W%L@#MgJ3`zoE8=i1a^7LLNEb}cta6{#0UVvY3+k8Ac0DlUFac4&_&<_ zPN30|Vu%%(cXigUcmN*ogXSFGC>~=83WPE;|Ksf4L{7*9$bn->tbi5x;X|lbu6RHq zsDnKeU{+wGVr*kK&VkN-W0NVQHgY2Xcw_V+2PIb6C7!`^#9}_oA_dgqE#@LV9>gaq zODNV0L#UxKA|pL6qwFDKI|SawtiU8NWAv1yK8B*L_#;39B2dbC zToxMIKcL=EK;Y1gBLyx*S&kTF@`{GY|K1G(g>V_BLMS9Pwn?uT)>0CLL0kyVEy6!M zCc0pNWKt%qIA&)uWo@Qj&~4?Ikkm{bgKcC$TrS33jzC>@%eR!|SiXf|CI?}XCLDej zP~bs7OlIlO`VVe|3H2A!F}eZi3)^*GN|{phl0iphBCyAHpznlKtLtw z6y9i#f~T0~l6Vfwk6u!jKB;!%+sSwLXI*6c-Kop?JI!K?MaS=aMVmMxAWvVDdgjjm5n;Fzywt(s#h$^X?s#I)C z&kaK!+}m5MW>%O%cL*b^x+<)$hOAa%Z?&pDyy_rUtApfeER^PH^{Gk#qgy?yq(UmC zT1Y8SX@BO&QhaKtj%pp0|EfKd3bP8t!pWS280+RCtFn$Mz*-)QbxO2m>$GMew;on^ zSVJS!gFkeG8?cun<_!j*!8jykDfj?j-Ubx>bmXd82oI|Rs@NGKqqJ&KU_mAq(PW2#l0@2(sI_l5DN}HZOink+NuTH zZdTHIEXXpY$Xct?%4~|zEOWZ+g#zkB_$<&u5JKJSu(U0W0)W?ot=N+7#s;k9vQe3) zEz|0Y;%*GnA(5&;|JcOdO?=v1EP&A7-~<&SM_bYZoRX~II)tG9C(0J8%_6FwEowDN zrVGvLgQ_g1QlRY)g+PUE^ky>%OD_hVvP3Or`k7|NuGM

W$HI4|^`qV#r`RzB~#I7R$&B8CBJ>aOne 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 z zYR2Ym*5+=m8D<7&YW`+%US@LMnQ+eLaZcxQUgvW*=VMl9 zZjNVfW@mY>XL@$$c82GBp67n%=YQUrT0UV54vxn0kIOme&%qqO;T(qc9EY|Xg-&RQ zUg*Km9EzT3i*9I*erS%4XpB}Gi4JLyW*U($X_9stlTK-rzEPG&8;`E&!r5q68BHl@98b9_p9Y>74!>fwm$DCJy#_pQnx=sGc9Gt{#7dys~+sFF6^&9?6FSlvtI1AZfvDCAcBVM$);>&yyD8n?9A>e5!UR^ z_UucO;?EZC(RQuNChgNk?Ns98)MoA09w!`j?bw#>Wa{DIo$cGkZPk|S+~)1xh9o5R z?cf&f-hGPUHtyr@qtHg~^( zrWWt=Ht!dD?(wxV7NALufj@nl626yn~=-~iXoTsDz|cj5b!J4@+}YHzu-zP2lFt`2nHAPGB@*ypz4 zHFxtjABZ%E^EnrB5U2Ayzj8Up^F5#NEa&q-C-N5u^g(BGJty=-_iyh+^hFPHLud3y ze{V;J^hr1G`KI(sKXE(9^i9WbN#}Gir&0j-^H1-kkkW)mn4`nl%ZqRVTCo4W^F-4| zJXhR4a4G7hZ{o(vV8l~F%Kyp*)|3q>gLH+*p{T37nt8BGEW0m@)B9ATNWQPAe1=&3U%ZXI>Z`wEu?XXYZ4+G| zF#EKuNq7}HyV87vcr#}Z1z26$zKVp&w+P9{eARD!onXtn=OUyj2EKpxLZt{(Nd|#e zdifoiCjhupIe4;|`?!$Fu6X{enEs;~G15}R6==hd7hQ?%FTSnAxT=Jd7tYNJmPuRt zmx%tcfK55t`^A$8zjuhe&wbV=_}AcpMN>C#y4JP;h#|X}DYO6L;~>I>3KueL=DRRS{Wz2@<$p~N$Gia42F}wEX z1400Fph1I#kOwDb%xqyC$wL;iWM>pUW#;UU%ZwE-X6>4l3^Zo3ypk^zTcrq>1i!P$nq258vIR~9|Y#owGFXjQmizw+GM4Lqe z;?|UC1DX~QefDjb-$Fv%K}dqdK*QmK4IP*uGYd9YBT3?sry+U}9)y}|thol8Y|AzG z9BdV)v{hajWg%lLI%wt$B-{;E)Mc3|HOv&3AyndRCo*ILUU4Pl0Z&+}(4!tPp68)M z-U;agnYNkv5D)d42OTDeaOWb7KhEgjojyp&WOUrk=SXX?SrUj+ue{(GrkQHGsYH;$ zg+ZtF-N&Lrlr1GDLQ`6Wl|ztO6B4Jf$~r5pwc7uBt6zsHc32g-`g-dal30O}sde#H zhEmoPu^fEzRYjnqRE;JgGsgO`!BEtaHVmvAfpr@+1Ca(zD@Z6b#SSGZ=8zH_cUqau;LV0r5Tqxz?Y013zbY7Xbqtm)l>;-kkrp30Z_BPby`$!LsKVva!@H#%d)8= zzML_>N_CCU3Y2Qw->-S=y*FEKMWg0J1RMW6F3ShaRjopv2@MekT%*l5$U!UUQAi<&*u5u1?2*QcDWhz%NhrWXlF%5a+fB?NsP|q7 zog5)MQz3we>x?Ldw8yI9ArdnRC62fjrQu%|e|+-G&uh_leLqhLCVndAOSk{coX`nr zrcIhNsf03ZE8xW>7Q+e6kGBuG7qa{BzXxA2PjZq9Pf&sqs{xQg4#$k|NiRp#J4#ZN z0u5c50u%|_ng00aJJiw1biP|1Ti(Dv_rVW5hg${(X(z6g%_o4+7+?Vph(D3E<$K{{ zMTp3!5Z5UMcCw?L?Gorb=rwQ|5A6TQN4RAZ&E(|;RubBDghCWZI8AQsQiumr2STf< zFCnV(g}pk(2^)%!89HQ$of?uw)hUH_3W15uoKeA0*oQv}IbjMzvBJXnK!FUL9(OY1 zJX4IvHV@kzA`{scPXW$GB2-HI-bb4Fc~L1lQ=Bs*ajCvV<#CGqBq&2E%27rHF=99j zDN~seRau5F9&n4lK;;pOBm)a*`E7-we3UjC=EMi+G z*1ecdJv$FEeuaknVBGUq8`^tL0Yxjh4>a$hs!1jl44PWD0eTg&B+WWTUoX=ceJ33?nVTeuK^JwT2=AIdY|Z~ zrOC(?;DkhRZ<$>#4oD`@7zWO|a$ea&b|&2XWn>!?v}!p@Vcz_%Lc&{H{)%>o?-gC8 z60)rS#3I0nJ#pn2$=RfPX|$H^)&>5O*>6&ouA%UX`mb~Qq;B@5UZ4q0Kq}iBW3_6T@%Wxc$!n2g$=ugCNQ;h@jtngS-@l) z>SqoeX@$%$tBj_u4{-b3OfSUGob~|@K!?(A&H2a9zExX_@i$qvt{7}yh@zSIT@NJn zo*AL-5uYvYag#fdxI2WD)y)Hcl2lP71_1a00AiT5LZCDq*YP>8n0+JD za;q?R!nyzAAs+}zSR!9&tiqE8U%0{%-f(zdCfghN$g8ot+_Baexz|d0{V<$B76V$M zH7AS5Iqq?gqYvK@xjDjf&TxlAyd!D;!*qgx6sAKsK3wHVIGA^kZ1rMW34VyZ2~5yJ z+~E+Lz+p~oLFkHYyyG7y`;ohc>j5)FRo7*wST0PLg-~6qGr{@7cmD9pm`WKwW~a;5 zt$D|&*deMq`M{8VO{J_NfC$#e*ZjKq)w90!vBKObU605h_vLVAx(U4RRFPr?y&)#0 ztX6W(lKlV!z-2G7n@==6i7DlnP(J>pN;LGJkIh~RRYftLA)-e6U~7vTeYL1PJnp~K z><9n3AIp0i+(KuC1Ob$ZE=YAp=9A`_6`_O)Bt-C7Fro*`b&TUU+nyw(!>B_`!joF~BImCZ7q7OVGPu&%;{aT-OqQTHy z7D5C75F)i?6Gzb#9x|UEc0`GEg9fHvGL+#4W}Zj22_#%VLikh=e8kP9AmJ5N7wOv* zR*Wu+2rIf4lTF`#C{qAt9xRd*0QMIB@rPcl;(J)4iGd+Jb{Yyc1UoW>D>hG#NRTbu z;z58wQj}ag2Bbg^qz_<@Vi4r7u$y0OKy4h;7nw_g31YR?*n{=czO962UB*@*pY)|$ z<1vIcJ>Nz~A5aRYEY?DJ(;Hf0N={$BWgO#Gh7H!EpAl%92{GDUMDB%Du z1nq^TLR^?r+GOG(q+?D*v`yO%E+%7I3!|xx(R>?T;N@d>re`KrkM+uD%F0ykh2nro z&^QC>)CtV>q&;0)L}a1_lEm}zKmpZ=^no83Nkm5afNfF&ZkD8&t>Dt+#49*T7RUto z!GtkwAW>Exgjk7;91{j0<_|7}YfoME<6bTW+v zD&0k~;&!5yk)fW$9cQU1Cw0B1d8&$1l7tY5m+|onS$b3mrX$FdU|ae?LdgPA_`>7x zB!Ok)kT4@>>XvaLCw{UYMYJYXeumNYn&J^ElxD{qT!nJdRRA4IVxp2g#P8+kb6u8{ zP6U?)s9O2sGbjU4*ppB`#Pca=LTD+MK8bbBrsx#Id}5P;fk|v-N!6{2qxfS_T+tV) zM>t+0$yF&ys6c303u(0(LrfSSC0UJCX(key>s-a0CdD<_DP{jE7WplNpE3Rf|rkoGJvJKI)5^1XSRt7#%8+{wu(a z(ia9SV<;q_L{KycDoKoAY;~POVBox#l+{fH1}bbvgsHs>r$;41Se(&ATxo#iW=a^< zy!K0iUeu&|i8CMq$0FrHOc*W%5_}YhMTU;TFhs?sg)IO20Gn8Z$9^n00l+(A8(vs! z&PXUiz^u$Bi*C4QzDmT-Rt$h9t6;9j#5zRJnh~E$2`%aB@Svv;AXlZTsnBxR$$C`E z@TzZK<6N@sK{x^Eq+!2ui8)TH50I=xK&*sinHk}!!-S|*GF{gGtb_&7KDO8lL6A}; zY($J5GcYbhI4dMcBf!h+PDMEnne9DH1I325;mb#;yqOU>NED1z|~CjWci{E{d0Z zDhdFA)S3a`L~PCNPKOQvKtjNhS!l6L`3^%JaL4v? zhlWK)F%GI*Bry64Nxwv|Ldly%Axw-}L;yeSdnN=AH1KjTO`$?2d}O8<0qYK_AezSI zmrO8MrEf=gA2jG)P~0d^vBgmM1i#2C-Egdn0nCL)$n+jW3!ckp*lnl8Fmlo`L%{DV z$ZrV8YZK4!K@@@F*snrqg18`GVVdL4M6eE{FL#{ND!d(tPM~dEs$JD_L{Oj|52f-Z zuZT7!Y3y+^1#%waF<2V%$0G7%67nH4aw7jb@*?+fU`p~GLo&cfn;TFYL-;R@0v#w@DH7P>KYAinX&>T6(6JqR`mZYkY1bmcb+5h!f)83$M~AG6DD1SX{dC4{pid0S8@&@#Y|1xYh6T@3;n z^FRl4i!Pr)>(M=zA2U#pLr2$5s53CP^BvtT)izo_?=vOzbDlla@uUJci!&<#-Y}yG zKsPi&mjv7@1u=kg11$wNNhg+QNnQWxn{1_@FGn;mTVYi=1E`_SYJ^C*{oF{G^f>?V z`5~Y#$n!(@v_vB_LcoMfXiHX=Bg&HCN=LIo^NZRz!zqkHC^!dy1Q;%})-BtHTE7Qd zM?_k~^;<(kT+{VjKSW*Q^<6teUi0-{uQh43^+NPDV2_Vn2linbHtf!$jifUqU3A`= z5dYHIzD93DaKL7=vSx3#W5C|!aW+|$FSUSPLIASxN ziobY{19^`N`Hwq9jT5xl2eRwZNr_ zJ3}jUMOO)OP}l)2$Otc>N4NZ5_rfR}m^Cy6hoFcA$O?D+78@JrkxXpJC?Eo?+;%_a z`AP@@Af)+>pogOfL7e~4!azXBC>+A$^h+ezOP{C+hor$Z&xadaIzhnzOmoh{}eDf~GUx zWTUW0rTVI$hpVduqmRdv~B9fuYwCFr)$xKg1I~DhVQSH$lb} z&Ihn3I-r+MAe@4EARugDLLT&TMnt=#f4VMId%GTbu-CbbwELRBdqPlv4!j8XbR2HE zlhda0EXKN@8+<~jfDqjJv^R*Tck(WXxkSWR$B%hLfV{_#{2PS)$De%3Lj=i-Jj<)R z%ac3Ir##A=e9iyA{LRaJ&cFK1+x*Jw{Ll0J&NDHB<1Ddf`leH%h}LgZ54yi{In+nJ zOBiHxOMO{1shDEQJHEgRd;q>bgg0q2M|?oiC&UL}y*KR@*Oxs)oPAdy6hvGGsx&Ex zWS~TBQV?K0QyqlZb3{|Xy-0lh*NY|J-@V~mmELE>-yg)_i-g=GM7ynh<2%IUYeW!K ze%HSMbuEkL?|nn471xu+``7mR)w-2y zLI9jXN&+yJY*&qA{QzJi3tBP&X5q$_JC|-~G=2iXlg3oVAQ%XORXgbR3!U=C;ieGF2_A&D%~hv~$S!pJ3=Y|_amp^Q?>DXFZ|$}6$VQp=g9<7>+=!36s_0YDK?yC?&_fYTRMABlWsd)^ zB>Bpq(Mc(-)Y3~a%@imd-qO_5PjUMb)KN+O2|!IMX`<9sS#8xmupqN0N0^+Tg&A9B zy{Ndz+;Y{|UmfZTL?wZ(kpxGAV~3s;TJL*`A84DjSxl$yFHJKqH0wLF#Rm}Zt+^B|$ zStlMc*+o_%0nlS0M?k_?Kb&>$I3{nSHM!@XgI3w;N%Kdujj?t*@8QS}oY1b5aU&?c zVd9|K>?=^Rc{oK9z9+oZzVXK8f(rzfXV5vB}l}35AL%q1j}n>XCJ2Y!-6%?)AGr|D!=tK9lWF zeFZdA?!@<#tHFc>^FyBjAtLs<0I>R#69*(E=L8sKn@c_7oM0MIkh_VN!Cq!yf)nC)M+q z9t5{9Bv3DOA&NT4up~mbLxAuI4PFS6kYxj-7|Hk(06Y>HV{BLk8^XcX zT~ZH1R0`k5%NKA)T^|-zX)L3;{qN2eb@8GBSd_Tut~e zkdaA-v68h+$i`xKOps{NV_;#NFQFMF_6g5yBdg<9pyfJ^bn*Wn3;Wjtec(;bU~L22 zaZSx$LI&|51{zRdXDi-^lCvcy8P`0BIu+B-#6Sa{q)VO0=D9hVDeE9zgGMP9(lwDJ zWCL#?*XF#45PZg>Y6-#TD_T^}b#exsFF{&l_{qO&?sK3Coy2H-A(M@Pv=#ho=tDWj z8iztArU}Vr#{j@hdGgAh2oYyw1b7evsxqMB83sWQ;;n~#lor3ABb$2tc^g8KVC%L>9N`gO3tp6qL~1)b<5g z$x_y`nDwXK8d)|@aF!AsVZ~v55i_<$H6bUMUjhN}2E;fc6`?p=OA|7O-cXc_!$k>V zqe6_uF4nOwS*bJr8VYwpBNmrjZD(Bz+nDWYbE)tIB`9G+W}Fd#1k!~m1Up@>=61Ke z{cUP7!rrLx*0+cO?m^mWO}K_+t_ay|XFp3;gzz;geg&*x3EPlV?&rF#1*3M4Ip0%^ z0u-S*3_s&p(5FJfyWkZsdCeMPxr9$wx}C3k?Q1>l9R#eA0Fo_%2Tl>sI77wbi-?9t z0}Rw@MMrgw!p1L+0LIL87~hn|^wic6H7_%>w$-mfbTF@&tk?&Kj*61y3{iyG8P74%B%L{(=N}(b$ZbyYg@l?# zEq9a45?rN6{+Om-J*awb&E2Je&V_|0nffxC-7ej4RkR)@>B6%@G z8aOgk51ZOGo?zr`_q^WdO)InQo=z7syNfv58cmh8>E8C(&DBqn#I06T#|4d557@1V zhB73o*6ymk`?SM=FGDb3FX|@5f}IX-REYmuwxg*4n;h|_^X+tSl!w(fsU_d0aYDmeMN<=I$a z-LDT~L1v<|gX5N4*V%5jpTp$dE2`U{yEYke=T8TT2uY_Z<7^+?eMNac^S>DaEnEPJ za(z?i7GGTqv?HWiYkxSxBmQvi{8=H`d!1JmLWih~86%3{(W@VQID_>LA(R7g?Fg86qQ@4nhOsD$n?))SyKa zEvn`YLb >LxDk2(ep0F9)}9EIu(5Sc0Tdxa6bp^!qGd;V(Kna@824}vqp#Q;!WhfQ z89(aMc#b5(PaW-XFdn5ZB;^igD$9LOhuBYVzkvat#iY9z-g!dNoQwooXEGAh6@9q*Ca%J3b_(kY__CXuE$ zHpdpG5CohF?%J|1;Uet725GJ#68^C(He<}z%oj2&+`zIBTLvc${m{#*JjEl^$Zd_4+Z(M zCBiWYhw(IPFg0DVDM!fv5Y8t3gmEW}kr_)cE=!L&SF&#olgdCsF>SFa58}m8Z>>l| z8rCJfN|VZGW6%uJd~nP#U^69Z^PUYSxlN^pk;-InS2(9cPqZ*+1pr=a zVrY-h&XY%R%|#hP2V(RkT$F#D6v)2HAq$kh6k^RZks%zd_<*i;qK-tfG)R3DDK(=D z8DdC_)bjRI`rdN?A)<64rgX|~4HtA^44?uUytEZsng6D zQ|<%~5{3`68b&Yk&z=&qdt$=@ug`QM^#+qv3zO+VRMi7n^&wOs-mo(xlrl@1k*|i8 zR?B|cSUG4mk$jyq!OIV+WE6H+EjfM{V1 z<&rW!6)q$YL1K1JA+9u90wx+Cv(4N}>n4gJkX9#Abj1t`61LV%F=AOg>Q`56Y;mD% zDTg~xLT~w2=@tTLp-^loBx*B43Qgi_U6aAImRadm3rq6S;?N=Nmib_%XE9<>c{XWx zmKiknA~+XxJvW>(w{%B0bRQyhQTKE=*L6h~c2`$+9fEaVw{vfobaPjAYnOH#!gga9 zcY{~|cZZjEf7f@}7I_x}cxP95o0oW>*LaiHd9xPN7y@v;qHm{h*(4+gMprT&BHD_q zX36&_0<~nS>H8jn8lCec7}pszP*(`GQ^q1!BCp&^f)tl+UpNIan{jF{RaEgOnCiwp z1BO%fWoZLAQ*{>n2=ztk*AHo?M6IkLW}v`+mH#x&X=jSCSa1F8Gq`H<+4T2T_!j{D zcX08xM=56*D7aX|=@4!&-jvZKC{0QG=GY9ag9(#X=FE$(4kSEyA>KEBTh(sr;D9rx zwu)g8ACe&o?iC;y0CL1^T{ufCc>5}6iAN|;aWjTz*avIaEO#{V5+hkB5h$NijW2@# zebJbV+c%BNcyP~njpvvJ>$r{cczyR6KG~R$|JaZ3*cJy_`2x8i+}Mue7>?gKk@fhH z%@~sDSd#IWk_mZ{9ht$b$_=by62iH@Qtz>rZEkxqo#9!!)UuspBBu@mx*+=hE@kMJ zsjKV&;G>qouPQ8>-|T)H*axs#n~j;9DeIfVd7QIwr5@T=Xr`$M+Mp=`q1Bgi+6E(Z zni*=jA$t0!86v28Ix&QLA&B}SlKLT*+98@6sExX)X<4dwTB)Pjs;3&OtD32=nya(g ztGC*zyIQQjnykYbs>>R!&w8lGTCIJWs^40xA!ArcVwzxfAuymm_vsa6NVg%wHW5Pqvn}JCNuoDL zd8bAL98sfFp|p%|IJaSnw`T&nx2lmM+aMnTwmGA;yA`lYqP2JHwPPDG!0{(xHdokp zvmdCj)$*|!g1yBIBjB4(A7Z}Udm-#wz8zb?+nc}R`y%oi!1r6g`<{27r2ocsZ z@Y*%|1XxblK)QOPl1AakJ9Elaw#s!v2mpb|6{(6EMhNZ*DpY9y6b|8m3@Sitmy#AK z4XoKKG=azoL@7i8WQDTHpL{(f7f#C<%>_iwm4eL=_z65hmh${SN;>gYxGZ@<&ex;P zVPevWY0fR(&>9aTgc8e>f)}>jW|F)m#GK5H{LIrl&vgOO$#NC+;DnN*ivCdt6kUiK z9YG%A>UiA8J$=V}JR^8L*nhp(g&o*^{UU}v*>@e;kKNddo!FJ#*`uA=9b(y^UD}%+ z+NYh{w_V$>o!Yw{+#3Sht3BJTz1+q9+ta;C*FD_59p1^^-Q#_~>N?Idz0y^fCCxUU zzkE_>Jm5*<+HTu>R+}Mmxh`_~1zz9-5}wv@J0?D0(+}eR1B4b-F+MvR{vj-$Dq7^> z5r*}I)`cW*vKOL7g5ct3Vn;gJEJ7Y3{!w*4J}*#SPsT(k-qA+{_3qh&AL7#us-9#{_7!r z>%HFW$3DKOkCVCH2T~p;E*?L}SK#U1O}eD+6Q<0@81J=WpZR_c-}EkG{O|d`#tENP z&TbwLUn}-{@x_oG^&*uWzwQb9;3q#WhJq+60x&Y4D14$SHliaw;>&EK^LgU)?ZWhJ z0`-GJ^;aTFF8A`sPocyjH35C1(LycQLcnBS#&5R&_v4~9Tw_ave~r3>_+Q5|cr7!8 zA2pEQESi5bo!Lp2r;6>i4-eZyofQQ z#*G|1di>}R0HpY}>kh3sEP>)$6adjumW(YH?bUFNWqc=Bk4g z8<&O5OgU_|X1(f0N6l8mfrKeq3+{?SIR<062eAZ@rFJ!lJfq!ZI_w>AMfZ%rj2dYFm=C5xq)q&qfO;^3esI zKvKm#;j7G}K0z(D(!A0ewQ8ywrZCQLNgIpRVs|vL(-%ux5eaJ+J$Bo%#VM2yZu@Y; znrGmYK}-5I>GeSAsj|y5z~~}L(EyAQ3N0Qk(~B;m)ZumBxawz3-xF;Bz#*Ej0t`ul z>rx3p$cA0e=mZII2O)_s(;zR8TkZqusw>|5>*(nL_~(Tqgt_LNdp>yR82ugi;JXv# z1WD24VUP(DI`fA?#Ouzs51D7uOEH4Gj)>Q$mtGR-K1jYdim*>_yy1u|&J5#@LtasY z6DlP8>b1jv(U=(%lm$uu&`XcI;t{TBz1v*p+84ajrOtlPa2@{66+r4KBv`Pch!E_c zitzwr6?G6oAZP&%VpM`)05AkAwl}bqIHGfdvqT^|^(pH`Fnx2Yq2D-GF$^@MgeL?B zFH&d_{UMBh{)--ZIQA_YD$zCDDVNyBq$!(hfDRWbU!$Zp1}4QP8O$q4e#++yf=scB zR&LYMuChmjlRO77GFY0k}VR9XZ&PIoRknd#s`lgB8kwF z#|&pc!XS_Yr6dRc;>YqiOO-ESB`r+}lUo*~YZ{SOFX?5HIU+%fZjdiSg+H5Chk|wq%a7>mu8ABloF->CF zs7Ckf9ywh?j8Ra+U8Q=`7om{~Pf&sqo10ca?1|5k=<}ZY^k+p*fJ|oa!W5&}#3n)k z4E78pWsNKdN&neCU6|sd$b4o&BDD;ava}>FeJMZ%N>Er41fwR|Xh%O9(pE|or=HWP z4~pP{QQBge0*RC_EEg$e1azQ94HFMIMpKo7Vs*|UsUrn)(nsDipZlz6-=aFzoEmhL z2vsOUS>e$CimX5n6rE-i4FXe{hN7k86jRX58Oum~3avzKpHT(E(~|hqqaYP&Q$43w zzT)+!R5fhTD5#M#G^!AQK&CT9VF-HtPaptzgGro`icn~gj-mvpDH8XRojrD_l`W_{ zt@g8(m{w1zU2WfLCJL%f#YgA&0I5AJJP?->XIinI@ zyQ+trRX1!JL5&Nh&oC zWWD&^h=02qV4(p(7FrVN5EvXvfgBjX-z7*=^<;@OV4x~5DA^jr2VeQ7<-GN(-h>6> zT>;boZB5xEuf8UNS%m2KzU`HW!G_qsSVH1cC-y;#g+|~SA7sY-B}k8dj1XdGgj4kN zfo9>A-v+G;mNoSOI2!~FbVfOr|FtpzSkWNa9y!6uHCow(To5Xc^UBc3GD`QCV_C+~ zAZqKFOv;Ss%piwOM$-UR^+a5G(aT?6YzZ{FJYaeyBauFUB5G9oNXD!(xL;KC{vh_uXTJ*@UMC8Mj#slBc0S?3vIxkbnq0eSB} zZ428C+V&CaRkeXjJTZ-V%r^h5C)PeqW|dx=x2mRtM8uLu1kUCJIo^@=;#(o_#UzH{ zh$LbA;E>f6qDjLy;E#`2^o!+9K6DFG$gQzU#j>q~)2G z?Lb~;5TS#d=Ok~M$vJXeh1i@RG`EPVJP-)4A4KarH~N5O26X<#{N{08F~r*rb%Q7w zBTU}04|?@5_!Oz+29e)r@l{}ZXVUINAIQ^>JMmC)(+xUA2-7H*O~V_}$*x5IyV@P1 z&cjB2^o4<^(KfIncaf)p&&FEZ$25#C-C;&&D#RjlJw!%9V-=A9yX#Y~=;K~ebUNF# zEAcAz*#c6t0*1ZpX-}iu2RqU01e&j&FMHd+Yx)FP!6^3=#71Gi_R>d~<{dKp4?pwR zzn8K2&jfw#OEI~!nVB{e425P_Vcfj-cHvsZu+NP6K?a6^V( z%OZjef@%H-fC^F~YjS5V=!1drB2HvAPCzf!mJ?x@f1G3z@zQCd0t|lFE7T%CMY4bhAzw{3cUl-~ocM#)a%R!Cir|AGp_V7B1`(lGh_A?t zYGHa}vs;g56LqLE?j?t@@FY6G24I#<_Ju7h>Xj9?XjTG6BY6y{h=qs!z5f&n668Lf($T3BN zEPx~s;e-nxaFHgNGCSjt)HIO2s39>3kB)efuB4Kj*FH4GkPra^NECN6DU`lAE<$KE zz2Y%P*nSDPhy*cbUSu{YI6{kTnRAmIv{am=l$!6_Zl9k*V~RP5BT86HdND zFnwtdWqFkh5tf?>mjdyWo@jRMhA^Wz6{RRQ%;yUcxRR>>XpzLHX{2eAaXFN+GI^E9 zl%;8!42UF)sVfiBE$6tJxw#g)T5_Hh zK{W>Hh?xix`cnjY1P#o&W!GtW65^i;7@gqAfbQuKNVsG=HWfUuQ1DZV|7iuTDG)kf zXr-_LDwru%qn`h%o90536}d3ub)mK>DLOC)ML3`cp^G2tqGhpMMyWKgWQ7QUI({&i z3&B&3w`}575h`$MCb^^9DG^mjq*>r0PX#_HsDiWq=#{0JkWacls`hnjhh9V)5mX8h zMA$KBKyOQhZa%sY^ww8mm!y`1f?N4*cV~HuR;3LAq=uH75&?~`=cG*vn~x}EI`m-g zQk!AwaM*dJl6j|Nr=<~rFVUEpA4wGi@srP>3@XV}&L9Vc$1&HC2M9B%yOO3=ai|e- zqrDfZ4l$|?QK}4astb{-^wTAx+Nz}bs-_yNs5+~vTC1#zqp+H*vbw9Z+N-wutGMc_ z!I}`OTCBHvti)=g$m&HSk$61{Jz?^b%z&xCCUt$|s(5ur5bIi{2pg}4Ij!2-$RMWhxMwOCFpH0B5PAhghQv;b)CP zk1Yvv_xXi~sbZfy453SOqicqU$dB~t61^+BYZYd2)HF}RyT0pqz)Nbelws0C5VPwm zwVRIyp}IX;p{=WtpnJRul(1%;d>D3^h{8(z7)}SbM~lvsb-EBP9a5IRJICtICI%6Va0pD^##0>XcBY! zmWZjr9^Aq1d$}A;njl=lBK#60ynh=UvnkxdEIe*59K$dyi!|(qCcMHpJi;%m!!lgM zJ#52#>BBdCvaP8Q3tXWM%zb$O>MU-EF=t6MjVr}+QE@W5FV?6Fpa2S*fG0F%zF_qd z@|G$lK@6s_Rl7i!01#Enuu)qDI0YtLsXz&bKnbc9OhPPCX1rH)IT8AcrTya}&S1u% z065`ee0iFX-)OC+sm3nB#(Mn5HRi`Mvc_#33XzOb5nEqM#Z=Z)$&if3HD_2i$$99adpJ);)wa1L6$Wkhr4grSP`U*VK$fRt^ zn7mW4OQgSyr{#Lg?UT%?j1Y$uQ-qAhlIRdmrU(cuA<7^(R<>KQ^ja-Nzkair&TtB& zaLLHjm7eU#)BLPpY!YJsoDheUEO;2k`drWaoX-n^&jRbu0u9jnT+sb|(EqFu0d3FK zY|sn+(DxkC4vo+dUC|PK(G;!F7`@RNP0$wI(O5XqBAwCAdb2Ar2QDGVd&QGyRiF|k zA?ce=Ycs_)JsPCZGi<3JTM~CbkW6f-5rMcj$0-oC^b*(LmY2KJY$Mb=J)aOUjlH*+ zdoy3UyGvI6OXY*q9(S)CVbmtE)dFF&4p#xs<9F7i)$zA7AT2Z@R2^ibDCt>+olgo?Y4c-M(tw{-o~Q2*KHV0ustt$m+cck~>~(uvZ@9L!*EQkD>!l zU-~GKb4E_-q9yl7# z3;dlU889RROw!5_CLpfgBhKOHLI{PtJtQtY4i4d>psW?j2J@ZZyTalkPUDUT-~ui< z1b(z3VFwTY-ax7F;yT&C?#sW@b6*1Y;~OqDW*M^;&cp{%d~K3;FI3-HYl@7)Rw0qBCx=YGBrgZ}4* z9_WeQ=Zk*micaW_9_faz5QzThlFsOqo)DH^>5!i3j^62*e(9R--kv_{puXv*9_pmd z<*IJoCylji?n7=4=lm+=k(dx^e&plL>lOj7QV`BOBWVp$u^o{X6uJ;|hBF*C5yyTJ zFYp2%Q0#0mXV5O~J2CBk2@%=es5Rm24x#LpDem6>6~V0Sj~VUUZtM@i>=y9>wQdk9 z`IJ@v5d`L*-Q(Wu4599-x$P0L@AwYx{x0zSPVnqL@CIM-0iW>1e((y9@DK0s5r0I* z&`rp$5cF>E3z6}+xwyUV@gLDdA20GFPx2+dx3I1h;cfCOKN024@-GkbG5>b z^EZzWF^cn9F$R|6^FI&tmVyYK(epuX^g_QJMSt``KlC)A^h^KqO}`mW5A{gD^T-7s zTLLCqZ?G1z^*E{~+Sc_~FW!4b_DNBLayXuwk@j*3puW-eYWVgU5%)?s_mNTeoOJhR zk6QuOKFa|3ifR)k0{DK-t$hFTRV)RNf%rcG092&*iMSb$Pew{8`Ncu`kxvtuFNc@^ zpBbF5hMwQ}b2BlNGwdM2C|g92qtEg#&-zQz6|52a-ZA?>QTwTJ`>#JY|E~MJ@9W^| z`@t{#!%zIh4>PWB{K>EU%g_AHPaPre{LwG{(@*`?|28;n{n@Ym+t2;oUwSX{@z&w} z<4^wCA^zo0^1m*wHC-P`@*MzB2$~=~!U6yCkN)>B8t`HI7t#Kw4-h^C0+__i;6Y}| z00?B*(BVUf5hYHfSkdA|j2Sg<UN01>!jwD&qVl7ypW!_qVrSoA1WtXZ{g<=WNjSFmCK#f~Lw z5df!UN{3AWc{3+Qg2j|tvr^XXUA%eq?&XWMr5Kid1rP3;#h|SsgfH^=3Dnxclu{s% zG|UWfMaFX{=bdwqY17J=6^|xe+VpAEsa2N+kO-PHNq78+0kFAa=)@Om`?*tx&Cg_5 zRu3m$-1zaXEnOmK4s*yZX3qZXw0azXoj8xhqf=}8`t2l zx=qVBr(fUxef;_LH}rs3A%&8v%LsrGe8AwWglyYyK?WOiFgfL zGV_odLawz=`jD9rNeV3pjYcd{FES4DWjvlvq;N(WYqask2@y=j4U;(k^oWC>=)!SG zB8!}nB_UeU!ASgg-~<|jq@zwQTTpCnL}i)~5jyb#l&eFFu+(xd9qc&85(R4#kV!V% zbn{I(VLGdr9Zf2#%8oeTBq8DA^z%h2+c z1t!>Fn?nLwWR^=l6#$qCmYL?DD@;1AnQKm%TdJ$J`o>T(Fv}2D;N=BPh|Xmw-9;lp zP{^y(R-3_At61Bs%!UDA13F$?m!>rZazZW)9~z@Eppwbsq0B0-6kWrafu@z4|E^eY ziHHq}ZYUEc@mC)#1gdhW94e|}XjYjjBFQ6iTq(#aEXZ(QJ}Mlc$0Vu@bjI6~3v|&J zUcKVhRZqP4w}Tgcc+=*LM%0Nmc;j}2pn2t?ynWa^I!Gx0(lbHgv)A4<5vEiDV3gC`2F(LX1ODFoT8YSVPVdKT2SNYq#?S=4Kb2QG|jO2ay};P&Sdz zfzW}QA&5bskqS>xf)e;*Ml3$E0zGKZg6xq90S$;i8$RzNCv-*%SNI+puI`38+~Ep+ z=s_vsP=`FcV)(rHMKFqQPtTGF0N!vAXQUz&fUDiu4#$x7wT~jE3S%AZ2qER%F(%%E zh5$2HH|gLCZ^8N)-yq?>hCC-am4K9!($b+5$c2pmvU{UIbkLcEAkPPibOs|C;>c)0 zkUKZ)NNz?HyNe*w2V}dO4~jwzllfo+Y>*2yVv(lOIfRp*6bLBIa{}ND&LKLG0*pQq z!H1CQk_MUNMqU}qS=REF3;||BgxL^c!l;_5!)7t{_)Tzzvm(yYBt)jCK!GR^C(x*# zO$fP4L#k3D)8dmj?Rigs`pKrm302kP>LD(fgOsR>e% zN{>4+!IDKo$&idHuAR4OsexcRnd$9JB1!H4XH%W}%}||ZbzmX`l;GpWXdM(M3O&!B z(DqcYhP5HB5-UeC&>3z5fDY;svMfj~&|ni35X<}8_r=xipEm#c`_1cJ#fPSmbG z&;eeZfy+^f2t*atNEs@nN@G=MPXirkVj&{fhZHui)Re1pO@)7jBdh!VaLId6LZx$9jkxhHh!JLXP{c+mkCRU@SOFHQnH5Q;5$ zju~DBgO&SX5VI+&viqztW!ceK0I0vzeb4B01DrmESjO8U>x@-LCrzp~I(ax&Og_iV zoI=lX2)VF8_Da#dN|*_i-0zT?QRGBgHd3D*CjeAYjAv8~PgQ|%Lt5ltf#lLAPBsXX zNwB)K=mKme`A{)HOGz*<8O#|WuMfU#o--RH&1;58o@K@iHG9~`gC;b7$O*&4^jIKY zZtjmEs*&Ley3mt;m9~;Q=)E~lo&tr$1q#|cTb%04PM)Tbsa284BF-{a8ugG%9pwR= zqymdQ4{#G_9?_O}&Lm|>t1b8cYQnOC70lBxNqYL=RXZft7|Hd1JEQ|)6T8C^f%dQu zQfx|RTieVTaYXJMk>wdhkOZ`0s51fruhw_l>rNGtTH>}sW75gih_9Wf6mAlw$=MT; z=W0J&?~h4XQ~Q=kzZoLtkSLPAp{-?yYg3y!JaT|tX2={U9%dtHtt=OPbv?r#V?_ds~R?cgqg>cEBxz?-pJJNZ6}+(K}UR zLeq`RBx3AKaN{^=Tvgc)zDPBt?o%*nDqTtq6O~UgYo?bGQLnH~7SgiwlV_lAPZsc- z!$$Rtj0XV2wp&T(t8Q2SUi{=4N&DKN%n-TLy=)E%JWd90bi^nAE8Q-6mNE-qcN#?p zV}MT7@$QgFD_-+(`b%IY{R9f$A@me1ghnkCt`nz;6EbzhY0_9CY$yH!1oit z_#?piGr;;o!246c{A0lVbHM(C!2esl0;IqLw7>+!zy;L62IRm8^uP!N!3hjN5p)Ru zGeHSN!4mwJ6lB5w6=aALbU_kCpF^uXMfn5UKsRQ%hHGk+*HawrE2DJVJR;1N2%$7c z8ISZktd3j0Jz<7un7)mWksxrA_h2j!pqLV>I*?*8^3s$nl#4CQ2onH~a`LBO5<;Qs zF|nbrSV;@vV~8@$q=*PZW*9?{ShkJGjAoiRliN6EE2=kShBzcJ@1uxivqC78!cTg_ zD8vj$WW-*IL`XD{+5 zgvE$B#aCQKRUE}zEX7%rMTnrqShU4j#KmCL#a`q^!ZAjN@WovO#$Oc1W+cXAWX5H5 z#%YAcWJJdQhEPVyqC<(u!b+hbOrp2sON=Uf!iA#Wumyz~5fZ6`&Cr2Z;w(37x{}Mfmh#7807z^aNX*lySJ05!FtLr$0OGTZ z$q>jfi4`cCI^^Rr{t^P<3ZRa=9_Nur02mL2WJrb($j#t^L)a~bYA%pK5a)r6iNq>| zoHLBb8JWRpt%EG+4i8!a`(kf;MAYmaDe54Y9qa$|w%o=ew zmRJ&+3zu<$gj12XJEV-F`oWuAsLxr3{Q-qgaF=F^BsaUgyh}D!+c^M;&DpHYUFgG# z(=%NF1=|cVk7zPtFa_Ei&YV)D=tu=jI0Q=InwF{zXgE&fOrSP`2uv!V?hGl|ydBwe zPT`!S^+^Rvu+BANrcT6&Q8S=WfX?J&!$X`x8{$rPv7ztOsp)J_N_fxUf+N0RPxow$ z*7_@i03L}@O$Ut#2(5?-rHBfhhzott2Bpvqwa^a5&=1v67vazm_0SRp(GwL>gNRW7 z@C;E2b@YRU`R*4W+R-MycHP&B6)?iiEVP)20b=6~q)?}5|Wu?~tX0_I5 zmDOm~)@kL|YW3D@1=nn~)omr$ZZ+3$Mb~gu*KyU=ICWNtAl7(=S6HQ2TD4bP#n)Zc z*LTHM$-=RUzyN$}o>%Y~(IT6nf{4spQ-)>JJgPbK;Xy^Sq&L_FXJ`dV0W*$#G;Csn z`b-O8;Du2@1iLyifZNTTv=nIj*vk}IlD$7bpap~Bm`#-kXzNdC0ESgS1lte-Js241 z;FnQ2gv|gLO=y_Q473fHznGm_p3N+eeV3LUSMy(-OM80&{f^jh27Sb-PdK^*~Q)4b=_g2-Q49}Z1UaRwcXy;-P7Vm%-|ZY z5rs5ptH=YBjU~^B(7{4%SnH(^OEbC`_<$FfSdQZf?}dntdx7n3!j~}Lhd?Cs{RmaU zIfy_JVVPL1Szn4!-;C(EF|ps6px>96-;2m!fmmMv_5uE7hypfXjX2-{PGAPU-v-7A z1t#DJPNfO9hzP#m3a$tYc3=*M;0&(c59YTJF1Qhnhz<7O4yNG$6t>_J7GJXW|A>T4=@g6Fh+zM`C>6XB0-L1DLxBNT?R{z3WJP^ zxVYqls9s6_lqE@yO3XLVj@b`G)5yR>$WXL+7ya1uy*4(D^e=T!lKLeK<%VGTXd z1V7T}zTjtnCg^>BLdU3OjBo;4undOw9n|4tga+b#hUTY;3Kz2uLm(J_!Gt>C+M%ek zhsYU(U}Tf1D&|s+jJB?CGHK6n5R#rJsBr0uwh>BJi%h07^@EFB?qrd^=^^fB(!6GQ zL+R5nBSp5Mg#8G~7?s0_=w1SxPNry$h`1g?X1h4*P_}9&d7Y|m5ZuTJuEvO`7Haxv zjn{~c*>Fgrf@rt^0N(Ho7(;6~?qzLz)N5<%uy7CmzA}z~W%O-8^i}w(H~QkNN=s z6$6&TtdIgZkTLRV)dtOrp0QJH9=8sQQF{^Nu*k!(D*!lIijWkj=FMTDwU|)pSdeV8 z`0d2*J?2&xjcV%Szb;Tog=9TxE<{^4xvjOI)5PdTLmPm%IYZvlt3 zVVLockZ%NMlIyl^l4}b0aE1?;h(l2v(FX4u?}#5)an(R?kPy`u_ERS(;_hjQC#JNz zNe1H!9^cz(kWjdwwg~XMhzL(b^T9nvLmg96wo1Z<%TV07uZ5v>l=zXP1$Ym!o-_rI8v^ zvX@Ngg<@FVRcDBLxtDy|mw5SSNmmO0ls4Li;TKQ;>54IR590K%c{zK*7k!bLXBUZC z_x1d`Z6`QGlNib&(;`{Np=Ct@i_u^NS~Spqw>D`7SkjbB@daAF&c+} zX-ba>Qnws!H*~?snyu-YPEV7A<(n=)-u8BQh6QAJ9yIewQ}NMmpm0#I-tJmg=!$7R zOD)ikP$#88CuLtd%Mta9pqwGoKFw*K7JDDV37w4|aL1d7m2a&T7$U=Y_n}A1-|0W2 z*XeAWbfCD3k|)G!xd~8*4aTy0%h5iU-yBh&dd1X;q<@GFu!H=mn3Zzw*IqOz+4X^# zbk6yCSPOcy5qT@k2yg5g!~O~XxuF~lCv#9&oZJaN&4>uCPn^ou_`ImCis*WRT_FEj zAHIG_e^-3#MRX858y!!H8d4lBs-hlh2p|%ogf|;D&>#&W1O>H?7;2z>0eQGMqbN}$ zHd2@gs-Uz;h78&u)cc6rA0iAweGa-9P5>bhvbH-kAxL#17CIGP^FzUQeVHLHzi!Tf zK?t|s&FG(>*N-9_`g|^$RK+nM+9ViAI{W?UpZ^IUh8X=qID{cW>B?}1Qy2x>yq(2C zf`<703)&!nI8>+*q9#5L0(ir0p|ez>WG!UY%HY9+Cw4{TWop!>O`(8Aswi?K$&qD> zEhILIk|UJMk^vBz5G6DJXB#!C!pQ8^i;8oWF^ffE0=1W zVH8UOn6waDDo>OsX<}tmD%p`kFb<1aR_3H)Fkk4<>I_oMwPmwJtT`%_*oDLD`dFc7 z;GsYZm6aVc&@eM%0t-DZXm>K@%9bx<&a8Pe=gyu#gAOfvG-agBlDS!%Fk-`p5hseo z%M6!DCn%jo>RHw_Owzu80}n2IIPv1fk0Vd6T)33xQp8AEE`2(6%>ZN-Bj%|PfIQMq zKuqR+qz)-Xq*7&OmF)SWZ&3CYdA4Syt2{UXrM=lUGia|WZtxy7&_E(md+7~>g=hz& zWDQ_;>E=TgxBVgi(n*a0P}c@*pae}U4_1_(GFOo_pELDUbYFh4-G-tM8vx*xe-WDK zq7N$0CK7n3jVDrh?H%NSRyA3Y1_rvlkl{!tz?WKO2$EnfBGzAPZz~~~0S|m*f4VD1V0gW?%e2^$g#3=b9hy8sRXjOw|c~E*>R@bVlufiIu ztiK`UmXl<8`QJeS7BgUh6w{@K8R$cvb0VZ}#1p@(CQ9SlRcs`~+O?xKQ&?6m zSxhWs3&0x;yyc+&0-UbFB-MK_p6-4i!vFmE2mB#-1aXqnjky#k`j ziVvRuNKJ1~#-v+@9s~kJo0eEr(4!%^`(;ey5@E0?S7h2;X*0~sh*GUik`AsrQx^{I z?tM_tJXr*hHLO4XdPSleu3F!ei4ORarIQ6bNvGqU{4lg(_Sjv}zR%H(-D)vfE%q=+{0UTnHB)u+r!#!vVxykcC1VA`y#dM9E2k za?Pp25u2Eq2m#;;E%6KdgwwS)!R~4#vrE^YLMgx8!4G!;Kp5{(2(4I(c$V?gzeuwG z0d+wwU(m3I8bdQ5W#A7hVgP_203ZhS1#V{}d1J3iakcf7L^i`HBN@eb#{Qg4LWijw zoKUA5MKZFCk4$67I<$%``oM;Oik-VaS;8%TF-G5inLJdY2bOtsB0JmZPMH9)dLx;o8*NFfMGfh5mbuPpmi5km0yLlkZI!i} zGth&MWKI1kMFN3Cf*}D#7vHiARf=Ja%Dm?^pfkzWI@(IjFilt`$x}B&RE%Q(#Ij82 z+YJCzQH*DFRHO&V;YhV9pOIyaX&%jsD??gGjXe!mJMES^eelqQM0BDg0pCVb)}Shu zrJ@&2A4+5@l9~lCc~g~&Piu;??>%)iIvwgqacEO|*(pLriK8*cQoNSFG^RgQ;PS|F zRAwY~nv*$QGMcJ2w{;RVtE@>L=2{t=^6aeJwCD*>Rh2izT5fl1rcc`Vx?r@`9OpW1{tv=u`6q)N$`6>{aQgJP8 zxAMu((61YQwODxm5JTI{$VYO4#0Au5*0i{2- zP%j7&w>=c&ILA7+TtQL0;~@qiJ@+LJj10jo@1dt*Yas#RzFHFZlBA)N>1tx7t15jN zwljBJr-Y=Lp$rveUWg^il(d-B-&RC;ANFlfbOU8RNu|g~j-LG}RvFP*gMo)+;Fzyk z=2t3jom6J?yzs2&%)D77YG#Jas?4$Z8W62*4xE?29Ofi1a58fLW{rADQcaR*V1$6G z*>3X;(m#{g%G;#wTDJV-SHn6qE`(ua8GTu8J~yN4=`9D+8|z;KJJ{9fv9MPNvQC2r zNHOZw4FR1}vSiRN>+};kK_i>GYPgW6{0)hfYcVvYZk|A~>NrJ`0ag~(q>fD_ZA&&X z)?M$C?Y4}vWpqOmAg&t^lp#dSM;TX3<+*j-0J=%iZCM^>P;t@r_xjD{%ut&$)Mgzq z1*qzHcMNqP%EGwkx+==lo7v&Kj1Uk9nDd>RdjpNV%jY}egFe3XhDi|7qN&ImB zL}TCIbC=ljcUxR91KF`7-RUWvJ?b!6YjXWsIqlAV-Du4IOB z*+b22U0qc20NtJJ)WliF>r_=WKaI5{eXVn1n*x#A*>+XbY72)(-1%eG!z|Km}eOlSnYxLX=cV2nkAf(P#*X!92(W+RQU~8)xuU*_j4`)R6|>la<{bP$=L*m_T@R zpbv~7Gn8OioFJotNYBJVMf_k8iqo1&(%>~;E>#8xBAlu*jd`t4p^(K8;zA16jH!*? zXPIDI0O9j79;*3Qgt*<|wbc>oTz^2q3^Jie?BHqi;9Afi#KA|!MAx(VK+e$K|9F@U z7DW!;2oKuD3sQy!V&GaR;^-YjBOYRtEn-U^B8##_ z;4va&tPxu>HVYs5l@KrnZarCEAQ)!wz#%-)(1c*|m|VD73;+O3ocK(u(Zn#+!Gq{P zsAxv697;xk#7`j?F@i>@RTW^QBSom-U(8eO0o}YnBT9fo0(q8yi~vG_qq?cbIr@k? z`hb_X$yEWxf2Bu4q9a3IV>YTwNnMOHD3wga&&upmH`Yal0Lg`biiX%0sZ0$$65*i$ zz*VrLH@RaZft5wIpk_>@IacI@_{q9V$`dph8y-Zc`Neq=7(&#HKC;N`NyR|ELP35b zkX)pvWh7@1B{bN=q@<*!a8agQL*M=Xjv4;rqvXciX`DgQ5=+jQOQOnHRbzCanT1f} z0nO!FxFuai#9Pi(U6#dN+9hAsT3o`Vs)1Nu;-z5jrD6W1V)i9qa+PCFBVz_8VRDpY zKBi^v-wVYMu5?H@5e`7oWE0NI6JQ6U9U5Q8i zLe`KP5Q4_Jqyi-z0wuVH8nPKP=z=MP!Y*)DA^OE3IOlUZr=bAAh2WJ&fI=wb1OSvk zYrkb##~jdvO(f@5_twb|5fo&XMmpPfNEz$-o|eR=Wx216ic=TOmQHpYXdXYp{SiGHVy70pR~C};d9Qf(-K$^@x7!(JR& zq;(lk*hGCU=*+RFdr|^?Qs#gm6@RwqU$G}}3MaQ*C}t>>1RcT@nWkMV++3t)4bCT# z_6L4?m2o2HlUC-6qUUm6RbH^xTI}YVmPMS}1)ajFo7O3w-f2Y0DOuR*oa!l{@@bw9 z>Yh@dq4Fl8_Gx|ws-HG0qZX>59;&1!Dy1%JN&KlB2Yhrqou8xGW zR%p=*CmUfg-Rs^zs z&A1lq*Fcv@SfI8JX0?84Y=moUWo2z;O6!GXx?ZaX@+!ZQ#J(~FSk~%R-0DcI>_XYa z%R)oTiiFJ4Y(>~?%-X8XvMkTS>{{e3(C)0z_AJr?(hjZC7A?~rEmm zHZ9dYtyDybDWkGWPsXAhYmb}A>;&5 zKuSk07`Qq^D};vJ((Nt)#WDaxDo6?!*#Rx|3NNGrB23E50&7XQR&v@cFN^}?eg+RT zh2ZjnD5ODujYK>?-)6X$fvk^nTu< zVa4Gl?&400Y^;JFu!5ruE+S-^H?aZfnl9?DPv#CU=X$P1G>G?#E*nVhqYy9az=-7D z?Ht(4xFUomATIUl{{r)Zi|Vqj_QJ00wjb|?FZ_Db+)l6H5^m^DMi2-AAh3enrUFBj zh1sy(`i@`i+AZ$tu4;gu?Ysu@e8V5jQas4>4H?u@fgT5kD~#SFsc?u@z@=-d-^mN3j=ou?W#&BFgXP zcJ45lAyRIZ9?p-AX)xT%@f=6XGSV?}P{a#J8CmSHX5{ff^zrpdg$Z9qAQ$pM1OXxI zv2jGmW)89s5YJr2kRkIiAX~;BZzd&|L<0S>4`?!F=5G9+hN zD39`INLkwK|4E$`-e(L;B!jXgk3=WGGG+w9D8GOx6Y^wCGG_pDMX2&BSH>}4GAb{# zGv{(MOY<{Nb2L-)HCuBwPX;q%^ETHO-DxH@C-P>PaIf{#9iwwPPm6OXkvhBcJ3m&r zk=9zlb3NPh<)~P!nC(6Db3YG<9s6@Y3v}a%s%Qn!KqGWQ$I4{YVcr1I*(!8I!}9~a zrbIX37(i!6YxE%?!f;^pMuT)mTQp|ep|pH7NNco6iw4Hw%}Jv)bgDFPxb#aqXH1Xu zO>2wsNWzzWnR0wtAF+WfKm*d@bWz_kKpXYa5t4NshHxx3Q&(G3tMQUD%TrrdRC5L` z6R@^e|MhNRHE47-S$(xslXa^gMT{*&S}%j_701JV<8@0LAxGDAixon4X1l7>IY+B% z_Ax@`tAzGvv(RYUvMw8uX;+78lXhzpM=!f}Y$r5Q%XV$s_HE;KZtM1L^LB8UmO=G) za0~Zv6L)bN_i-yUZ6o(`Gk0@4_j5ycbc>d9OLui!_jO};c5C-`F9&BgN7Qk5c#HRV zlXnEBV|kJc6oMvHf_%Gn1my)~??>k}T5S!%ntQQH~$SnMb&qm&n<5#)|7@x|HMd@xY$< zxtOyi`8k~G0iw@YqDd6qp*jGab6iYh}w|hIP&s?N4ouE{QwP&c}W;-a|M_z-hWn{V( zcl)=e)0W)2v@;z;ixu)^#?hUZzZ?9tdv|Pi4M(DTu&oOIs+KaD*sd=ilu*aQ>$_B} z02(2BATGL=A^X8I;Idm=U?bo|i+swj1+~vL%>czPVB%`V-c173VV@YlF(A83hs&!+ z%(L;jXvJ67ue}TWZ{WPjOC7+A#=YA`vVbAdL%m3>cWVn-!)FEni~*YC|5Yy}a2;Gl zhv&st>vy|ig&x!`q-Z;6JopC>ufAu1i|^FPULOj-ZpK^8>PC1mV6bFVU{mP*>tX^0 zABEWiF4|`i05}3L6vN#z##X!c*+&ZOV7=Ba1J{d!*E`H1WI`|K{rvJhfB?WDgoWZG z@O{_Luqw~{(f!?m%iI5JNBLs$oUz@%Na&BgOF)Y0cgEuve&o-e9n>x0BLZq|jH3-c zns@%<>Y2swM%G98)^|O`lSS39QLnT@m(oQb`a%q2un2>FL}0b+lU3|TJ+SFbGCR!d zzYVb$3s`a^pwFX2a6Byckm8$cPI=KAgBB z5Ke)yCi2YaQzC##LVZyb5)|jKmI-;l#EFZl)q&OklsuWT;mWB9J<1e1%t}OyUBu$x=NYWwd`T3Gf0*r z!W{;ntU=CnPvV5Os%F-VTuGLVJ2UNQ*|TZawtX9SZr!_i_xAl8c<|l;I2DWL#`4;X zZY7qUo6reH&2>3&VwS9{aP8Z>+&7#;2?? zl_yG+G_hjl7oipOD5Q`>pi*WsHV8aqK|)}{uDO(m8HEx@Fj@ve+yHoEAZJnu#i~{$ zQ;5Jombs2W1_{jWwk!m4=9E!D2_+?HT0tTyP9~%T6GK=@$(Pv3nnNaMOqr3D6aR}) zpdBq_216EoP)b5mLZOAA!y+n;87D8y5TW~^$#1{@mXwGG>jZ*Emm{xS<~f8;;7cH0 zOc`a9O+o=Cl7s|Y@Ibm;EGWhqYrJu%5}Q;}MMOrNlEW824DnGp>14)EJoV&LH!i(| z5==-_GIA76RH0K#gyuL=Fo{+@Ga=%3?D0o|f+Xp&|CuUHYAO8m+U3W6107@;x*TE$0Tm;pu?NrVjV1cs1` zNCzp3z!jrmc8%!cq;A@mSZ6AEG|dy^tgT+=d{_$_Xf{izX$cgBbSEh;+m*tgi>X`xfaiggDQkmtd<%%fd>+<5*$ORgpPF*#ehcE5;CXjoZ&Y2d70BdQXh#B%NjWH z4HE=ni`C6+4g#^p@gjnO3_=4yYm-Z}3KI)_QE7m?GGHbt#)Q#=@EXm4t%K|FEE1GRTY#j*c(ZxFD|tvcSSPP)-aXB5+Vh zKuaC4hPK*O>pE9L&HW`X;UkF$H6pBtO`p;VlRIgOoZSdhj&AxVXDc?iFDH$EV8C$COOQI^^z{P^hy;c z5>3%1L}$$;8%k=4%+T$#Sijlk|2ct~&MXE~jWfL5P|ip>kaUhTRaB!POGlD*a*>t9 zaOELjDUeWQAcGsjUFdd)o6>MEM}7R`GM#8dW~vgVF_r0XigO$cn2w*<^npL6=^PLE zt(GOJ&UJ)2Q==XgsYwlw`%tOWn5N4hEDA!J=qbMdYGf5frN|LEGrwC&WKhGngCFhy zfU(}85KFOL5N88ADOSXWt$O1wpkWQR?k12)`3N&gK@k9m0RVpZ>oXGqI;l`{DEJEN zSV8rd2uEvt0l={<#*L#=LAjwQ)!V)okC2l7Q3 z@I;7P6}f@9o^gezwM}Nr|9aNPuC-_yysHn$c)7qd)M=NU=#&5}STPV5wYrkzRZpW^ z+CcRWQl;wH@QD!^@-ZBwbyii?Vnf%mw1@G8t!$mzUiUT$B^i_3p)9vUf?l0S7nZQ@V~Sx7*_=$ldwb15&P$utp%k%( z868Su+q3Nrw~4qQn%q=TjAw)d#I5tia0`>F)@|6eAEt3&G`wNHM#RAafv|)rLdy)( zMR8k{4@xZTl@m`S#cGr%NpwsR&H+@(zPa&ha!lUMB{m_ilIu9#$YK}6c(^_w9S407 zWGFEs!q(*sf{$FU{|p^VoFjEc9?~ouzFC&L%dD_VTWVlKA6jA3izJmRnC2^Mk03gn z%}^PPXiHxj(+o41dUTNK_4%_KM^;QS{&3Uq=ztBiT2p8plUjCuEqj38@bGMdxdkHR zCb%eRLMrf5->h~^tjZ>N!f^9FH3|@q`df>*@XvzMwm_*6`ftQle!GyRAGnk2vTQ`Ex|1 zZuG#dx9A`Sbexj>_S%&F=sjK{plOI}?yi)`?Z z`J4s)1O+mBskp?>iXQ5O)an(QB@4ta&8Dm_;1Bu4OE!Q{Bfux6L_-JYVA?QlyzH*| zybp@@&*FT|_hvBnI*$Jg5FxA&8qUi(bYKi3C;#Yz(r}Lki_i!K%zfD72>qq;E^8u6 zKx2*!A%tM8h>$i^0EPVR;+U^qqQU+OD2KjKHw-AMBm(7_;Tm+R0-^{wKyONti3Fu5 z`Md|&c&%=9N};|es3w98iAUM0DGYN13vK8*JfJv4%$+)EHnz~oSWq!ELyZhb0%tsu^iFy97`e{*>N4+u^ma`9py0|b7Xqt3x`6d zSNi9P7o?;KooHn8Y5LTyb+!!^NARvP5+OvW6bXX@XY#5zPE)bJ2A@eX7^D!y2BqWnDnZYqH(=tf{Gb^((BeOH#Vl*ok zGjlUNvoz79H3c&@4N_m05+|#ODmCa)PU>T(N=+z|F2?6Np7J4wlQ@woJx(b-GN3rc zCG$v5NB|&m?#1;6LKbO*2M$4}P^cDTCHO8dOaS15Dkq@gDJ6!X4yx^U|8T>4a_}bj zWVp=DH+iFX(sSn`r|?d)TCS!JcZl|;1OPOFPDC+RX5u_KCOr`%W>ACf%n#fM6g{Qn zI5C5}2RW6R?7 zP3v@;;M7j}bWim(q5!q%-1JWSR8Sf1P|tKuo5N27l~5aXP!|;y|BZw7=z>KtiA!I! zDC6V_WMO;W4wA5PIY*ULpQraq6+WDRPBe<6Qo zLSYvq5-w)q5Qddlht-$>fJ>IaPG;m(7R@#qZ&a*=PDoB~AV~WN!bq-UAY74H+yxVE z)knMxQ{$p@kToTg)lHf;&t?xsqUBT)Z%UGNSwlfxBZ5|K6;~hRnyR8k=Cxj7w8A#= z%LJlVf0gZ?r68;XOQ>Z4Z;Tk+#9(8k?h@8l*W^_*G+=|nT!qYEe>KlI&l#dcD|8qlCo8()cM2Y;)`ZPmS z2Zc>gR%Mr_BzRR?B{EGPc1CX2RS2Rf>LOOJmM*f^GPo9Nt+s2y_G<@1YfHjww^nS? zmTbdzZO7JD*!FGRR#xSGLmq|ic!No}gH8B@Q85*7 zad?I;FCQ%fc5inhc10$yrjLYBU-0*GlURw}<5ZVeJc2+703k^xCKYV-oRHTUTERDT z;0X-jR8C|WfFTuT$8ha{76gbFQUMW!JiiocDI($ML&(NtVnn?Zw}c$uAjnIurn zn336?uXq=-m@_b`p|#jDAljiP8jB@5;4b>1HQJ&hdZRtMqboY2L3*S`nl30BqfeTn zOFE@X0;NCNq+NQYRhpz}I;IbLre(UO|8@GNQ_5K%Gmo9Qk4>U0CDxH4`gth%caA_%zJ2VTId|4xABmou6sh{QTfK4KXl zfvv}yE=;Yg&2A#F8X@XhHpuz|^g8h5?yTe5u6>~OJO`|uj<5Z?H}3L}!sirwL#-1c zuCrPq@LI0BW2$N61^QaB=lZg5L$Qgbunl`QLR+&>yR=bzuT^_AOgpt-d$nP^wON}p zTsyXJd$!qW^+ua6@FlEqgR5;XIHQ`li~Bs9*tnCMU-HsBsOo<;j=B9y%#@qDtDBK* za9oDay0@FVaU%(#y1T<$JlwE1|Aascc0~w!#VTEnyx%Ji#T&lkyI&NIHqRqej2U1WTbCEdj>B4?MsV{5RMS zdM+Hpi&ew7!^1y(SVO$OPkeo#N)i~)iowI`1d9!1L6eXe#cO;xk=w>m&$a>>C!{Yp zc6{2x?Ieu!-Gst-pqb^`n_qx@+I)OB9=my%+{c{!H>7;Ss65BBoM4DUNtVIOm%$ZX zJb=D@E9hCv&wL?Gsj1NX<#4BtUZcsj!_9-d$%Ys>cz_VzM4n^$^rCyd_iEbW+&4-H zdgeUPyTj0ZOwie!(V6fN{~O}1IPR(%!X+Es(w7s*FP#RJbX+(+uealZ6kF3t!qXx9 z(?_Y)<0I8aoz=@l)LR|axjWNioz`pJ)^8oxb6wYWeK+=73ObF~gI(B%o!E=r*pJ<~ zpBmYho!Oh+*`FQSqy4>RUD~VN+OHkkvt8S_-8;UU+rJ&$!(H6Ro!ptdA7!oi zr=IFV-q(S>>a$+!x1Q@6-r~C+?89E{$G+m3itNuG?bBZEo!#5l-tFHW?&Dt5&t2~8 z-tO-n@84YtkW&ieJ@5Y>@B?4)i4*Dv-|!C~@e>~j_gnEB-|-(G@{QB%BcJjs-|{d2 ze7+v@H=px6-}B+4@IPPlN1yadzm!bg^iyB;SD*EL9p+gd_G4f6BR}+K-}Y}G_t75c zbD#Hn-}iNY@_%3WhoAVHzV?eB`IBGyt={F+1M8O``lDa^IbQRp-}l*{ny{v;XOIoAO7QC{%w8x=imPCAOH2f`SYLu`yU{D z2pmYTpuvL(6DnNDu%W|;5F<*QNU@^Dix@L%+{m$`$B!UGii{|wq{)#eQ>t9avZc$H zFk{M`NwcQSn>cgo+{v@2&!0ep3LQ$csL`WHlPaxfQi>QUOQTAiO0}xht5~yY-O9DA z*RNp1iXBU~tl5lBOP*cJwyoQ@aO29IOSi7wyLj{J-D|X|5U6|u3m#0ku;Igq6DwZK zxUu8LCI6PJ^0>0)%a}83-pskP=g*)+w@eFIwCU5RQ>$Lhy0z=quwR>eS~|Av+qiS< z-p#wW|L@$dfBy&b;~Pw9cbTpH98H_3PNP+l_2ehxYH_!;2qJzP$PK z1)Dxa&%V9;_weJ(pTBoF{rmXy>)+46zyFSY)%M?j1QuxEfe0p89BGsV2S6sO@S@5o z5)NYpLJvkbVTBhSl%a$as$wCA5Q4}di5iADqK74JC}M{OedywfF}A28j0MSfBZ?-z z$m5JS5~SmaET%|gjYj%dWREoFXycJiCfTHrPb!HdlR*wNq?J)(`QwgD0{JDBR7#np zKwD0!WtD28$>y0pqzR{-V{%!hmtlf=rI>k=xu%(Qwi)M}fzC;2pLYI<=bn0Y$tRZ%In zx>iZFNs$3@9B{(Su*4?g213CSTkNq1C95nm#v&{1v(Yv?t+Le`?JV40{Z5$Qn;va>X29Y;ndbZ#-|uCx0Aq%p=3x zvdK52{Ibd^v)r@G2;2NK&O_@QG|xNxoHWo23%xYaPaEAd(nl*@HPawFeKpiu|4W@U z)l*v?HrBe9EtY)_k{qMjIkGLJ+gienrrdGT4S?NvYdyE#b@P3<-+6=Gx8Q#h9=O

`R1gLZhGjCqaL}}sk6TN$F0Nu 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