Rust: Move from binary to library and Add Documentation Examples that are tested.

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:

  1. Document testing only works if you are writing a crate. This means it works against a lib.rs file and its related modules.
  2. The documentation is tested when you either run cargo test or cargo 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 main details. But if you really want to look at the gory details, take a look at this diff between my previous article and this article.

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

impl Spf {
    /// Create a new Spf with the provided `str`
    ///
    /// # Example
    ///
    /// ``` <- I am line 32
    /// 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.


See also