Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite CompareMessage to check the whole string #611

Merged
merged 1 commit into from
May 23, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions blog/content/second-edition/posts/04-testing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,31 +923,38 @@ Checking the reported panic message is a bit more complicated. The reason is tha

use core::fmt;

/// Compares a `fmt::Arguments` instance with the `MESSAGE` string.
/// Compares a `fmt::Arguments` instance with the `MESSAGE` string
///
/// To use this type, write the `fmt::Arguments` instance to it using the
/// `write` macro. If a message component matches `MESSAGE`, the equals
/// field is set to true.
/// `write` macro. If the message component matches `MESSAGE`, the `expected`
/// field is the empty string.
struct CompareMessage {
equals: bool,
expected: &'static str,
}

impl fmt::Write for CompareMessage {
fn write_str(&mut self, s: &str) -> fmt::Result {
if s == MESSAGE {
self.equals = true;
if self.expected.starts_with(s) {
self.expected = &self.expected[s.len()..];
} else {
fail("message not equal to expected message");
}
Ok(())
}
}
```

The trick is to implement the [`fmt::Write`] trait, which is called by the [`write`] macro with `&str` arguments. This makes it possible to compare the panic arguments with `MESSAGE`. By the way, this is the same trait that we implemented for our VGA buffer writer in order to print to the screen.
The trick is to implement the [`fmt::Write`] trait like we did for our [VGA buffer writer]. The [`write_str`] method is called with a `&str` parameter that we can compare with the expected message. An important detail is that the method is called _multiple times_ with the individual string components. For example, when we do `print!("{}z", "xy")` the method on our VGA buffer writer is invoked once with the string `"xy"` and once with the string `"z"`.

[`fmt::Write`]: https://doc.rust-lang.org/core/fmt/trait.Write.html
[`write`]: https://doc.rust-lang.org/core/macro.write.html
[VGA buffer writer]: ./second-edition/posts/03-vga-text-buffer/index.md#formatting-macros
[`write_str`]: https://doc.rust-lang.org/core/fmt/trait.Write.html#tymethod.write_str

This means that we can't directly compare the `s` argument with the expected message, since it might only be a substring. Instead, we use the [`starts_with`] method to verify that the given string component is a substring of the expected message. Then we use [string slicing] to remove the already printed characters from the `expected` string. If the `expected` field is an empty string after writing the panic message, it means that it matches the expected message.

[`starts_with`]: https://doc.rust-lang.org/std/primitive.str.html#method.starts_with
[string slicing]: https://doc.rust-lang.org/book/ch04-03-slices.html#string-slices

The above code only works for messages with a single component. This means that it works for `panic!("some message")`, but not for `panic!("some {}", message)`. This isn't ideal, but good enough for our test.

With the `CompareMessage` type, we can finally implement our `check_message` function:

Expand All @@ -960,11 +967,11 @@ use core::fmt::Write;

fn check_message(info: &PanicInfo) {
let message = info.message().unwrap_or_else(|| fail("no message"));
let mut compare_message = CompareMessage { equals: false };
let mut compare_message = CompareMessage { expected: MESSAGE };
write!(&mut compare_message, "{}", message)
.unwrap_or_else(|_| fail("write failed"));
if !compare_message.equals {
fail("message not equal to expected message");
if !compare_message.expected.is_empty() {
fail("message shorter than expected message");
}
}
```
Expand All @@ -973,7 +980,9 @@ The function uses the [`PanicInfo::message`] function to get the panic message.

[`PanicInfo::message`]: https://doc.rust-lang.org/core/panic/struct.PanicInfo.html#method.message

After querying the message, the function constructs a `CompareMessage` instance and writes the message to it using the `write!` macro. Afterwards it reads the `equals` field and fails the test if the panic message does not equal `MESSAGE`.
After querying the message, the function constructs a `CompareMessage` instance with the `expected` field set to the `MESSAGE` string. Then it writes the message to it using the [`write!`] macro. After the write, it reads the `expected` field and fails the test if it is not the empty string.

[`write!`]: https://doc.rust-lang.org/core/macro.write.html

Now we can run the test using `cargo xtest --test panic_handler`. We see that it passes, which means that the reported panic info is correct. If we use a wrong line number in `PANIC_LINE` or panic with an additional character through `panic!("{}x", MESSAGE)`, we see that the test indeed fails.

Expand Down