1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-05-20 17:33:31 +02:00

Be more consistent about tests vs. main (#2644)

The content slides all use `fn main`, with the exception of the testing
segment. But with this change, where it makes sense exercises use tests
instead, and not both tests and `fn main`.

A small change in `book.js` supports running tests when a code sample
does not have `fn main` but does have `#[test]`, so these work
naturally.

Fixes #1581.
This commit is contained in:
Dustin J. Mitchell 2025-02-18 15:13:16 -05:00 committed by GitHub
parent 699c5137c7
commit 44a79741ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 138 additions and 144 deletions

View File

@ -8,6 +8,27 @@ the [instructions in the README].
[instructions in the README]: README.md#building
## Writing Exercises
Each segment ends with an exercise. Exercises are typically structured as an
`exercise.rs` containing the problem and solution. This is referenced from
`exercise.md` and `solution.md`, using `{{#include exercise.rs:anchor_name}}` to
match ANCHOR comments in the `exercise.rs` file. Each segment also has a
`Cargo.toml` file containing a `[[bin]]` or `[lib]` section referring to
`exercise.rs`, and that Cargo package is referenced from the workspace the root
`Cargo.toml`. The result is that `exercise.rs` is built and tested by
`cargo test`.
For segments on day 1, exercises should use `fn main() { .. }` and `println!`,
with students visually verifying the correct output. On subsequent days, prefer
tests and omit `fn main() { .. }`. However, where tests would be difficult and
visual verification is more natural (such as in the Logger exercise), using
`fn main { .. }` is OK.
Especially for exercises without tests, consider including tests in
`exercise.rs` that do not appear in either `exercise.md` or `solution.md`, as
these can ensure the solution is correct.
## Testing
We test the course material in several ways:

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
[lib]
name = "borrowing"
path = "../../third_party/rust-on-exercism/health-statistics.rs"

View File

@ -9,10 +9,7 @@ minutes: 20
Copy the code below to <https://play.rust-lang.org/> and fill in the missing
method:
```rust
// TODO: remove this when you're done with your implementation.
#![allow(unused_variables, dead_code)]
```rust,editable
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:setup}}
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:User_visit_doctor}}
@ -20,7 +17,5 @@ method:
}
}
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:main}}
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:tests}}
```

View File

@ -4,7 +4,7 @@ Building on the generic logger from this morning, implement a `Filter` which
uses a closure to filter log messages, sending those which pass the filtering
predicate to an inner logger.
```rust,compile_fail
```rust,compile_fail,editable
{{#include exercise.rs:setup}}
// TODO: Define and implement `Filter`.

View File

@ -30,7 +30,5 @@ initial `n`.
todo!("Implement this")
}
{{#include exercise.rs:tests}}
{{#include exercise.rs:main}}
```

View File

@ -25,15 +25,14 @@ fn collatz_length(mut n: i32) -> u32 {
len
}
// ANCHOR: tests
// ANCHOR: main
fn main() {
println!("Length: {}", collatz_length(11)); // should be 15
}
// ANCHOR_END: main
// ANCHOR_END: solution
#[test]
fn test_collatz_length() {
assert_eq!(collatz_length(11), 15);
}
// ANCHOR_END: tests
// ANCHOR: main
fn main() {
println!("Length: {}", collatz_length(11));
}
// ANCHOR_END: main

View File

@ -8,6 +8,6 @@ publish = false
anyhow = "*"
thiserror = "*"
[[bin]]
[lib]
name = "parser"
path = "exercise.rs"

View File

@ -88,25 +88,30 @@ fn eval(e: Expression) -> Result<i64, DivideByZeroError> {
// ANCHOR_END: solution
// ANCHOR: tests
#[test]
fn test_error() {
assert_eq!(
eval(Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
}),
Err(DivideByZeroError)
);
}
#[cfg(test)]
mod test {
use super::*;
fn main() {
let expr = Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(20)),
right: Box::new(Expression::Value(10)),
};
println!("expr: {expr:?}");
println!("result: {:?}", eval(expr));
#[test]
fn test_error() {
assert_eq!(
eval(Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
}),
Err(DivideByZeroError)
);
}
#[test]
fn test_ok() {
let expr = Expression::Op {
op: Operation::Sub,
left: Box::new(Expression::Value(20)),
right: Box::new(Expression::Value(10)),
};
assert_eq!(eval(expr), Ok(10));
}
}
// ANCHOR_END: tests

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
[lib]
name = "generics"
path = "exercise.rs"

