Skip to content

Commit a4c9902

Browse files
authored
Add tests for the upgrade task (#2642)
* Adds stronger testing harness * Adds an iterator to generate test data for views * Adds a test to validate that consensus correctly updates its internal state when receiving an UpgradeCertificate * Correctly a mistake in consensus logic that took the UpgradeCertificate from the received QuourmProposal rather than the decided leaf.
1 parent 21a7e07 commit a4c9902

File tree

12 files changed

+786
-72
lines changed

12 files changed

+786
-72
lines changed

hotshot/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>> SystemContext<TYPES, I> {
234234
saved_leaves,
235235
saved_payloads,
236236
saved_da_certs: HashMap::new(),
237+
saved_upgrade_certs: HashMap::new(),
237238
// TODO this is incorrect
238239
// https://github.com/EspressoSystems/HotShot/issues/560
239240
locked_view: anchored_leaf.get_view_number(),

task-impls/src/consensus.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,13 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>, A: ConsensusApi<TYPES, I> +
498498
// We should just make sure we don't *sign* an UpgradeCertificate for an upgrade
499499
// that we do not support.
500500
if let Some(ref upgrade_cert) = proposal.data.upgrade_certificate {
501-
if !upgrade_cert.is_valid_cert(self.quorum_membership.as_ref()) {
501+
if upgrade_cert.is_valid_cert(self.quorum_membership.as_ref()) {
502+
self.consensus
503+
.write()
504+
.await
505+
.saved_upgrade_certs
506+
.insert(view, upgrade_cert.clone());
507+
} else {
502508
error!("Invalid upgrade_cert in proposal for view {}", *view);
503509
return;
504510
}
@@ -740,7 +746,7 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>, A: ConsensusApi<TYPES, I> +
740746
.last_synced_block_height
741747
.set(usize::try_from(leaf.get_height()).unwrap_or(0));
742748
}
743-
if let Some(ref upgrade_cert) = proposal.data.upgrade_certificate {
749+
if let Some(upgrade_cert) = consensus.saved_upgrade_certs.get(&leaf.get_view_number()) {
744750
info!("Updating consensus state with decided upgrade certificate: {:?}", upgrade_cert);
745751
self.decided_upgrade_cert = Some(upgrade_cert.clone());
746752
}

task-impls/src/upgrade.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>, A: ConsensusApi<TYPES, I> +
205205
}
206206
}
207207

208-
/// task state implementation for DA Task
208+
/// task state implementation for the upgrade task
209209
impl<TYPES: NodeType, I: NodeImplementation<TYPES>, A: ConsensusApi<TYPES, I> + 'static> TaskState
210210
for UpgradeTaskState<TYPES, I, A>
211211
{

task/src/task.rs

+5
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ impl<S: TaskState + Send + 'static> Task<S> {
166166
pub fn state_mut(&mut self) -> &mut S {
167167
&mut self.state
168168
}
169+
/// Get an immutable reference to this tasks state
170+
pub fn state(&self) -> &S {
171+
&self.state
172+
}
173+
169174
/// Spawn a new task adn register it. It will get all events not seend
170175
/// by the task creating it.
171176
pub async fn run_sub_task(&self, state: S) {

testing/src/lib.rs

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ pub mod spinning_task;
3333
/// task for checking if view sync got activated
3434
pub mod view_sync_task;
3535

36+
/// predicates to use in tests
37+
pub mod predicates;
38+
39+
/// scripting harness for tests
40+
pub mod script;
41+
42+
/// view generator for tests
43+
pub mod view_generator;
44+
3645
/// global event at the test level
3746
#[derive(Clone, Debug)]
3847
pub enum GlobalTestEvent {

testing/src/predicates.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use hotshot_task_impls::{
2+
consensus::ConsensusTaskState, events::HotShotEvent, events::HotShotEvent::*,
3+
};
4+
use hotshot_types::traits::node_implementation::NodeType;
5+
6+
use hotshot::types::SystemContextHandle;
7+
8+
use hotshot_example_types::node_types::{MemoryImpl, TestTypes};
9+
10+
pub struct Predicate<INPUT> {
11+
pub function: Box<dyn Fn(&INPUT) -> bool>,
12+
pub info: String,
13+
}
14+
15+
pub fn exact<TYPES>(event: HotShotEvent<TYPES>) -> Predicate<HotShotEvent<TYPES>>
16+
where
17+
TYPES: NodeType,
18+
{
19+
let info = format!("{:?}", event);
20+
21+
Predicate {
22+
function: Box::new(move |e| e == &event),
23+
info,
24+
}
25+
}
26+
27+
pub fn leaf_decided<TYPES>() -> Predicate<HotShotEvent<TYPES>>
28+
where
29+
TYPES: NodeType,
30+
{
31+
let info = "LeafDecided".to_string();
32+
let function = |e: &_| matches!(e, LeafDecided(_));
33+
34+
Predicate {
35+
function: Box::new(function),
36+
info,
37+
}
38+
}
39+
40+
pub fn quorum_vote_send<TYPES>() -> Predicate<HotShotEvent<TYPES>>
41+
where
42+
TYPES: NodeType,
43+
{
44+
let info = "QuorumVoteSend".to_string();
45+
let function = |e: &_| matches!(e, QuorumVoteSend(_));
46+
47+
Predicate {
48+
function: Box::new(function),
49+
info,
50+
}
51+
}
52+
53+
type ConsensusTaskTestState =
54+
ConsensusTaskState<TestTypes, MemoryImpl, SystemContextHandle<TestTypes, MemoryImpl>>;
55+
56+
pub fn consensus_predicate(
57+
function: Box<dyn for<'a> Fn(&'a ConsensusTaskTestState) -> bool>,
58+
info: &str,
59+
) -> Predicate<ConsensusTaskTestState> {
60+
Predicate {
61+
function,
62+
info: info.to_string(),
63+
}
64+
}
65+
66+
pub fn no_decided_upgrade_cert() -> Predicate<ConsensusTaskTestState> {
67+
consensus_predicate(
68+
Box::new(|state| state.decided_upgrade_cert.is_none()),
69+
"expected decided_upgrade_cert to be None",
70+
)
71+
}
72+
73+
pub fn decided_upgrade_cert() -> Predicate<ConsensusTaskTestState> {
74+
consensus_predicate(
75+
Box::new(|state| state.decided_upgrade_cert.is_some()),
76+
"expected decided_upgrade_cert to be Some(_)",
77+
)
78+
}

testing/src/script.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::predicates::Predicate;
2+
use async_broadcast::broadcast;
3+
use hotshot_task_impls::events::HotShotEvent;
4+
5+
use hotshot_task::task::{Task, TaskRegistry, TaskState};
6+
use hotshot_types::traits::node_implementation::NodeType;
7+
use std::sync::Arc;
8+
9+
/// A `TestScript` is a sequence of triples (input sequence, output sequence, assertions).
10+
type TestScript<TYPES, S> = Vec<TestScriptStage<TYPES, S>>;
11+
12+
pub struct TestScriptStage<TYPES: NodeType, S: TaskState<Event = HotShotEvent<TYPES>>> {
13+
pub inputs: Vec<HotShotEvent<TYPES>>,
14+
pub outputs: Vec<Predicate<HotShotEvent<TYPES>>>,
15+
pub asserts: Vec<Predicate<S>>,
16+
}
17+
18+
impl<INPUT> std::fmt::Debug for Predicate<INPUT> {
19+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
20+
write!(f, "{}", self.info)
21+
}
22+
}
23+
24+
/// `run_test_script` reads a triple (inputs, outputs, asserts) in a `TestScript`,
25+
/// It broadcasts all given inputs (in order) and waits to receive all outputs (in order).
26+
/// Once the expected outputs have been received, it validates the task state at that stage
27+
/// against the given assertions.
28+
///
29+
/// If all assertions pass, it moves onto the next stage. If it receives an unexpected output
30+
/// or fails to receive an output, the test fails immediately with a panic.
31+
///
32+
/// Note: the task is not spawned with an async thread; instead, the harness just calls `handle_event`.
33+
/// This has a few implications, e.g. shutting down tasks doesn't really make sense,
34+
/// and event ordering is deterministic.
35+
pub async fn run_test_script<TYPES, S: TaskState<Event = HotShotEvent<TYPES>>>(
36+
mut script: TestScript<TYPES, S>,
37+
state: S,
38+
) where
39+
TYPES: NodeType,
40+
S: Send + 'static,
41+
{
42+
let registry = Arc::new(TaskRegistry::default());
43+
44+
let (test_input, task_receiver) = broadcast(1024);
45+
// let (task_input, mut test_receiver) = broadcast(1024);
46+
47+
let task_input = test_input.clone();
48+
let mut test_receiver = task_receiver.clone();
49+
50+
let mut task = Task::new(
51+
task_input.clone(),
52+
task_receiver.clone(),
53+
registry.clone(),
54+
state,
55+
);
56+
57+
for (stage_number, stage) in script.iter_mut().enumerate() {
58+
tracing::debug!("Beginning test stage {}", stage_number);
59+
for input in &mut *stage.inputs {
60+
if !task.state_mut().filter(input) {
61+
tracing::debug!("Test sent: {:?}", input);
62+
63+
if let Some(res) = S::handle_event(input.clone(), &mut task).await {
64+
task.state_mut().handle_result(&res).await;
65+
}
66+
}
67+
}
68+
69+
for expected in &stage.outputs {
70+
let output_missing_error = format!(
71+
"Stage {} | Failed to receive output for predicate: {:?}",
72+
stage_number, expected
73+
);
74+
75+
if let Ok(received_output) = test_receiver.try_recv() {
76+
tracing::debug!("Test received: {:?}", received_output);
77+
assert!(
78+
(expected.function)(&received_output),
79+
"Stage {} | Output failed to satisfy {:?}",
80+
stage_number,
81+
expected
82+
);
83+
} else {
84+
panic!("{}", output_missing_error);
85+
}
86+
}
87+
88+
for assert in &stage.asserts {
89+
assert!(
90+
(assert.function)(task.state()),
91+
"Stage {} | Assertion on task state failed: {:?}",
92+
stage_number,
93+
assert
94+
);
95+
}
96+
97+
if let Ok(received_output) = test_receiver.try_recv() {
98+
let extra_output_error = format!(
99+
"Stage {} | Received unexpected additional output: {:?}",
100+
stage_number, received_output
101+
);
102+
103+
panic!("{}", extra_output_error);
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)