Testing the Library's Functionality
Writing tests for the core functionality of our code is now easier since we extracted the logic into src/lib.rs and left all the argument parsing and error handling in src/main.rs. We can now call our code directly with various arguments and check return values without having to call our binary from the command line.
We're going to write a function named grep
that takes our search term and the
text to search and produces a list of search results. Let's remove that
println!
from run
(and from src/main.rs as well, as we don't really need
those anymore either), and call the new grep
function with the options we've
collected. We'll add a placeholder implementation of the function for now, and
a test that specifies the behavior we'd like the grep
function to have. The
test will fail with our placeholder implementation, of course, but we can make
sure the code compiles and that we get the failure message we expect. Listing
12-14 shows these modifications:
Notice that we need an explicit lifetime 'a
declared in the signature of
grep
and used with the contents
parameter and the return value. Remember,
lifetime parameters are used to specify which function parameters' lifetimes
connect to the lifetime of the return value. In this case, we're indicating that
the vector we're returning is going to contain string slices that reference
slices of the parameter contents
, as opposed to referencing slices of the
parameter search
. Another way to think about what we're telling Rust is that
the data returned by the grep
function will live as long as the data passed
into this function in the contents
parameter. This is important! Given that
the data a slice references needs to be valid in order for the reference to be
valid, if the compiler thought that we were making string slices of search
rather than contents
, it would do its safety checking incorrectly. If we tried
to compile this function without lifetimes, we would get this error:
error[E0106]: missing lifetime specifier
--> src\lib.rs:37:46
|
37 | fn grep(search: &str, contents: &str) -> Vec<&str> {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `search` or
`contents`
Rust can't possibly know which of the two parameters we need, so it needs us to
tell it. Because contents
is the parameter that contains all of our text and
we want to return the parts of that text that match, we know contents
is the
parameter that should be connected to the return value using the lifetime
syntax.
Connecting parameters to return values in the signature is something that other programming languages don't make you do, so don't worry if this still feels strange! Knowing how to specify lifetimes gets easier over time, and practice makes perfect. You may want to re-read the above section or go back and compare this example with the Lifetime Syntax section in Chapter 10.
Now let's try running our test:
$ cargo test
...warnings...
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/greprs-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
Great, our test fails, exactly as we expected. Let's get the test to pass! It's
failing because we always return an empty vector. Here's what we're going to do
to implement grep
:
- Iterate through each line of the contents.
- Check if the line contains our search string.
- If it does, add it to the list of values we're returning.
- If not, do nothing.
- Return the list of results that match.
Let's take each step at a time, starting with iterating through lines. Strings
have a helpful method to handle this, conveniently named lines
:
Filename: src/lib.rs
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
We're using a for
loop along with the lines
method to get each line in turn.
Next, let's see if our line contains the search string. Luckily, strings have a
helpful method named contains
that does this for us! Using the contains
method looks like this:
Filename: src/lib.rs
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(search) {
// do something with line
}
}
}
Finally, we need a way to store the lines that contain our search string. For
that, we can make a mutable vector before the for
loop and call the push
method to store a line
in the vector. After the for
loop, we return the
vector. Listing 12-15 has the full implementation:
Let's give it a try:
$ cargo test
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-2f55ee8cd1721808
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Great! It works. Now that our test is passing, we could consider opportunities
for refactoring the implementation of grep
and be certain we maintain the
same functionality while we do so. This code isn't bad, but it isn't taking
advantage of some useful features of iterators. We'll be coming back to this
example in Chapter 13 where we'll explore iterators in detail and see how to
improve it.
Now that the grep
function is working, we need to do one last thing inside of
the run
function: we never printed out the results! We'll do that by adding
a for
loop that prints each line returned from the grep
function:
Filename: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
for line in grep(&config.search, &contents) {
println!("{}", line);
}
Ok(())
}
Now our whole program should be working! Let's try it out:
$ cargo run the poem.txt
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target\debug\greprs.exe the poem.txt`
Then there's a pair of us - don't tell!
To tell your name the livelong day
$ cargo run a poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe a poem.txt`
I'm nobody! Who are you?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Excellent! We've built our own version of a classic tool, and learned a lot about how to structure applications. We've also learned a bit about file input and output, lifetimes, testing, and command line parsing.