View File

@ -7,12 +7,12 @@ minutes: 10
In this short exercise, you will implement a generic `min` function that
determines the minimum of two values, using the [`Ord`] trait.
```rust,compile_fail
```rust,editable
use std::cmp::Ordering;
// TODO: implement the `min` function used in `main`.
// TODO: implement the `min` function used in the tests.
{{#include exercise.rs:main}}
{{#include exercise.rs:tests}}
```
<details>

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
// ANCHOR: solution
use std::cmp::Ordering;
@ -22,15 +23,22 @@ fn min<T: Ord>(l: T, r: T) -> T {
}
}
// ANCHOR: main
fn main() {
// ANCHOR: tests
#[test]
fn integers() {
assert_eq!(min(0, 10), 0);
assert_eq!(min(500, 123), 123);
}
#[test]
fn chars() {
assert_eq!(min('a', 'z'), 'a');
assert_eq!(min('7', '1'), '1');
}
#[test]
fn strings() {
assert_eq!(min("hello", "goodbye"), "goodbye");
assert_eq!(min("bat", "armadillo"), "armadillo");
}
// ANCHOR_END: main
// ANCHOR_END: tests

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "offset-differences"
[lib]
name = "offset_differences"
path = "exercise.rs"

View File

@ -11,7 +11,7 @@ Copy the following code to <https://play.rust-lang.org/> and make the tests
pass. Use an iterator expression and `collect` the result to construct the
return value.
```rust
```rust,editable
{{#include exercise.rs:offset_differences}}
todo!()
}

View File

@ -51,5 +51,3 @@ fn test_degenerate_cases() {
assert_eq!(offset_differences(1, empty), vec![]);
}
// ANCHOR_END: unit-tests
fn main() {}

View File

@ -7,6 +7,6 @@ publish = false
[dependencies]
thiserror = "*"
[[bin]]
[lib]
name = "protobuf"
path = "exercise.rs"

View File

@ -83,7 +83,7 @@ What remains for you is to implement the `parse_field` function and the
// TODO: Implement ProtoMessage for Person and PhoneNumber.
{{#include exercise.rs:main }}
{{#include exercise.rs:tests }}
```
<details>

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
// ANCHOR: solution
// ANCHOR: preliminaries
@ -193,21 +194,31 @@ impl<'a> ProtoMessage<'a> for PhoneNumber<'a> {
}
}
// ANCHOR: main
fn main() {
// ANCHOR: tests
#[test]
fn test_id() {
let person_id: Person = parse_message(&[0x10, 0x2a]);
assert_eq!(person_id, Person { name: "", id: 42, phone: vec![] });
}
#[test]
fn test_name() {
let person_name: Person = parse_message(&[
0x0a, 0x0e, 0x62, 0x65, 0x61, 0x75, 0x74, 0x69, 0x66, 0x75, 0x6c, 0x20,
0x6e, 0x61, 0x6d, 0x65,
]);
assert_eq!(person_name, Person { name: "beautiful name", id: 0, phone: vec![] });
}
#[test]
fn test_just_person() {
let person_name_id: Person =
parse_message(&[0x0a, 0x04, 0x45, 0x76, 0x61, 0x6e, 0x10, 0x16]);
assert_eq!(person_name_id, Person { name: "Evan", id: 22, phone: vec![] });
}
#[test]
fn test_phone() {
let phone: Person = parse_message(&[
0x0a, 0x00, 0x10, 0x00, 0x1a, 0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x33,
0x34, 0x2d, 0x37, 0x37, 0x37, 0x2d, 0x39, 0x30, 0x39, 0x30, 0x12, 0x04,
@ -221,8 +232,11 @@ fn main() {
phone: vec![PhoneNumber { number: "+1234-777-9090", type_: "home" },],
}
);
}
// Put that all together into a single parse.
// Put that all together into a single parse.
#[test]
fn test_full_person() {
let person: Person = parse_message(&[
0x0a, 0x07, 0x6d, 0x61, 0x78, 0x77, 0x65, 0x6c, 0x6c, 0x10, 0x2a, 0x1a,
0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x30, 0x32, 0x2d, 0x35, 0x35, 0x35,
@ -243,4 +257,4 @@ fn main() {
}
);
}
// ANCHOR_END: main
// ANCHOR_END: tests

View File

@ -18,7 +18,7 @@ implementing that same trait, adding behavior in the process. In the "Generics"
segment this afternoon, we will see how to make the wrapper generic over the
wrapped type.
```rust,compile_fail
```rust,compile_fail,editable
{{#include exercise.rs:setup}}
// TODO: Implement the `Logger` trait for `VerbosityFilter`.

View File

@ -29,7 +29,7 @@ files in the `src` directory.
Here's the single-module implementation of the GUI library:
```rust
```rust,editable
{{#include exercise.rs:single-module}}
```

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
[lib]
name = "eval"
path = "exercise.rs"

View File

@ -46,7 +46,7 @@ evaluate to `85`. We represent this as a much bigger tree:
In code, we will represent the tree with two types:
```rust,editable
```rust
{{#include exercise.rs:Operation}}
{{#include exercise.rs:Expression}}

View File

@ -127,13 +127,3 @@ fn test_zeros() {
);
}
// ANCHOR_END: tests
fn main() {
let expr = Expression::Op {
op: Operation::Div,
left: Box::new(Expression::Value(10)),
right: Box::new(Expression::Value(2)),
};
println!("expr: {expr:?}");
println!("result: {:?}", eval(expr));
}

View File

@ -7,7 +7,7 @@ minutes: 20
We will create a few utility functions for 3-dimensional geometry, representing
a point as `[f64;3]`. It is up to you to determine the function signatures.
```rust,compile_fail
```rust,compile_fail,editable
// Calculate the magnitude of a vector by summing the squares of its coordinates
// and taking the square root. Use the `sqrt()` method to calculate the square
// root, like `v.sqrt()`.

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "binary-tree"
[lib]
name = "binary_tree"
path = "exercise.rs"

View File

@ -14,7 +14,7 @@ Implement the following types, so that the given tests pass.
Extra Credit: implement an iterator over a binary tree that returns the values
in order.
```rust,editable,ignore
```rust,compile_fail,editable
{{#include exercise.rs:types}}
// Implement `new`, `insert`, `len`, and `has` for `Subtree`.

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
// ANCHOR: solution
use std::cmp::Ordering;
@ -96,14 +97,6 @@ impl<T: Ord> Node<T> {
}
}
fn main() {
let mut tree = BinaryTree::new();
tree.insert("foo");
assert_eq!(tree.len(), 1);
tree.insert("bar");
assert!(tree.has(&"foo"));
}
// ANCHOR: tests
#[cfg(test)]
mod tests {

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "std-traits"
[lib]
name = "std_traits"
path = "exercise.rs"

View File

@ -9,12 +9,12 @@ In this example, you will implement the classic
playground, and implement the missing bits. Only rotate ASCII alphabetic
characters, to ensure the result is still valid UTF-8.
```rust,compile_fail
```rust,editable
{{#include exercise.rs:head }}
// Implement the `Read` trait for `RotDecoder`.
{{#include exercise.rs:main }}
{{#include exercise.rs:tests }}
```
What happens if you chain two `RotDecoder` instances together, each rotating by

View File

@ -36,15 +36,7 @@ impl<R: Read> Read for RotDecoder<R> {
}
}
// ANCHOR: main
fn main() {
let mut rot =
RotDecoder { input: "Gb trg gb gur bgure fvqr!".as_bytes(), rot: 13 };
let mut result = String::new();
rot.read_to_string(&mut result).unwrap();
println!("{}", result);
}
// ANCHOR: tests
#[cfg(test)]
mod test {
use super::*;
@ -72,4 +64,4 @@ mod test {
}
}
}
// ANCHOR_END: main
// ANCHOR_END: tests

View File

@ -7,6 +7,6 @@ publish = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(never)'] }
[[bin]]
[lib]
name = "luhn"
path = "exercise.rs"

View File

@ -27,7 +27,7 @@ correctly.
Copy the code below to <https://play.rust-lang.org/> and write additional tests
to uncover bugs in the provided implementation, fixing any bugs you find.
```rust
```rust,editable
{{#include exercise.rs:luhn}}
{{#include exercise.rs:unit-tests}}

View File

@ -11,8 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
// ANCHOR: solution
// This is the buggy version that appears in the problem.
#[cfg(never)]
// ANCHOR: luhn
@ -40,6 +40,7 @@ pub fn luhn(cc_number: &str) -> bool {
// ANCHOR_END: luhn
// This is the solution and passes all of the tests below.
// ANCHOR: solution
pub fn luhn(cc_number: &str) -> bool {
let mut sum = 0;
let mut double = false;
@ -69,14 +70,6 @@ pub fn luhn(cc_number: &str) -> bool {
digits >= 2 && sum % 10 == 0
}
fn main() {
let cc_number = "1234 5678 1234 5670";
println!(
"Is {cc_number} a valid credit card number? {}",
if luhn(cc_number) { "yes" } else { "no" }
);
}
// ANCHOR: unit-tests
#[cfg(test)]
mod test {

View File

@ -8,7 +8,7 @@ Rust and Cargo come with a simple unit test framework. Tests are marked with
`#[test]`. Unit tests are often put in a nested `tests` module, using
`#[cfg(test)]` to conditionally compile them only when building tests.
```rust,editable,ignore
```rust,editable
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(idx) => &text[..idx],
@ -39,9 +39,3 @@ mod tests {
- This lets you unit test private helpers.
- The `#[cfg(test)]` attribute is only active when you run `cargo test`.
<details>
Run the tests in the playground in order to show their results.
</details>

View File

@ -26,15 +26,10 @@ transpose a matrix (turn rows into columns):
Copy the code below to <https://play.rust-lang.org/> and implement the function.
This function only operates on 3x3 matrices.
```rust,should_panic
// TODO: remove this when you're done with your implementation.
#![allow(unused_variables, dead_code)]
```rust,should_panic,editable
{{#include exercise.rs:transpose}}
todo!()
}
{{#include exercise.rs:tests}}
{{#include exercise.rs:main}}
```

View File

@ -25,7 +25,23 @@ fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
result
}
// ANCHOR: tests
// ANCHOR: main
fn main() {
let matrix = [
[101, 102, 103], // <-- the comment makes rustfmt add a newline
[201, 202, 203],
[301, 302, 303],
];
println!("matrix: {:#?}", matrix);
let transposed = transpose(matrix);
println!("transposed: {:#?}", transposed);
}
// ANCHOR_END: main
// ANCHOR_END: solution
// This test does not appear in the exercise, as this is very early in the course, but it verifies
// that the solution is correct.
#[test]
fn test_transpose() {
let matrix = [
@ -43,18 +59,3 @@ fn test_transpose() {
]
);
}
// ANCHOR_END: tests
// ANCHOR: main
fn main() {
let matrix = [
[101, 102, 103], // <-- the comment makes rustfmt add a newline
[201, 202, 203],
[301, 302, 303],
];
println!("matrix: {:#?}", matrix);
let transposed = transpose(matrix);
println!("transposed: {:#?}", transposed);
}
// ANCHOR_END: main

View File

@ -51,7 +51,7 @@ The [Nomicon] also has a very useful chapter about FFI.
Copy the code below to <https://play.rust-lang.org/> and fill in the missing
functions and methods:
```rust,should_panic
```rust,should_panic,editable
// TODO: remove this when you're done with your implementation.
#![allow(unused_imports, unused_variables, dead_code)]

View File

@ -148,6 +148,11 @@ function playground_text(playground, hidden = true) {
crateType: "bin",
};
// If the code block has no `main` but does have tests, run those.
if (text.indexOf("fn main") === -1 && text.indexOf("#[test]") !== -1) {
params.tests = true;
}
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
}

View File

@ -50,13 +50,6 @@ impl User {
}
}
// ANCHOR: main
fn main() {
let bob = User::new(String::from("Bob"), 32, 155.2);
println!("I'm {} and my age is {}", bob.name, bob.age);
}
// ANCHOR_END: main
// ANCHOR: tests
#[test]
fn test_visit() {