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

Introduce proptest in chainstate #294

Merged
merged 12 commits into from
Jul 26, 2022
Merged

Conversation

azarovh
Copy link
Member

@azarovh azarovh commented Jul 20, 2022

Replace explicit random number generation in chainstate tests with proptest.

This crate generates input values automatically. In case of a test failure it performs input shrinking (the process of finding minimal test input to reproduce the fail). After that it store the seed of failed case in a file for future regressions.

Example of regression seeds:

# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 9063b1363c7586876acf0491e5ce091fa9b32487f615fe1dd269664af493c862 # shrinks to atoms1 = 101461, atoms2 = 1064

By default every test is run 256 times with different inputs. But for the cases in chainstate it's an overhead, so I configured proptest to run each test once.

P.S. while reading the code I noticed that chainstate/src/lib.rs exposes ChainstateInterfaceImpl and detail::Chainstate as public, which looks wrong considering there is dedicated public interface ChainstateInterface.

Copy link
Contributor

@iljakuklic iljakuklic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just some small nitpicks.

chainstate/src/detail/mod.rs Show resolved Hide resolved
chainstate/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Contributor

@stanislav-tkach stanislav-tkach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

chainstate/src/detail/tests/double_spend_tests.rs Outdated Show resolved Hide resolved
chainstate/src/detail/tests/double_spend_tests.rs Outdated Show resolved Hide resolved
chainstate/src/detail/tests/double_spend_tests.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@TheQuantumPhysicist TheQuantumPhysicist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the naming, I think everything looks fine.

chainstate/src/detail/tests/mod.rs Outdated Show resolved Hide resolved
chainstate/src/detail/tests/mod.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@iljakuklic iljakuklic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments. The one on TestFramework::random_block I consider quite important to address (to be discussed whether now or as a subsequent PR)

chainstate-storage/src/mock.rs Show resolved Hide resolved
chainstate/src/detail/tests/mod.rs Outdated Show resolved Hide resolved
chainstate/src/detail/tests/test_framework.rs Show resolved Hide resolved
test-utils/Cargo.toml Outdated Show resolved Hide resolved
@iljakuklic
Copy link
Contributor

iljakuklic commented Jul 25, 2022

Was experimenting a bit with ways we could pass custom or randomly-initialized seeds to test cases, and the following hacekd together prototype seemed to work okay:

use test_case::test_case;
use rand::{rngs::StdRng, Rng, SeedableRng, RngCore};
type Seed = <StdRng as SeedableRng>::Seed;

// The test previously failed with these seeds. Fixed in commit 0xabcd, blah blah.
#[test_case([0x12; 32])]
#[test_case([0x13; 32])]
// Generate a new seed at random.
#[test_case(rand::thread_rng().gen())]
fn silly(seed: Seed) {
    let mut rng = StdRng::from_seed(seed);
    let x = rng.next_u32() as u64;
    assert_eq!(2 * x, x + x)
}

It uses the test_case crate to run the test with custom seeds (presumably populated with seeds that failed at some point) followed by test case that generates a single fresh seed. There are other crates with similar functionality (e.g. rstest, parametrized); test_case seemed to work fine but there might be better options.

Note this prototype uses rand directly, which the real version should not, so really the annotations would look something like:

#[test_case(Seed::from_str("1212121212121212..."))]
#[test_case(Seed::from_str("1313131313131313..."))]
#[test_case(Seed::fresh())]

If the Seed::fresh constructor prints the random seed to stdout when it's run, it will show up in the output upon test failure.

Thoughts / comments?

@TheQuantumPhysicist
Copy link
Contributor

If this prints the seeded value, then I think it's a good choice.

@azarovh
Copy link
Member Author

azarovh commented Jul 25, 2022

Thoughts / comments?

I like the idea and will try to apply it. rstest looks like the preferred option here because it will give us fixture functionality as well.

Sam, I believe that seed will be printed with the same function as right now (or maybe in fixture, not sure yet). The difference is that a seed would be a parameter to test case and the regression list would be explicit.

@iljakuklic iljakuklic closed this Jul 26, 2022
@iljakuklic iljakuklic reopened this Jul 26, 2022
@iljakuklic
Copy link
Contributor

Sorry closed the PR by accident

@mintlayer mintlayer deleted a comment from iljakuklic Jul 26, 2022
@iljakuklic
Copy link
Contributor

If this prints the seeded value, then I think it's a good choice.

Well there are two options:

  1. We print the seed to stdout or stderr when it's generated. It will then be shown in the test output (which is only printed when the test fails by default)
  2. Some of the crates I mentioned support printing the function inputs, e.g. rstest has the #[trace] attribute that enables it.

I am leaning towards (1) since it gives more freedom to pick these crates. It also only prints the seed, in case the table driven testing is used for more stuff besides the seed (i.e. there are multiple arguments, only one of them is seed, we probably want to only print the seed, not several kB of block data). However, there might be other criteria that I have not considered.

@TheQuantumPhysicist
Copy link
Contributor

Let's start with simple printing, since Stanislav showed that nextest also prints things correctly and doesn't mix output from different threads.

@iljakuklic
Copy link
Contributor

Thoughts / comments?

I like the idea and will try to apply it. rstest looks like the preferred option here because it will give us fixture functionality as well.

