|
| 1 | +# First steps with RMC |
| 2 | + |
| 3 | +> This tutorial expects you to have followed the RMC [installation instructions](./install-guide.md) first. |
| 4 | +
|
| 5 | +RMC is unlike the testing tools you may already be familiar with. |
| 6 | +Much of testing is concerned with thinking of new corner cases that need to be covered. |
| 7 | +With RMC, all the corner cases are covered from the start, and the new concern is narrowing down the scope to something manageable for the checker. |
| 8 | + |
| 9 | +Consider this first program (which can be found under [`rmc-docs/src/tutorial/rmc-first-steps`](https://github.com/model-checking/rmc/tree/main/rmc-docs/src/tutorial/rmc-first-steps/)): |
| 10 | + |
| 11 | +```rust |
| 12 | +{{#include tutorial/rmc-first-steps/src/lib.rs:code}} |
| 13 | +``` |
| 14 | + |
| 15 | +Think about the test harness you would need to write to test this function. |
| 16 | +You would need figure out a whole set of arguments to call the function with that would exercise each branch. |
| 17 | +You would need to keep that test harness up-to-date with the code, in case some of the branches change. |
| 18 | +And if this function was more complicated—for example, if some of the branches depended on global state—the test harness would be even more onerous to write. |
| 19 | + |
| 20 | +We can try to property test a function like this, but if we're naive about it (and consider all possible `u32` inputs), then it's unlikely we'll ever find the bug. |
| 21 | + |
| 22 | +```rust |
| 23 | +{{#include tutorial/rmc-first-steps/src/lib.rs:proptest}} |
| 24 | +``` |
| 25 | + |
| 26 | +``` |
| 27 | +# cargo test |
| 28 | +[...] |
| 29 | +test tests::doesnt_crash ... ok |
| 30 | +``` |
| 31 | + |
| 32 | +There's only 1 in 4 billion inputs that fail, so it's vanishingly unlikely the property test will find it, even with a million samples. |
| 33 | + |
| 34 | +With RMC, however: |
| 35 | + |
| 36 | +```rust |
| 37 | +{{#include tutorial/rmc-first-steps/src/lib.rs:rmc}} |
| 38 | +``` |
| 39 | + |
| 40 | +``` |
| 41 | +# cargo rmc |
| 42 | +[...] |
| 43 | +Runtime decision procedure: 0.00116886s |
| 44 | +
|
| 45 | +** Results: |
| 46 | +./src/lib.rs function estimate_size |
| 47 | +[estimate_size.assertion.1] line 9 Oh no, a failing corner case!: FAILURE |
| 48 | +
|
| 49 | +** 1 of 1 failed (2 iterations) |
| 50 | +VERIFICATION FAILED |
| 51 | +``` |
| 52 | + |
| 53 | +RMC has immediately found a failure. |
| 54 | +Notably, we haven't had to write explicit assertions in our "proof harness": by default, RMC will find a host of erroneous conditions which include a reachable call to `panic` or a failing `assert`. |
| 55 | + |
| 56 | +By default, RMC only reports failures, not how the failure happened. |
| 57 | +This is because, in its full generality, understanding how a failure happened requires exploring a full (potentially large) execution trace. |
| 58 | +Here, we've just got some nondeterministic inputs up front, but that's something of a special case that has a "simpler" explanation (just the choice of nondeterministic input). |
| 59 | + |
| 60 | +To see traces, run: |
| 61 | + |
| 62 | +``` |
| 63 | +rmc --visualize src/lib.rs |
| 64 | +open report/html/index.html |
| 65 | +``` |
| 66 | + |
| 67 | +The first command runs RMC and generates the html-based report in `report/`. |
| 68 | +The second command opens that report in your default browser (on mac, on linux desktops try `xdg-open`). |
| 69 | +From this report, we can find the trace of the failure and filter through it to find the relevant line (at present time, an unfortunate amount of generated code is present in the trace): |
| 70 | + |
| 71 | +``` |
| 72 | +let x: u32 = __nondet(); |
| 73 | +x = 1023u |
| 74 | +``` |
| 75 | + |
| 76 | +Here we're seeing the line of code and the value assigned in this particular trace. |
| 77 | +Like property testing, this is just one example of a failure. |
| 78 | +To find more, we'd presumably fix this issue and then re-run RMC. |
| 79 | + |
| 80 | +### Exercise: Try other failures |
| 81 | + |
| 82 | +We put an explicit panic in this function, but it's not the only kind of failure RMC will find. |
| 83 | +Try a few other types of errors. |
| 84 | + |
| 85 | +For example, instead of panicking we could try explicitly dereferencing a null pointer: |
| 86 | + |
| 87 | +```rust |
| 88 | +unsafe { return *(0 as *const u32) }; |
| 89 | +``` |
| 90 | + |
| 91 | +Notably, however, the Rust compiler emits a warning here: |
| 92 | + |
| 93 | +``` |
| 94 | +warning: dereferencing a null pointer |
| 95 | + --> src/lib.rs:10:29 |
| 96 | + | |
| 97 | +10 | unsafe { return *(0 as *const u32) }; |
| 98 | + | ^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed |
| 99 | + | |
| 100 | + = note: `#[warn(deref_nullptr)]` on by default |
| 101 | +``` |
| 102 | + |
| 103 | +Still, it is just a warning, and we can run the code without test failures just as before. |
| 104 | +But RMC still catches the issue: |
| 105 | + |
| 106 | +``` |
| 107 | +** Results: |
| 108 | +./src/lib.rs function estimate_size |
| 109 | +[estimate_size.pointer_dereference.1] line 10 dereference failure: pointer NULL in *var_10: FAILURE |
| 110 | +
|
| 111 | +** 1 of 1 failed (2 iterations) |
| 112 | +VERIFICATION FAILED |
| 113 | +``` |
| 114 | + |
| 115 | +**Can you find an example where the Rust compiler will not complain, and RMC will?** |
| 116 | + |
| 117 | +<details> |
| 118 | +<summary>Click to show one possible answer</summary> |
| 119 | + |
| 120 | +``` |
| 121 | +return 1 << x; |
| 122 | +``` |
| 123 | + |
| 124 | +Overflow (addition, multiplication, etc, and this case, [bitshifting by too much](https://github.com/rust-lang/rust/issues/10183)) is also caught by RMC: |
| 125 | + |
| 126 | +``` |
| 127 | +** Results: |
| 128 | +./src/lib.rs function estimate_size |
| 129 | +[estimate_size.assertion.1] line 10 attempt to shift left by `move _10`, which would overflow: FAILURE |
| 130 | +[estimate_size.undefined-shift.1] line 10 shift distance too large in 1 << var_10: FAILURE |
| 131 | +
|
| 132 | +** 2 of 2 failed (2 iterations) |
| 133 | +VERIFICATION FAILED |
| 134 | +``` |
| 135 | + |
| 136 | +</details> |
| 137 | + |
| 138 | +## Assertions, Assumptions, and Harnesses |
| 139 | + |
| 140 | +It seems a bit odd that we can take billions of inputs, but our function clearly only handles up to a few thousand. |
| 141 | +Let's codify this fact about our function by asserting some reasonable bound on our input, after we've fixed our bug: |
| 142 | + |
| 143 | +```rust |
| 144 | +{{#include tutorial/rmc-first-steps/tests/final-form.rs:code}} |
| 145 | +``` |
| 146 | + |
| 147 | +Now we have stated our previously implicit expectation: this function should never be called with inputs that are too big. |
| 148 | +But if we attempt to verify this, we get a problem: |
| 149 | + |
| 150 | +``` |
| 151 | +** Results: |
| 152 | +./tests/final-form.rs function estimate_size |
| 153 | +[estimate_size.assertion.1] line 3 assertion failed: x < 4096: FAILURE |
| 154 | +
|
| 155 | +** 1 of 1 failed (2 iterations) |
| 156 | +VERIFICATION FAILED |
| 157 | +``` |
| 158 | + |
| 159 | +We intended this to be a precondition of calling the function, but RMC is treating it like a failure. |
| 160 | +If we call this function with too large of a value, it will crash with an assertion failure. |
| 161 | +But we know that, that was our intention. |
| 162 | + |
| 163 | +This is the purpose of _proof harnesses_. |
| 164 | +Much like property testing (which would also find this assertion failure as a bug), we need to set up our preconditions, call the function in question, then assert our post conditions. |
| 165 | +Here's a revised example of the proof harness, one that now succeeds: |
| 166 | + |
| 167 | +```rust |
| 168 | +{{#include tutorial/rmc-first-steps/tests/final-form.rs:rmc}} |
| 169 | +``` |
| 170 | + |
| 171 | +But now we must wonder if we've really fully tested our function. |
| 172 | +What if we revise the function, but forget to update the assumption in our proof harness to cover the new range of inputs? |
| 173 | + |
| 174 | +Fortunately, RMC is able to report a coverage metric for each proof harness. |
| 175 | +Try running: |
| 176 | + |
| 177 | +``` |
| 178 | +rmc --visualize tests/final-form.rs |
| 179 | +open report/html/index.html |
| 180 | +``` |
| 181 | + |
| 182 | +The beginning of the report includes coverage information. |
| 183 | +Clicking through to the file will show fully-covered lines in green. |
| 184 | +Lines not covered by our proof harness will show in red. |
| 185 | + |
| 186 | +1. Try changing the assumption in the proof harness to `x < 2048`. Now the harness won't be testing all possible cases. |
| 187 | +2. Rerun `rmc --visualize` on the file |
| 188 | +3. Look at the report: you'll see we no longer have 100% coverage of the function. |
| 189 | + |
| 190 | + |
| 191 | +## Summary |
| 192 | + |
| 193 | +In this section: |
| 194 | + |
| 195 | +1. We saw RMC find panics, assertion failures, and even some other failures like unsafe dereferencing of null pointers. |
| 196 | +2. We saw how to get a failing trace using `rmc --visualize` |
| 197 | +3. We saw how proof harnesses are used to set up preconditions and assert postconditions. |
| 198 | +4. We saw how to obtain coverage metrics and use them to ensure our proofs are covering as much as they should be. |
0 commit comments