finished ch12.4
All checks were successful
Test Gitea Actions / first (push) Successful in 22s

This commit is contained in:
2025-02-18 15:28:22 -07:00
parent e0bc88cf97
commit fae65591e6
5 changed files with 392 additions and 10 deletions

View File

@ -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

View File

@ -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));
}
}

View File

@ -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);
}