mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-07-06 11:07:12 -06:00
This commit is contained in:
@ -727,4 +727,301 @@ We add `use minigrep::Config` line to bring the `Coinfig` type from the library
|
||||
|
||||
And we prefix the `run` function with our crate name so that it can also be used
|
||||
|
||||
This work sets up for success in the future.
|
||||
This work sets up for success in the future.
|
||||
|
||||
### Sixth Goal: Developing the Library's Functionality with Test-Driven Development
|
||||
Now that the code and logic has been extracted out of *main.rs* and left behind the argument collecting and error handling
|
||||
|
||||
It is now much easier and possible to write tests for the core functionality of the code.
|
||||
|
||||
We can now call functions directly with various arguments and check the return values without having to call our binary from the command line.
|
||||
|
||||
This goal's section will focus on adding the search logic to the `minigrep` program using the test-driven development (TDD) process with the steps:
|
||||
1. Write a test that fails and run it to make sure it fails for the reason you expect
|
||||
2. Write or modify just enough code to make the new test pass
|
||||
3. Refactor the code you just added or changed and make sure the tests continue to pass
|
||||
4. Repeat form step 1
|
||||
|
||||
Even though this is one of many was to write software, TDD can help drive code design
|
||||
|
||||
Writing the tests before you write code that makes the test pass helps to maintain high test coverage throughout the process.
|
||||
|
||||
We will test drive the implementation of the functionality that will actually do the searching for the query string in the file contents and produce a list of lines that match the query
|
||||
|
||||
We will add this in the function called `search`
|
||||
|
||||
#### Writing a Failing Test
|
||||
First lets remove the `println!` statements because we don't need them anymore to check the program's behavior.
|
||||
|
||||
Next we'll add a `tests` module with a test function the same as [The Test Anatomy](../Writing_Tests.md) from before.
|
||||
|
||||
This test will specify the behavior we want the `search` function to have
|
||||
- It will take a query and the test to search
|
||||
- it will return only the lines form the text that contain the query
|
||||
Here is the test (it goes in *src/lib.rs*)
|
||||
Note it will not compile yet
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_result() {
|
||||
let query = "duct";
|
||||
let contents = "\
|
||||
Rust:
|
||||
safe, fast, productive.
|
||||
Pick three.";
|
||||
|
||||
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
|
||||
}
|
||||
}
|
||||
```
|
||||
This test will search for the string `"duct"`
|
||||
|
||||
The test we will search three lines only one that contains `"duct"`
|
||||
|
||||
Note that the backslash after the opening double quote tells Rust not to put a newline character at the beginning of the contents of this string literal.
|
||||
|
||||
We will then assert that the value returned from the `search` function only contains the line we expect
|
||||
|
||||
We aren't yet able o run this test and watch it fail because the function it needs in order to compile and run doesn't exist yet.
|
||||
|
||||
In accordance with TDD principles we will add just enough code to compile and run by adding a definition of the `search` function that always returns an empty vector that doesn't match with the one in the assert.
|
||||
|
||||
Here the what the function will look like at this point
|
||||
```rust
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
vec![]
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we need to define an explicit lifetime `'a` in the signature of `search` and use that lifetime with the `contents` argument and the return value.
|
||||
|
||||
This case specifies that the vector returned should contain string slices that reference slices of the argument `contents` (rather than the argument `query`).
|
||||
|
||||
It also could be said that the returned value will live as long as what was passed into the `contents` arguments.
|
||||
|
||||
This is important the data referenced *by* a slice needs to be valid for the reference to be valid.
|
||||
|
||||
If the compiler assumes we are making string slices of `query` rather than `contents` it will do its safety checking incorrectly.
|
||||
|
||||
|
||||
If we forget lifetime annotations and try to compile we will get this error
|
||||
```
|
||||
$ cargo build
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
error[E0106]: missing lifetime specifier
|
||||
--> src/lib.rs:28:51
|
||||
|
|
||||
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
|
||||
| ---- ---- ^ expected named lifetime parameter
|
||||
|
|
||||
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
|
||||
help: consider introducing a named lifetime parameter
|
||||
|
|
||||
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
|
||||
| ++++ ++ ++ ++
|
||||
|
||||
For more information about this error, try `rustc --explain E0106`.
|
||||
error: could not compile `minigrep` (lib) due to 1 previous error
|
||||
```
|
||||
|
||||
Rust can't possibly know which of the two args we need so we need to tell it explicitly.
|
||||
|
||||
Due to `contents` is the arguments that contains all of our text we want to return the parts of that text that match.
|
||||
|
||||
This shows that `contents` is the argument that should be connected to the return value using the lifetime syntax.
|
||||
|
||||
Other programming languages don't require you to connect the arguments to return value, but this practice will get easier over time with more exposure.
|
||||
|
||||
Here is the output of the test
|
||||
```
|
||||
$ cargo test
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
|
||||
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
|
||||
|
||||
running 1 test
|
||||
test tests::one_result ... FAILED
|
||||
|
||||
failures:
|
||||
|
||||
---- tests::one_result stdout ----
|
||||
thread 'tests::one_result' panicked at src/lib.rs:44:9:
|
||||
assertion `left == right` failed
|
||||
left: ["safe, fast, productive."]
|
||||
right: []
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
|
||||
|
||||
failures:
|
||||
tests::one_result
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
error: test failed, to rerun pass `--lib`
|
||||
```
|
||||
This test fails exactly as expected
|
||||
|
||||
#### Writing Code to Pass the Test
|
||||
Our test is failing because we always return an empty vector
|
||||
|
||||
To fix this and implement `search`, the program needs to follow these steps:
|
||||
1. Iterate through each line of the contents
|
||||
2. Check whether the line contains the query string
|
||||
3. If it does add it to the list of values we are returning
|
||||
4. If it doesn't do nothing
|
||||
5. Return the list of result that match
|
||||
|
||||
##### Iterating Through Lines with the lines Method
|
||||
Rust includes a helpful method to handle line-by-line iterations of strings, named `lines`
|
||||
|
||||
Here it is how it would be used in this case
|
||||
```rust
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
for line in contents.lines() {
|
||||
// do something with line
|
||||
}
|
||||
}
|
||||
```
|
||||
The `lines` method returns an iterator.
|
||||
For now recall that when used in a `for` loop with an iterator to run some code on each item in a collection
|
||||
|
||||
##### Searching each Line for the Query
|
||||
Next we will check whether the current line contains our query string.
|
||||
|
||||
Strings have a helpful method named `contains` that does this for us.
|
||||
|
||||
now lets add a call to the `contains` method in the `search` function
|
||||
|
||||
Here is the updated function
|
||||
|
||||
Note it still will not compile
|
||||
```rust
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
for line in contents.lines() {
|
||||
if line.contains(query) {
|
||||
// do something with line
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
At the moment we are only building up functionality
|
||||
|
||||
To get the code to compile we need to return a value from the body as we indicated in the function signature
|
||||
|
||||
##### Storing Matching Lines
|
||||
To finish this function we need a way to store the matching lines that we want to return.
|
||||
|
||||
To do this for now 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 the vector will be returned
|
||||
|
||||
Here is what the function like after adding the `vector` and the `push` method
|
||||
|
||||
Note it will now compile
|
||||
```rust
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for line in contents.lines() {
|
||||
if line.contains(query) {
|
||||
results.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
```
|
||||
Now the `search` function should return only the lines that contain `query` and the test should pass
|
||||
|
||||
Here is the output when running the test at this point
|
||||
```
|
||||
$ cargo test
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
|
||||
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
|
||||
|
||||
running 1 test
|
||||
test tests::one_result ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Doc-tests minigrep
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
```
|
||||
|
||||
As we can now see the test passes.
|
||||
|
||||
At this point we could consider opportunities for refactoring the implementation of the search function while keeping the tests passing to maintain the same functionality
|
||||
|
||||
The code in the search function isn't too bad but it doesn't take advantage of some useful features that iterators have
|
||||
|
||||
This will be further improved in the iterators chapter
|
||||
|
||||
##### Using the Search Function in the `run` Function
|
||||
Now that the `search` function is working and tested, we now need to call `search` from our `run` function.
|
||||
|
||||
We need to pass the `config.query` value and the `contents` that `run` reads from the file to search function.
|
||||
|
||||
Then `run` will print each line returned from `search`
|
||||
|
||||
Here is what run will look like now
|
||||
```rust
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.file_path)?;
|
||||
|
||||
for line in search(&config.query, &contents) {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
We are still using a `for` loop to return each line form `search` and print it
|
||||
|
||||
Now that the entire program should work
|
||||
|
||||
Lets try it with first with a word that should return exactly one line from the Emily Dickinson poem: *frog*
|
||||
|
||||
Here is the output
|
||||
```
|
||||
$ cargo run -- frog poem.txt
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
|
||||
Running `target/debug/minigrep frog poem.txt`
|
||||
How public, like a frog
|
||||
```
|
||||
|
||||
Now lets try with a word that will match multiple lines like *body*
|
||||
```
|
||||
$ cargo run -- body poem.txt
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
|
||||
Running `target/debug/minigrep body poem.txt`
|
||||
I'm nobody! Who are you?
|
||||
Are you nobody, too?
|
||||
How dreary to be somebody!
|
||||
```
|
||||
|
||||
Then lets make sure we don' get any lines when we search for a word that isn't anywhere such as *monomorphization*
|
||||
```
|
||||
$ cargo run -- monomorphization poem.txt
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
|
||||
Running `target/debug/minigrep monomorphization poem.txt`
|
||||
```
|
||||
|
||||
Now that it is finished we will finished off with a demonstration on how to work with environment variables and how to print a std error, both are useful when you are writing command line programs
|
||||
|
||||
|
@ -23,7 +23,39 @@ impl Config {
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.file_path)?;
|
||||
|
||||
println!("With text:\n{contents}")
|
||||
// refactor 10
|
||||
// println!("With text:\n{contents}")
|
||||
|
||||
for line in search(&config.query, &contents) {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
// original that only can fail
|
||||
// vec![]
|
||||
|
||||
for line in contents.lines() {
|
||||
if line.contains(query) {
|
||||
// do something with line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_result() {
|
||||
let query = "duct";
|
||||
let contents = "\
|
||||
Rust:
|
||||
safe, fast, productive.
|
||||
Pick three.";
|
||||
|
||||
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
|
||||
}
|
||||
}
|
||||
|
@ -30,12 +30,13 @@ fn main() {
|
||||
// process::exit(1);
|
||||
// });
|
||||
|
||||
|
||||
println!("Searching for {}", config.query);
|
||||
println!("In the file {}", config.file_path);
|
||||
// refactor 10
|
||||
// println!("Searching for {}", config.query);
|
||||
// println!("In the file {}", config.file_path);
|
||||
|
||||
// refactor 8
|
||||
if let Err(e) = minigrep::run(config) {
|
||||
// needed for helping the user
|
||||
println!("Application error: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
|
Reference in New Issue
Block a user