Files
pezkuwi-subxt/substrate/frame/im-online/src/tests.rs
T
Andrew Jones 49b6dfd2e5 Enrich metadata with type information (#8615)
* Cargo.lock after merge

* Restore scale-info feature

* Fully qualify TypeInfo derive

* Skip PendingSwap T

* Add missing skip_type_params attr

* metadata docs features

* Reduce pallet event attribute to struct

* Cargo.lock

* Update frame/balances/src/tests_composite.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Line widths check

* Cargo.lock

* Add scale-info/std

* Update frame/system/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Use `skip_type_params` to remove `TypeInfo` requirements on checks

* Revert "Remove unused Call metadata stuff"

This reverts commit 41311f85

* Skip BalanceSwapAction type parameter

* Remove unused event metadata macro

* Update frame-metadata

* Update primitives/npos-elections/compact/src/codec.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Manual TypeInfo for Header

* Remove TypeInfo requirement for consts in BoundedVec etc.

* Another TypeInfo bound removed

* review: fix indentation

* TypeInfo impls for Identity types

* Add some todos to add custom TypeInfo impls

* Update frame/support/procedural/src/pallet/expand/pallet_struct.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Add some todos to add custom TypeInfo impls

* Add a test for manual Data TypeInfo impl

* Add custom TypeInfo impl for Vote

* Era custom TypeInfo crimes

* Revert finality-grandpa version to 0.14.z

* review: renamed module to pallet_constants_metadata

* New line at end of file

* Add missing scale-info/std

* Update frame/support/src/storage/types/mod.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Remove StorageEntryType::Map unused flag

* Add missing scale-info dependency after merge

* SignedExtension::AdditionalSigned metadata

* Update frame-metadata, use abbreviated docs and args fields

* Update frame/example/Cargo.toml

Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>

* Add scale_info/std and remove unused scale-info dependency

* Remove scale-info dependency

* Remove treasury pallet::metadata

* Remove redundant Event test

* Add back scale-info as dev dependency

* fix error metadata when no error defined in decl_module

* Add Module3 to tests

* Fix metadata test

* Add docs feature to frame-support test

* WIP fixing pallet metadata test

* Remove redundant FunctionMetadata, FunctionArgumentMetadata as per https://github.com/paritytech/frame-metadata/pull/20

* Use main branch of frame-metadata

* Use patch of scale-info for latest changes

* Use latest patched scale-info

* Manual TypeInfo for DigestItem

* Manual TypeInfo for DigestItem

* Update scale-info

* Skip __Ignore variants for Error, depends on https://github.com/paritytech/scale-info/pull/117

* Named fields for FRAME v2 pallet Call variants

* Named fields for FRAME v1 pallet Call variants

* Add missing scale-info dependency

* WIP expand benchmark call variant

* fix benchmark with new function

create a new function for each variant of a pallet call.
This function is called by benchmarking macro in order not to break call
creation with unnamed argument

* fix tests

* more fix

* Fix staking tests

* Fix offchain workers calls

* Cherry pick rustfmt.toml from master

* cargo +nightly-2021-06-22 fmt --all

* Update to new call variant structs

* More call variant struct updates

* Remove unused import

* More call variant structs

* More call variant structs

* Even more call variant structs

* Mooar variant structs

* Evermore variant structs

* Call variant structs ad infinitum

* Fmt

* More call variants

* Last call variant

* Call variants all done?

* Fix SS58Prefix type

* Potential workaround for BitFlags<IdentityFields> TypeInfo

* Enable docs capturing for Call, Event, and Error types

* Fix IdentityFields TypeInfo

* Remove metadata-docs feature

* Add capture_docs = true for legacy Call, Event and Error types

* Fmt

* Fix metadata test type

* Update benchmarks with call struct variants

* Fmt

* More test fixes

* Fmt

* Fix benches

* Use latest capture_docs attr

* Latest scale_info

* Fmt

* review: change &Vec to &[]

* Remove pallet metadata attr

* review: remove commented out test code

* review: skip_type_params trailing comma suggestion

* Update to scale-info 0.10.0

* Update construct_runtime ui tests, different because of metadata TypeInfo impls

* Add some TypeInfo derives for UI tests

* Update storage ensure span ui stderrs

* Update call argument bound ui tests

Possibly changed because change from tuple to struct variants?

* Add scale-info dev dependency

* Update to latest finality-grandpa release

* review: missing newline

* review: missing scale-info/std

* review: remove duplicate scale-info/std

* review: remove fully qualified TypeInfo

* review: add missing scale-info/std

* review: remove unnecessary imports.

* Fmt

* Use crates.io RC version of frame-metadata

* Remove scale-info/std because it is a dev dependency

* Add missing scale_info dev-dependency for test

* Delete empty metadata folder

* Fix sp_std import

* review: improve manual UncheckedExtrinsic TypeInfo impl

* review: use full scale-info for dev-dependency

* Remove DefaultByteGetter impl

* review: derive TypeInfo for generic header

* Fmt

* Update primitives/runtime/src/generic/unchecked_extrinsic.rs

Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>

* Update primitives/runtime/src/generic/unchecked_extrinsic.rs

Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>

* Update bin/node/executor/Cargo.toml

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update frame/identity/src/types.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update frame/support/src/dispatch.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Remove redundant derive

* Simplify scale-info dependency

* Strip underscore prefix from call variant struct names

* Another underscore field

* More underscore fields

* Another underscore field

* Update to frame-metadata 14.0.0-rc.2 with combined StorageEntryType::Map

* Fmt

* Revert weights formatting

* Fix up some tests

* Fix up some tests for StorageEntryTypeMetadata

* scale-info dev dependency

* Fix test error

* Add missing TypeInfo derives

* Add back missing scale-info dependency

* Add back missing scale-info dependency

* Fix npos compact impls

* Cargo.lock

* Fmt

* Fix errors

* Fmt

* Fix renamed raw_solution field

* Fix error

* Fmt

* Fix some benchmarks

* Fmt

* Stray R

* Fix

* Add missing TypeInfos

* ui test fix

* Fix line widths

* Revert "ui test fix"

This reverts commit 2d15ec058a216e3f92d713f1174603a2bb1eac65.

* Upgrade to scale-info 0.11.0

* Revert "Upgrade to scale-info 0.11.0"

This reverts commit 047bb179085a0059c36cd20ab405f55cf0867e28.

* Add Runtime type

* Update to scale-info 0.12

* Update to scale-info 1.0

* Update frame-metadata to version 14.0.0

* Patch finality-grandpa until release available

* Fix metadata tests

* Fix metadata tests

* Fmt

* Remove patched finality-grandpa

* Fix tests, use scale_info imports

* Fix pallet tests

* Add BlockNumber TypeInfo bound

* ui test fix

* Cargo.lock

* Remove pallet metadata

* Cargo.lock

* Add missing scale-info dependency

* Remove pallet event metadata

* Fix error

* Fix collective errors

* Semicolol

* Fmt

* Remove another metadata attribute

* Add new variant to custom digest TypeInfo

* Fmt

* Cargo.lock from master

* Remove comma lol

* Fix example call error

* Fix example call error properly

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
2021-09-15 11:40:41 +00:00

529 lines
16 KiB
Rust

// This file is part of Substrate.
// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
//! Tests for the im-online module.
#![cfg(test)]
use super::*;
use crate::mock::*;
use frame_support::{assert_noop, dispatch};
use sp_core::{
offchain::{
testing::{TestOffchainExt, TestTransactionPoolExt},
OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
},
OpaquePeerId,
};
use sp_runtime::{
testing::UintAuthorityId,
transaction_validity::{InvalidTransaction, TransactionValidityError},
};
#[test]
fn test_unresponsiveness_slash_fraction() {
// A single case of unresponsiveness is not slashed.
assert_eq!(UnresponsivenessOffence::<()>::slash_fraction(1, 50), Perbill::zero());
assert_eq!(
UnresponsivenessOffence::<()>::slash_fraction(5, 50),
Perbill::zero(), // 0%
);
assert_eq!(
UnresponsivenessOffence::<()>::slash_fraction(7, 50),
Perbill::from_parts(4200000), // 0.42%
);
// One third offline should be punished around 5%.
assert_eq!(
UnresponsivenessOffence::<()>::slash_fraction(17, 50),
Perbill::from_parts(46200000), // 4.62%
);
}
#[test]
fn should_report_offline_validators() {
new_test_ext().execute_with(|| {
// given
let block = 1;
System::set_block_number(block);
// buffer new validators
advance_session();
// enact the change and buffer another one
let validators = vec![1, 2, 3, 4, 5, 6];
VALIDATORS.with(|l| *l.borrow_mut() = Some(validators.clone()));
advance_session();
// when
// we end current session and start the next one
advance_session();
// then
let offences = OFFENCES.with(|l| l.replace(vec![]));
assert_eq!(
offences,
vec![(
vec![],
UnresponsivenessOffence {
session_index: 2,
validator_set_count: 3,
offenders: vec![(1, 1), (2, 2), (3, 3),],
}
)]
);
// should not report when heartbeat is sent
for (idx, v) in validators.into_iter().take(4).enumerate() {
let _ = heartbeat(block, 3, idx as u32, v.into(), Session::validators()).unwrap();
}
advance_session();
// then
let offences = OFFENCES.with(|l| l.replace(vec![]));
assert_eq!(
offences,
vec![(
vec![],
UnresponsivenessOffence {
session_index: 3,
validator_set_count: 6,
offenders: vec![(5, 5), (6, 6),],
}
)]
);
});
}
fn heartbeat(
block_number: u64,
session_index: u32,
authority_index: u32,
id: UintAuthorityId,
validators: Vec<u64>,
) -> dispatch::DispatchResult {
use frame_support::unsigned::ValidateUnsigned;
let heartbeat = Heartbeat {
block_number,
network_state: OpaqueNetworkState {
peer_id: OpaquePeerId(vec![1]),
external_addresses: vec![],
},
session_index,
authority_index,
validators_len: validators.len() as u32,
};
let signature = id.sign(&heartbeat.encode()).unwrap();
ImOnline::pre_dispatch(&crate::Call::heartbeat {
heartbeat: heartbeat.clone(),
signature: signature.clone(),
})
.map_err(|e| match e {
TransactionValidityError::Invalid(InvalidTransaction::Custom(INVALID_VALIDATORS_LEN)) =>
"invalid validators len",
e @ _ => <&'static str>::from(e),
})?;
ImOnline::heartbeat(Origin::none(), heartbeat, signature)
}
#[test]
fn should_mark_online_validator_when_heartbeat_is_received() {
new_test_ext().execute_with(|| {
advance_session();
// given
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
assert_eq!(Session::current_index(), 2);
assert_eq!(Session::validators(), vec![1, 2, 3]);
assert!(!ImOnline::is_online(0));
assert!(!ImOnline::is_online(1));
assert!(!ImOnline::is_online(2));
// when
let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap();
// then
assert!(ImOnline::is_online(0));
assert!(!ImOnline::is_online(1));
assert!(!ImOnline::is_online(2));
// and when
let _ = heartbeat(1, 2, 2, 3.into(), Session::validators()).unwrap();
// then
assert!(ImOnline::is_online(0));
assert!(!ImOnline::is_online(1));
assert!(ImOnline::is_online(2));
});
}
#[test]
fn late_heartbeat_and_invalid_keys_len_should_fail() {
new_test_ext().execute_with(|| {
advance_session();
// given
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
assert_eq!(Session::current_index(), 2);
assert_eq!(Session::validators(), vec![1, 2, 3]);
// when
assert_noop!(
heartbeat(1, 3, 0, 1.into(), Session::validators()),
"Transaction is outdated"
);
assert_noop!(
heartbeat(1, 1, 0, 1.into(), Session::validators()),
"Transaction is outdated"
);
// invalid validators_len
assert_noop!(heartbeat(1, 2, 0, 1.into(), vec![]), "invalid validators len");
});
}
#[test]
fn should_generate_heartbeats() {
use frame_support::traits::OffchainWorker;
let mut ext = new_test_ext();
let (offchain, _state) = TestOffchainExt::new();
let (pool, state) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
// given
let block = 1;
System::set_block_number(block);
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
// buffer new validators
Session::rotate_session();
// enact the change and buffer another one
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
Session::rotate_session();
// when
ImOnline::offchain_worker(block);
// then
let transaction = state.write().transactions.pop().unwrap();
// All validators have `0` as their session key, so we generate 2 transactions.
assert_eq!(state.read().transactions.len(), 2);
// check stuff about the transaction.
let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap();
let heartbeat = match ex.call {
crate::mock::Call::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => heartbeat,
e => panic!("Unexpected call: {:?}", e),
};
assert_eq!(
heartbeat,
Heartbeat {
block_number: block,
network_state: sp_io::offchain::network_state().unwrap(),
session_index: 2,
authority_index: 2,
validators_len: 3,
}
);
});
}
#[test]
fn should_cleanup_received_heartbeats_on_session_end() {
new_test_ext().execute_with(|| {
advance_session();
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
assert_eq!(Session::current_index(), 2);
assert_eq!(Session::validators(), vec![1, 2, 3]);
// send an heartbeat from authority id 0 at session 2
let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap();
// the heartbeat is stored
assert!(!ImOnline::received_heartbeats(&2, &0).is_none());
advance_session();
// after the session has ended we have already processed the heartbeat
// message, so any messages received on the previous session should have
// been pruned.
assert!(ImOnline::received_heartbeats(&2, &0).is_none());
});
}
#[test]
fn should_mark_online_validator_when_block_is_authored() {
use pallet_authorship::EventHandler;
new_test_ext().execute_with(|| {
advance_session();
// given
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
assert_eq!(Session::current_index(), 2);
assert_eq!(Session::validators(), vec![1, 2, 3]);
for i in 0..3 {
assert!(!ImOnline::is_online(i));
}
// when
ImOnline::note_author(1);
ImOnline::note_uncle(2, 0);
// then
assert!(ImOnline::is_online(0));
assert!(ImOnline::is_online(1));
assert!(!ImOnline::is_online(2));
});
}
#[test]
fn should_not_send_a_report_if_already_online() {
use pallet_authorship::EventHandler;
let mut ext = new_test_ext();
let (offchain, _state) = TestOffchainExt::new();
let (pool, pool_state) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
advance_session();
// given
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
assert_eq!(Session::validators(), Vec::<u64>::new());
// enact the change and buffer another one
advance_session();
assert_eq!(Session::current_index(), 2);
assert_eq!(Session::validators(), vec![1, 2, 3]);
ImOnline::note_author(2);
ImOnline::note_uncle(3, 0);
// when
UintAuthorityId::set_all_keys(vec![1, 2, 3]);
// we expect error, since the authority is already online.
let mut res = ImOnline::send_heartbeats(4).unwrap();
assert_eq!(res.next().unwrap().unwrap(), ());
assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(1));
assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(2));
assert_eq!(res.next(), None);
// then
let transaction = pool_state.write().transactions.pop().unwrap();
// All validators have `0` as their session key, but we should only produce 1 heartbeat.
assert_eq!(pool_state.read().transactions.len(), 0);
// check stuff about the transaction.
let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap();
let heartbeat = match ex.call {
crate::mock::Call::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => heartbeat,
e => panic!("Unexpected call: {:?}", e),
};
assert_eq!(
heartbeat,
Heartbeat {
block_number: 4,
network_state: sp_io::offchain::network_state().unwrap(),
session_index: 2,
authority_index: 0,
validators_len: 3,
}
);
});
}
#[test]
fn should_handle_missing_progress_estimates() {
use frame_support::traits::OffchainWorker;
let mut ext = new_test_ext();
let (offchain, _state) = TestOffchainExt::new();
let (pool, state) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
let block = 1;
System::set_block_number(block);
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
// buffer new validators
Session::rotate_session();
// enact the change and buffer another one
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![0, 1, 2]));
Session::rotate_session();
// we will return `None` on the next call to `estimate_current_session_progress`
// and the offchain worker should fallback to checking `HeartbeatAfter`
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
ImOnline::offchain_worker(block);
assert_eq!(state.read().transactions.len(), 3);
});
}
#[test]
fn should_handle_non_linear_session_progress() {
// NOTE: this is the reason why we started using `EstimateNextSessionRotation` to figure out if
// we should send a heartbeat, it's possible that between successive blocks we progress through
// the session more than just one block increment (in BABE session length is defined in slots,
// not block numbers).
let mut ext = new_test_ext();
let (offchain, _state) = TestOffchainExt::new();
let (pool, _) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
// buffer new validator
Session::rotate_session();
// mock the session length as being 10 blocks long,
// enact the change and buffer another one
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![0, 1, 2]));
// mock the session length has being 10 which should make us assume the fallback for half
// session will be reached by block 5.
MOCK_AVERAGE_SESSION_LENGTH.with(|p| *p.borrow_mut() = Some(10));
Session::rotate_session();
// if we don't have valid results for the current session progres then
// we'll fallback to `HeartbeatAfter` and only heartbeat on block 5.
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly));
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
assert!(ImOnline::send_heartbeats(5).ok().is_some());
// if we have a valid current session progress then we'll heartbeat as soon
// as we're past 80% of the session regardless of the block number
MOCK_CURRENT_SESSION_PROGRESS
.with(|p| *p.borrow_mut() = Some(Some(Permill::from_percent(81))));
assert!(ImOnline::send_heartbeats(2).ok().is_some());
});
}
#[test]
fn test_does_not_heartbeat_early_in_the_session() {
let mut ext = new_test_ext();
let (offchain, _state) = TestOffchainExt::new();
let (pool, _) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
// mock current session progress as being 5%. we only randomly start
// heartbeating after 10% of the session has elapsed.
MOCK_CURRENT_SESSION_PROGRESS
.with(|p| *p.borrow_mut() = Some(Some(Permill::from_float(0.05))));
assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly));
});
}
#[test]
fn test_probability_of_heartbeating_increases_with_session_progress() {
let mut ext = new_test_ext();
let (offchain, state) = TestOffchainExt::new();
let (pool, _) = TestTransactionPoolExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
let set_test = |progress, random: f64| {
// the average session length is 100 blocks, therefore the residual
// probability of sending a heartbeat is 1%
MOCK_AVERAGE_SESSION_LENGTH.with(|p| *p.borrow_mut() = Some(100));
MOCK_CURRENT_SESSION_PROGRESS
.with(|p| *p.borrow_mut() = Some(Some(Permill::from_float(progress))));
let mut seed = [0u8; 32];
let encoded = ((random * Permill::ACCURACY as f64) as u32).encode();
seed[0..4].copy_from_slice(&encoded);
state.write().seed = seed;
};
let assert_too_early = |progress, random| {
set_test(progress, random);
assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly));
};
let assert_heartbeat_ok = |progress, random| {
set_test(progress, random);
assert!(ImOnline::send_heartbeats(2).ok().is_some());
};
assert_too_early(0.05, 1.0);
assert_too_early(0.1, 0.1);
assert_too_early(0.1, 0.011);
assert_heartbeat_ok(0.1, 0.010);
assert_too_early(0.4, 0.015);
assert_heartbeat_ok(0.4, 0.014);
assert_too_early(0.5, 0.026);
assert_heartbeat_ok(0.5, 0.025);
assert_too_early(0.6, 0.057);
assert_heartbeat_ok(0.6, 0.056);
assert_too_early(0.65, 0.086);
assert_heartbeat_ok(0.65, 0.085);
assert_too_early(0.7, 0.13);
assert_heartbeat_ok(0.7, 0.12);
assert_too_early(0.75, 0.19);
assert_heartbeat_ok(0.75, 0.18);
});
}