Spring Cleaning: Refactoring the U-Boot Test Suite for a Brighter Future


A robust and efficient test suite is the backbone of a healthy open-source project. It gives developers the confidence to add new features and refactor code without causing regressions. Recently, we’ve merged a significant 19-patch series that begins a much-needed cleanup of our Python test infrastructure, paving the way for faster, more reliable, and more parallelizable tests.

The Old Way: A Monolithic Setup

For a long time, many of our tests, particularly those for the bootstd (boot standard) commands, have relied on a collection of disk images. These images simulate various boot scenarios, from Fedora and Ubuntu systems to ChromiumOS and Android partitions.

Our previous approach was to have a single, monolithic test called test_ut_dm_init_bootstd() that would run before all other unit tests. Its only job was to create every single disk image that any of the subsequent tests might need.

While this worked, it had several drawbacks:

  • Inefficiency: Every test run created all the images, even if you only wanted to run a single test that didn’t need any of them.
  • Hidden Dependencies: The relationship between a test and the image it required was not explicit. If an image failed to generate, a seemingly unrelated test would fail later, making debugging confusing.
  • No Parallelism: This setup made it impossible to run tests in parallel (make pcheck). Many tests implicitly depended on files created by other tests, a major barrier to parallel execution.
  • CI Gaps: Commands like make qcheck (quick check) and make pcheck were not being tested in our CI, which meant they could break and remain broken for long periods.

A New Direction: Embracing Test Fixtures

The long-term goal is to move away from this monolithic setup and towards using proper test fixtures. In frameworks like pytest (which we use), a fixture is a function that provides a well-defined baseline for tests. For us, this means a fixture would create a specific disk image and provide it directly to the tests that need it, and only those tests.

This 19-patch series is the first major step in that direction.


The Cleanup Process: A Three-Step Approach

The series can be broken down into three main phases of work.

1. Stabilization and Bug Squashing

Before making big changes, we had to fix the basics. The first few patches were dedicated to getting make qcheck to pass reliably. This involved:

  • Disabling Link-Time Optimization (LTO), which was interfering with our event tracing tools (Patch 1/19).
  • Fixing a memory leak in the VBE test code (Patch 2/19).
  • Standardizing how we compile device trees in tests to fix path-related issues (Patch 3/19).

2. Decoupling Dependent Tests

A key requirement for parallel testing is that each test must be self-contained. We found a great example of a dependency where test_fdt_add_pubkey() relied on cryptographic keys created by an entirely different test, test_vboot_base().

To fix this, we first moved the key-generation code into a shared helper function (Patch 6/19). Then, we updated test_fdt_add_pubkey() to call this helper itself, ensuring it creates all the files it needs to run (Patch 7/19). This makes the test independent and ready for parallel execution.

3. Preparing for Fixtures by Refactoring

The bulk of the work in this series was a large-scale refactoring of all our image-creation functions. Previously, functions like setup_fedora_image() took a ubman object as an argument. This ubman is a function-scoped fixture, meaning it’s set up and torn down for every single test. This is not suitable for creating images, which we’d prefer to do only once per test session.

The solution was to change the signature of all these setup functions. Instead of: def setup_fedora_image(ubman):

They now accept the specific dependencies they actually need: def setup_fedora_image(config, log, ...):

This was done for every image type: Fedora, Ubuntu, Android, ChromiumOS, EFI, and more. This change decouples the image creation logic from the lifecycle of an individual test run, making it possible for us to move this code into a session-scoped fixture in the future.

What’s Next?

This series has laid the groundwork. The immediate bugs are fixed, tests are more independent, and the code is structured correctly. The next step will be to complete the transition by creating a session-scoped pytest fixture that handles all this image setup work once at the start of a test run.

This investment in our test infrastructure will pay dividends in the form of faster CI runs, a more pleasant developer experience, and a more stable and reliable U-Boot. Happy testing! 🌱




Giving FIT-loading a Much-Needed Tune-Up

The U-Boot boot process relies heavily on the Flattened Image Tree (FIT) format to package kernels, ramdisks, device trees, and other components. At the heart of this lies the fit_image_load() function, which is responsible for parsing the FIT, selecting the right images, and loading them into memory.

Over the years, as more features like the “loadables” property were added, this important function grew in size and complexity. While it was a significant improvement over the scattered code it replaced, it had become a bit unwieldy—over 250 lines long! Maintaining and extending such a large function can be challenging.

Recognizing this, U-Boot developer Simon Glass recently undertook a refactoring effort to improve its structure and maintainability.


A Classic Refactor: Divide and Conquer

The core strategy of this patch series was to break down the monolithic fit_image_load() function into a collection of smaller, more focused helper functions. This makes the code easier to read, debug, and paves the way for future feature development.

The refactoring splits the loading process into logical steps, each now handled by its own function:

  • Image Selection: A new select_image() function now handles finding the correct configuration and image node within the FIT.
  • Verification and Checks: The print_and_verify() and check_allowed() functions centralize image verification and checks for things like image type, OS, and CPU architecture.
  • Loading and Decompression: The actual data loading and decompression logic were moved into handle_load_op() and decomp_image(), respectively.

Along with this restructuring, the series includes several smaller cleanups, such as removing unused variables and tidying up conditional compilation (#ifdef) directives for host builds.


Test Suite Improvements ⚙️

Good code changes are always backed by solid tests. This effort also included several improvements to the FIT test suite:

  • The test_fit() routine was renamed to test_fit_base() to prevent naming conflicts with other tests.
  • The test was updated to no longer require a full U-Boot restart, significantly speeding up test execution.
  • A new check was added to ensure U-Boot correctly reports an error when a required kernel image is missing from the FIT.

For a detailed look at all the changes, you can check out the merge commit or patches.




Streamlining the Final Leap: Unifying U-Boot’s Pre-OS Cleanup

What happens in the final moments before U-Boot hands control over to the operating system? Until recently, the answer was, “it’s complicated.” Each architecture like ARM, x86, and RISC-V had its own way of handling the final pre-boot cleanup, leading to a maze of slightly different functions and duplicated code. It was difficult to know what was really happening just before the kernel started.

Thanks to a recent series of commits in Concept, this critical part of the boot process has been significantly cleaned up and unified.

A Simpler, Centralized Approach

The core of this effort is the introduction of a new generic function: bootm_final(). This function’s purpose is to consolidate all the common steps that must happen right before booting an OS. By moving to this centralized model, we’ve replaced various architecture-specific functions, like bootm_announce_and_cleanup(), with a single, unified call.

This new approach has been adopted across the x86, RISC-V, and ARM architectures, as well as for the EFI loader.

Key Improvements in This Series

  • Unified Cleanup: Common tasks like disabling interrupts, quiescing board devices, and calling cleanup_before_linux() are now handled in one place, reducing code duplication and increasing consistency.
  • Better Bootstage Reporting: The EFI boot path now benefits from bootstage processing. If enabled, U-Boot will produce a bootstage report, offering better insights into boot-time performance when launching an EFI application. This report is emitted when exit-boot-services is called, thus allowing timing of GRUB and the kernel EFI stuff, if present.
  • Code Simplification: With the new generic function in place, redundant architecture-specific functions have been removed. We also took the opportunity to drop an outdated workaround for an old version of GRUB (EFI_GRUB_ARM32_WORKAROUND).

This cleanup makes the boot process more robust, easier to understand, and simpler to maintain. While there is still future work to be done in this area, this is a major step forward in standardizing the final hand-off from U-Boot to the OS.