new post: Quick check: does your code work?
This commit is contained in:
parent
8914e00a5d
commit
14ac659a43
112
_posts/2016-05-12-quickcheck-does-your-code-work.md
Normal file
112
_posts/2016-05-12-quickcheck-does-your-code-work.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
layout: post
|
||||
title: "Quick check: does your code work?"
|
||||
date: 12.05.2016 23:00
|
||||
---
|
||||
|
||||
… because mine didn't. At least not correctly in all cases.
|
||||
I'm talking about my Rust library [lzf-rs](https://crates.io/crates/lzf),
|
||||
a port of the small compression library [LibLZF](http://software.schmorp.de/pkg/liblzf.html).
|
||||
It started as a wrapper around the C library, but I rewrote it in Rust for v0.3.
|
||||
I now found three major bugs and I want to tell you how (tl;dr: Bug fixes and tests: [PR #1][pr]).
|
||||
|
||||
For a university paper I'm currently looking into different methods for automatic test generation,
|
||||
such as *symbolic execution*, *fuzzing* and *random test generation*.
|
||||
One of the popular methods is property-based testing, with QuickCheck being the best known application of this method.
|
||||
QuickCheck started as a Haskell library (see the [original paper](http://www.eecs.northwestern.edu/~robby/courses/395-495-2009-fall/quick.pdf)),
|
||||
but is ported to several other languages, including C (see [theft](https://github.com/silentbicycle/theft))
|
||||
and of course Rust: [QuickCheck](https://github.com/BurntSushi/quickcheck).
|
||||
|
||||
I knew this library for some time now, but never used it.
|
||||
So today I decided to use it for my [lzf](https://crates.io/crates/lzf) crate.
|
||||
Let me walk you through the process on how to use it.
|
||||
|
||||
First, you need to add the dependency and load it in your code.
|
||||
Add it to your `Cargo.toml`:
|
||||
|
||||
~~~toml
|
||||
[dependencies]
|
||||
quickcheck = "0.2"
|
||||
~~~
|
||||
|
||||
Add this to your `src/lib.rs`:
|
||||
|
||||
~~~rust
|
||||
extern crate quickcheck;
|
||||
~~~
|
||||
|
||||
Next, you need to decide what property to test.
|
||||
As the compression library needs data to compress and _valid_ data to decompress,
|
||||
I decided the easiest way to go through everything would be to test the round trip:
|
||||
Compress some random input, then decompress the compressed data and check that it maches the initial input.
|
||||
This should hold for all inputs, that can be compressed.
|
||||
Everything that cannot be compressed can be ignored at this point (a first test allowing _all_ input turned up too many false-positives).
|
||||
|
||||
The property function looks like this:
|
||||
|
||||
|
||||
~~~rust
|
||||
fn compress_decompress_round(data: Vec<u8>) -> TestResult {
|
||||
let compr = match compress(&data) {
|
||||
Ok(compr) => compr,
|
||||
Err(LzfError::NoCompressionPossible) => return TestResult::discard(),
|
||||
Err(LzfError::DataCorrupted) => return TestResult::discard(),
|
||||
e @ _ => panic!(e),
|
||||
};
|
||||
|
||||
let decompr = decompress(&compr, data.len()).unwrap();
|
||||
TestResult::from_bool(data == decompr)
|
||||
}
|
||||
~~~
|
||||
|
||||
Of course we need to test this.
|
||||
QuickCheck handles the heavy part for us:
|
||||
|
||||
~~~rust
|
||||
#[test]
|
||||
fn qc_roundtrip() {
|
||||
quickcheck(compress_decompress_round as fn(_) -> _);
|
||||
}
|
||||
~~~
|
||||
|
||||
Running the tests immediately turned up a bug:
|
||||
|
||||
~~~
|
||||
$ cargo test
|
||||
running 13 tests
|
||||
...
|
||||
thread 'safe' panicked at 'index out of bounds: the len is 67 but the index is 67', ../src/libcollections/vec.rs:1187
|
||||
test quickcheck_test::qc_roundtrip ... FAILED
|
||||
|
||||
failures:
|
||||
|
||||
---- quickcheck_test::qc_roundtrip stdout ----
|
||||
thread 'quickcheck_test::qc_roundtrip' panicked at '[quickcheck] TEST FAILED (runtime error). Arguments: ([0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 1, 1, 0, 1, 2, 0, 1, 3, 0, 1, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 9, 0, 0, 10, 0, 0, 11, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 10, 0, 0])
|
||||
Error: "index out of bounds: the len is 67 but the index is 67"', /home/jer/.cargo/registry/src/github.com-88ac128001ac3a9a/quickcheck-0.2.27/src/tester.rs:116
|
||||
~~~
|
||||
|
||||
It would be okay to return an error, but out-of-bounds indexing (and thus panicing) is a clear bug in the library.
|
||||
Luckily, QuickCheck automatically collects the input the test failed on, tries to shrink it down to a minimal example and then displays it.
|
||||
I figured this bug is happening in the compress step, so I added an explicit test case for that:
|
||||
|
||||
~~~rust
|
||||
#[test]
|
||||
fn quickcheck_found_bug() {
|
||||
let inp = vec![0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 1, 1, 0, 1, 2, 0, 1, 3, 0, 1, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 9, 0, 0, 10, 0, 0, 11, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 10, 0, 0];
|
||||
|
||||
assert_eq!(LzfError::NoCompressionPossible, compress(&inp).unwrap_err());
|
||||
}
|
||||
~~~
|
||||
|
||||
Taking a look at the full stack trace (run `RUST_BACKTRACE=1 cargo test`) lead to the exact location of the bug.
|
||||
Turns out I was checking the bounds on the wrong variable.
|
||||
I fixed it in [88242ffe](https://github.com/badboy/lzf-rs/commit/88242ffef3b00423572db66318becd5206880d94).
|
||||
After this fix, I re-run the QuickCheck tests and it discovered a second bug (`[0]` lead to another out-of-bounds access) and I fixed it in [5b2e8150](https://github.com/badboy/lzf-rs/pull/1/commits/5b2e81506e83a797519d5d85c776de296769fdd3).
|
||||
I found a third bug, which I (hopefully) fixed, but I don't fully understand how it's happening yet.
|
||||
|
||||
Additionally to the above I added QuickCheck tests comparing the Rust functions to the output of the C library.
|
||||
The full changeset is in [PR #1][pr] (currently failing tests, because of a broken Clippy on newest nightly).
|
||||
|
||||
Now quick, check your own code!
|
||||
|
||||
[pr]: https://github.com/badboy/lzf-rs/pull/1
|
Loading…
Reference in a new issue