Skip to content

Commit

Permalink
Be more consistent about tests vs. main (#2644)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
djmitche authored Feb 18, 2025
1 parent 699c513 commit 44a7974
Show file tree
Hide file tree
Showing 38 changed files with 138 additions and 144 deletions.
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/borrowing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 1 addition & 6 deletions src/borrowing/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,13 @@ 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}}
todo!("Update a user's statistics based on measurements from a visit to the doctor")
}
}
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:main}}

{{#include ../../third_party/rust-on-exercism/health-statistics.rs:tests}}
```
2 changes: 1 addition & 1 deletion src/closures/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 0 additions & 2 deletions src/control-flow-basics/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,5 @@ initial `n`.
todo!("Implement this")
}
{{#include exercise.rs:tests}}
{{#include exercise.rs:main}}
```
15 changes: 7 additions & 8 deletions src/control-flow-basics/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ fn collatz_length(mut n: i32) -> u32 {
len
}

// ANCHOR: tests
#[test]
fn test_collatz_length() {
assert_eq!(collatz_length(11), 15);
}
// ANCHOR_END: tests

// ANCHOR: main
fn main() {
println!("Length: {}", collatz_length(11));
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);
}
2 changes: 1 addition & 1 deletion src/error-handling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ publish = false
anyhow = "*"
thiserror = "*"

[[bin]]
[lib]
name = "parser"
path = "exercise.rs"
43 changes: 24 additions & 19 deletions src/error-handling/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

#[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)
);
}

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_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
2 changes: 1 addition & 1 deletion src/generics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
[lib]
name = "generics"
path = "exercise.rs"
6 changes: 3 additions & 3 deletions src/generics/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
14 changes: 11 additions & 3 deletions src/generics/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
4 changes: 2 additions & 2 deletions src/iterators/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "offset-differences"
[lib]
name = "offset_differences"
path = "exercise.rs"
2 changes: 1 addition & 1 deletion src/iterators/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
}
Expand Down
2 changes: 0 additions & 2 deletions src/iterators/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,3 @@ fn test_degenerate_cases() {
assert_eq!(offset_differences(1, empty), vec![]);
}
// ANCHOR_END: unit-tests

fn main() {}
2 changes: 1 addition & 1 deletion src/lifetimes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ publish = false
[dependencies]
thiserror = "*"

[[bin]]
[lib]
name = "protobuf"
path = "exercise.rs"
2 changes: 1 addition & 1 deletion src/lifetimes/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
22 changes: 18 additions & 4 deletions src/lifetimes/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -243,4 +257,4 @@ fn main() {
}
);
}
// ANCHOR_END: main
// ANCHOR_END: tests
2 changes: 1 addition & 1 deletion src/methods-and-traits/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion src/modules/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
```

Expand Down
2 changes: 1 addition & 1 deletion src/pattern-matching/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
[lib]
name = "eval"
path = "exercise.rs"
2 changes: 1 addition & 1 deletion src/pattern-matching/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
10 changes: 0 additions & 10 deletions src/pattern-matching/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
2 changes: 1 addition & 1 deletion src/references/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
4 changes: 2 additions & 2 deletions src/smart-pointers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "binary-tree"
[lib]
name = "binary_tree"
path = "exercise.rs"
2 changes: 1 addition & 1 deletion src/smart-pointers/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading

0 comments on commit 44a7974

Please sign in to comment.