diff --git a/content/posts/rustEmbeddedUnitTesting.adoc b/content/posts/rustEmbeddedUnitTesting.adoc new file mode 100644 index 0000000..7489a48 --- /dev/null +++ b/content/posts/rustEmbeddedUnitTesting.adoc @@ -0,0 +1,115 @@ ++++ +title = "Rust Embedded Unit Testing" +date = 2023-10-28T18:41:37-05:00 +draft = false ++++ +:caution-caption: pass:[] +:important-caption: pass:[] +:note-caption: pass:[✏️] +:tip-caption: pass:[💡] +:warning-caption: pass:[] +:toc: +:toclevels: 6 + +Ive been messing around with embedded rust recently, using the BBC micro:bit as a learning platform. +Its really cool to see a high level language achieving the same results as low level c. + +However, one of my favorite features of rust, the ease of unit testing, is a bit less straightforward to do in cross-compiled, no-std projects. +Obviously we cant run tests on our local machine that rely on hardware only found on the target board, +but most of a project is going to be logic independent of the hardware its running on. +What we really want to do is be able to unit test those independent blocks of logic on our local dev machine. + +== The Root Problem + +The root of the problem is that our entire project is setup to depend on our target architecture and its hardware features. +(you are using cargo embed, right?) +As long as there is no compiler separation between our logic and our hardware interaction, we cant compile only one for our local machine while leaving out the rest. +Fortunately, that realization leads us directly to... + +== The Solution + +In Rust, the minimum unit of compilation is the crate. +This means that if we want to separate our logic from our hardware interaction, we have to put them in separate crates. +Thankfully, Rust has a feature called https://doc.rust-lang.org/cargo/reference/workspaces.html[workspaces] dedicated to managing several crates in a single project/repo. + +So, what we will do is make a https://doc.rust-lang.org/cargo/reference/workspaces.html[virtual workspace] at the top level of our repo, +containing 2 or more crates: one (or more, if it makes sense) for our main hardware layer that is set up for cross compilation and flashing using cargo-embed, +one `#![no-std]` crate containing only hardware independent logic that can be used on any architecture, and do not have cross-compilation explicitly setup. +The latter set of crate(s) are the ones that we will be able to put unit tests in. + +== Implementation + +Our `main_hardware` crate will depend on the `independent_logic` crate, and our `independent_logic` crate cannot depend on any crate that is hardware specific, such as any HALs or BSPs in use. + +Lets say that we have a project that looks something like this: + +{{}} +. +├── .cargo +│ └── config +├── .gitignore +├── Cargo.toml +├── Embed.toml +├── LICENCE +├── README.md +├── build.rs +├── memory.x +└── src + ├── calibration.rs + ├── heading_drawing.rs + ├── led.rs + ├── line_drawing.rs + ├── main.rs + └── tilt_compensation.rs +{{}} + +=== Separating the crates + +`calibration.rs` and `main.rs` are the only modules that depend on hardware features, so they will be the modules going into `hardware_main`. +The rest will be going into `independent_logic`. + +First we create the two new crates, with `hardware_main` being a binary crate and `independent_logic` being a library one, and we move the files to their respective crate. + +Then, we move the existing `Cargo.toml`, `Embed.toml`, `build.rs`, `memory.x`, and `.cargo/config` into the `hardware_main`. +Then, we create a new `Cargo.toml` at the top level, with the only section being a `[workspace]` section, +including both of the crates in the workspace. +We edit the `Cargo.toml` of `hardware_main` to point to `independent_logic` as a https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies[path dependency], and remove any dependencies that are no longer used. +We edit the `Cargo.toml` of `independent_logic` to add any packages that our code depends on. +We edit `lib.rs` to declare all the modules in `independent_logic` (and declare the crate as `#![no_std]`), +and remove those declarations from `main.rs`. +Finally, in the `hardware_main` crate, we start running `cargo check` and fix all the imports that went from being `crate::` imports to `independent_logic::` imports. + +In the end, our file tree should look something like this: + +{{}} +. +├── .gitignore +├── Cargo.toml +├── LICENCE +├── README.md +├── hardware_main +│ ├── .cargo +│ │ └── config +│ ├── Cargo.toml +│ ├── Embed.toml +│ ├── build.rs +│ ├── memory.x +│ └── src +│ ├── calibration.rs +│ ├── led.rs +│ └── main.rs +└── independent_logic + ├── Cargo.toml + └── src + ├── heading_drawing.rs + ├── lib.rs + ├── line_drawing.rs + └── tilt_compensation.rs +{{}} + +=== Adding tests + +Now that we have separated the crates from each other, we are free to add unit tests to `independent_logic` the same way we would for any 'normal' rust project. +However, we must keep in mind that we will only be able to run `cargo test` from inside the `independent_logic` crate. +Likewise, we will not be able to run `cargo build/check/embed` from the top level workspace, but must run it from the `hardware_main` crate, as that crate is bound to a specific target. +Cargo commands will generally also not work in the root workspace, as it will try to compile `hardware_main` for our local architecture, inevitably failing. diff --git a/content/resume.adoc b/content/resume.adoc index 4354861..62b31db 100644 --- a/content/resume.adoc +++ b/content/resume.adoc @@ -13,6 +13,13 @@ showToc = true == Work Experience +=== John Deere +Embedded Linux Engineer:: +August 2023--current +* Develop Linux distributions for embedded systems with Yocto +* Develop Linux hardware drivers for embedded system +* Use Git to collaborate across teams + === Appareo Embedded Systems Intern:: Summer 2023