The rstest crate seems more featureful and can be made more ergonomic to with automatic conversions, which is a clear advantage of that crate.

However, there are some other criteria that come to my mind that should be considered when picking the crate to use:

  • I noticed test_case generates a Rust test for each test case (with naming scheme: test_name + :: + test_case_name). That means the test cases show up individually in the listing of tests and can be run/filtered individually. It would be nice to have that feature, not sure how other crates fare in this regard.
  • In a security-sensitive application, the dependency footprint should also be considered. Pulling more third-party code increases the attack surface. Maybe this is not as much of an issue in testing but still should be given some weight.

Please be aware that I only used test_case and only very briefly, so my comments come largely from a place of ignorance.

@azarovh
Copy link
Member Author

azarovh commented Jul 26, 2022

rstest with #[trace] attributte produce nice structured output that doesn't require us to write any additional macros.

cargo test --package chainstate --lib -- detail::tests::double_spend_tests
     Finished test [unoptimized + debuginfo] target(s) in 0.28s`
     Running unittests src/lib.rs (target/debug/deps/chainstate-c420b73216d9337b)

running 5 tests
test detail::tests::double_spend_tests::spend_output_in_the_same_block_invalid_order::case_1 ... ok
test detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1 ... FAILED
test detail::tests::double_spend_tests::double_spend_tx_in_another_block::case_1 ... ok
test detail::tests::double_spend_tests::double_spend_tx_in_the_same_block::case_1 ... ok
test detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1 ... FAILED

failures:

---- detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1 stdout ----
------------ TEST ARGUMENTS ------------
seed = Seed(16783609377823135750)
-------------- TEST START --------------
thread 'detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1' panicked at 'explicit panic', chainstate/src/detail/tests/double_spend_tests.rs:292:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1 stdout ----
------------ TEST ARGUMENTS ------------
seed = Seed(533591205870860245)
-------------- TEST START --------------
thread 'detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1' panicked at 'explicit panic', chainstate/src/detail/tests/double_spend_tests.rs:69:9


failures:
    detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1
    detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1

test result: FAILED. 3 passed; 2 failed; 0 ignored; 0 measured; 58 filtered out; finished in 0.00s

error: test failed, to rerun pass '-p chainstate --lib'

Also these test cases can be filtered by name

chainstate-storage/src/store.rs Outdated Show resolved Hide resolved
chainstate/src/lib.rs Outdated Show resolved Hide resolved
p2p/tests/ban.rs Outdated Show resolved Hide resolved
p2p/tests/libp2p-gossipsub.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@iljakuklic iljakuklic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some comments. All minor suggestions or discussion points. I am happy with merging the PR as is from what I see.

chainstate/src/detail/orphan_blocks/pool.rs Outdated Show resolved Hide resolved
chainstate/src/detail/mod.rs Show resolved Hide resolved
chainstate/src/detail/tests/syncing_tests.rs Show resolved Hide resolved
test-utils/src/random.rs Show resolved Hide resolved
test-utils/src/random.rs Show resolved Hide resolved
@TheQuantumPhysicist
Copy link
Contributor

rstest with #[trace] attributte produce nice structured output that doesn't require us to write any additional macros.

cargo test --package chainstate --lib -- detail::tests::double_spend_tests

     Finished test [unoptimized + debuginfo] target(s) in 0.28s`
     Running unittests src/lib.rs (target/debug/deps/chainstate-c420b73216d9337b)

running 5 tests
test detail::tests::double_spend_tests::spend_output_in_the_same_block_invalid_order::case_1 ... ok
test detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1 ... FAILED
test detail::tests::double_spend_tests::double_spend_tx_in_another_block::case_1 ... ok
test detail::tests::double_spend_tests::double_spend_tx_in_the_same_block::case_1 ... ok
test detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1 ... FAILED

failures:

---- detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1 stdout ----
------------ TEST ARGUMENTS ------------
seed = Seed(16783609377823135750)
-------------- TEST START --------------
thread 'detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1' panicked at 'explicit panic', chainstate/src/detail/tests/double_spend_tests.rs:292:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1 stdout ----
------------ TEST ARGUMENTS ------------
seed = Seed(533591205870860245)
-------------- TEST START --------------
thread 'detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1' panicked at 'explicit panic', chainstate/src/detail/tests/double_spend_tests.rs:69:9


failures:
    detail::tests::double_spend_tests::spend_bigger_output_in_the_same_block::case_1
    detail::tests::double_spend_tests::spend_output_in_the_same_block::case_1

test result: FAILED. 3 passed; 2 failed; 0 ignored; 0 measured; 58 filtered out; finished in 0.00s

error: test failed, to rerun pass '-p chainstate --lib'

Also these test cases can be filtered by name

That looks good. I think we can give that a shot.

Copy link
Contributor

@TheQuantumPhysicist TheQuantumPhysicist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good now. A good step in the right direction.

Copy link
Contributor

@iljakuklic iljakuklic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff

@TheQuantumPhysicist TheQuantumPhysicist merged commit c3006ad into master Jul 26, 2022
@TheQuantumPhysicist TheQuantumPhysicist deleted the proptest_in_chainstate branch July 26, 2022 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants