diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml new file mode 100644 index 0000000..1bbae74 --- /dev/null +++ b/.gitea/workflows/testing.yaml @@ -0,0 +1,37 @@ +# name of the workflow. +# this is optional. +name: Test Gitea Actions + +# events that will trigger this workflow. +# here, we only have "pull_request", so the workflow will run +# whenever we create a pull request. +# other examples: [push] and [pull_request, push] +on: [push] + + +# each workflow must have at least one job. +# jobs run in parallel by default (we can change that). +# each job groups together a series of steps to accomplish a purpose. +jobs: + # name of the job + first: + # the platform or OS that the workflow will run on. + runs-on: ubuntu-latest + + # series of steps to finish the job. + steps: + # name of the step. + # steps run sequentially. + # this is optionale + - name: checkout + # each step can either have "uses" or "run". + # "uses" run an action written somewhere other than this workflow . + # usually from the community. + # this action checks out the repo code to the runner (instance) + # running the action + uses: actions/checkout@v3 + + # another step. + # this step runs a bash (Ubuntu's default shell) command + - name: list files + run: ls \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 7abc687..636b8b0 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -24,6 +24,20 @@ { "id": "53b36d00b704136e", "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Tests.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Tests" + } + }, + { + "id": "ec34d2df2728a299", + "type": "leaf", "state": { "type": "markdown", "state": { @@ -36,7 +50,7 @@ } } ], - "currentTab": 1 + "currentTab": 2 } ], "direction": "vertical" @@ -179,13 +193,14 @@ "command-palette:Open command palette": false } }, - "active": "53b36d00b704136e", + "active": "ec34d2df2728a299", "lastOpenFiles": [ + "Writing_Tests.md", + "Tests.md", + "minigrep/README.md", "minigrep/src/lib.rs", "Test_Organization.md", "Test Controls.md", - "Tests.md", - "Writing_Tests.md", "Traits.md", "Modules and Use.md", "Modules.md", diff --git a/minigrep/README.md b/minigrep/README.md index 9e6f48c..3a86dfb 100644 --- a/minigrep/README.md +++ b/minigrep/README.md @@ -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. \ No newline at end of file +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> { + 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 + diff --git a/minigrep/src/lib.rs b/minigrep/src/lib.rs index 488b891..7f95b91 100644 --- a/minigrep/src/lib.rs +++ b/minigrep/src/lib.rs @@ -23,7 +23,39 @@ impl Config { pub fn run(config: Config) -> Result<(), Box> { 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(()) -} \ No newline at end of file +} + +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)); + } +} diff --git a/minigrep/src/main.rs b/minigrep/src/main.rs index ed6a23e..0049ff4 100644 --- a/minigrep/src/main.rs +++ b/minigrep/src/main.rs @@ -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); }