Rust: Move from binary to library and Add Documentation Examples that are tested.
Adam.S
Posted on May 11, 2021
In the previous article in this series. I went through some basics of documenting your rust code. I had hoped to be able to take advantage of another nice feature of rust. That being the fact that code examples are actually tested by rust. I will go through the changes I had to make for this to work.
TL;DR:
- Document testing only works if you are writing a crate. This means it works against a
lib.rs
file and its related modules. - The documentation is tested when you either run
cargo test
orcargo test --doc
if you only want to test the documentation examples.
Example:
Doc-tests decon-spf
running 3 tests
test src/spf/mod.rs - spf::Spf::new (line 32) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_string (line 166) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_mechanism (line 145) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.85s
How we get the results above
Initially when I started trying to test my documentation I found that no matter what I did. The tests did not appear to be getting run.
It turns out binary projects are really not expected to have documentation with testable examples, makes sense really.
So the answer is to have your modules as libraries. In my case I went for the fastest approach, which is fairly straight forward. I converted my project into a mixed library and binary
I will give the main details. But if you really want to look at the gory details, take a look at this diff
There were a large number of changes. I took this opportunity to rename my project and to move the modules around as I suspect mx and soa will not be used.
The Steps
Update Cargo.toml
This step is not really needed. But I wanted to rename the project more in line with what it was doing. So I changed the name
from trust-dns
to decon-spf
which is short for "deconstruct spf".
Create a lib.rs file
Next I created a lib.rs file under src
.
This file eventually became simply
pub mod spf;
Update main.rs
Here I removed the mod dns
and use crate::dns::...
and replaced them with a single line of use decon_spf::spf::Spf;
When you are creating a library in this way, you no longer make use of the use crate::
within the source of the binary application. You actually call the crate by name. You still make use of the use crate::
within the module code itself though. So be aware of that.
Move spf up a directory level in line with dns
I moved the spf
directory up one level along side dns
given the new directory structure you see here.
src
├── dns
│ ├── mod.rs
│ ├── mx.rs
│ └── soa.rs
├── lib.rs
├── main.rs
└── spf
├── kinds.rs
├── mechanism.rs
├── mod.rs
└── tests
├── mod.rs
├── spf_a_mechanism_test.rs
├── spf_mx_mechanism_test.rs
└── spf_test.rs
Update dns/mod.rs
I removed the reference for for pub mod spf
from within dns/mod.rs
Update all .rs files below the spf directory
Within all these files we replace any reference to use crate::dns:spf::...
with use crate::spf::...
.
As spf no longer lives below dns.
Adding some documentation examples which are tested.
Spf::new()
I added my first example in spf/mod.rs
Note: I am using single quote marks in the following examples. Dev.to does not seem to allow me to escape backpacks. Resulting in incorrectly formatted contents.
impl Spf {
/// Create a new Spf with the provided `str`
///
/// # Example
///
/// ```
/// use decon_spf::spf::Spf;
/// let source_str = "v=spf1 redirect=_spf.example.com";
/// let spf = Spf::new(&source_str.to_string());
/// ```
///
pub fn new(str: &String) -> Self {
Anything between three backticks is assumed to be rust code, unless a type other than rust is given.
For example:
'''
I am rust.
'''
'''rust
I am also rust.
'''
'''text
I am not rust.
'''
If you look in the result section below you will see that the example code was tested
test src/spf/mod.rs - spf::Spf::new (line 32) ... ok
This tells us that the code in the file mod.rs with the three backticks starting on line 32 was tested and passed.
SpfMechanism::as_mechanism()
Next I updated spf/mechanism.rs
.
/// Returns the mechanism string representation of an IP4/6 mechanism.
/// # Example
///
/// ```
/// use ipnetwork::IpNetwork;
/// use decon_spf::spf::mechanism::SpfMechanism;
/// let ip: IpNetwork = "192.168.11.0/24".parse().unwrap();
/// let ip_mechanism = SpfMechanism::new_ip4('+', ip);
/// assert_eq!(ip_mechanism.as_mechanism(), "ip4:192.168.11.0/24");
/// ```
///
pub fn as_mechanism(&self) -> String {
let mut txt = String::new();
if self.qualifier != '+' {
txt.push(self.qualifier);
};
txt.push_str(self.mechanism_prefix_from_kind().as_str());
txt.push_str(self.mechanism.to_string().as_str());
txt
}
The Result
With these changes I was able to achieve my main goal, of having my documentation examples tested. But I also moved forward in making spf more of an independent crate that could, with an extreme amount of more work, be made available as a crate for others to use.
Testing this now becomes a matter of running cargo test
which results in the following output.
Finished test [unoptimized + debuginfo] target(s) in 0.34s
Running target/debug/deps/decon_spf-66ff691605dc6b68
running 39 tests
test spf::mechanism::SpfMechanismIpNetwork::test_ip4_neutral ... ok
test spf::mechanism::SpfMechanismIpNetwork::test_ip6_fail ... ok
test spf::mechanism::SpfMechanismIpNetwork::test_ip4_fail ... ok
--snip--
test spf::tests::spf_test::test_spf::test_netblocks2_google_com ... ok
test spf::tests::spf_test::test_spf::test_hotmail ... ok
test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
Running target/debug/deps/decon_spf-6020a1334ed1a1df
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests decon-spf
running 3 tests
test src/spf/mod.rs - spf::Spf::new (line 32) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_mechanism (line 145) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_string (line 166) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.60s
Or cargo test --doc
which results in the shorter output as it only tests the documentation.
Finished test [unoptimized + debuginfo] target(s) in 0.25s
Doc-tests decon-spf
running 3 tests
test src/spf/mod.rs - spf::Spf::new (line 32) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_string (line 166) ... ok
test src/spf/mechanism.rs - spf::mechanism::SpfMechanism<IpNetwork>::as_mechanism (line 145) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.54s
That pretty much wraps things up I think for this article.
Posted on May 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.