Skip to content

Commit 56177ab

Browse files
sanityclaude
andauthored
refactor(contract): improve testability and error handling (#2258)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent eda31bb commit 56177ab

File tree

4 files changed

+715
-95
lines changed

4 files changed

+715
-95
lines changed

crates/core/src/contract/executor.rs

Lines changed: 168 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ use crate::{
3939
operations::{self, Operation},
4040
};
4141

42+
pub(super) mod init_tracker;
4243
pub(super) mod mock_runtime;
4344
pub(super) mod runtime;
4445

46+
pub(crate) use init_tracker::{
47+
ContractInitTracker, InitCheckResult, SLOW_INIT_THRESHOLD, STALE_INIT_THRESHOLD,
48+
};
49+
4550
#[derive(Debug)]
4651
pub struct ExecutorError {
4752
inner: Either<Box<RequestError>, anyhow::Error>,
@@ -521,30 +526,6 @@ pub(crate) trait ContractExecutor: Send + 'static {
521526
fn get_subscription_info(&self) -> Vec<crate::message::SubscriptionInfo>;
522527
}
523528

524-
/// A WASM executor which will run any contracts, delegates, etc. registered.
525-
///
526-
/// This executor will monitor the store directories and databases to detect state changes.
527-
/// Represents an operation that's waiting for a contract to finish initializing
528-
#[derive(Debug)]
529-
struct QueuedOperation {
530-
update: Either<WrappedState, StateDelta<'static>>,
531-
related_contracts: RelatedContracts<'static>,
532-
/// When this operation was queued
533-
queued_at: std::time::Instant,
534-
}
535-
536-
/// Tracks the initialization state of a contract
537-
#[derive(Debug)]
538-
enum ContractInitState {
539-
/// Contract is currently being initialized (validation in progress)
540-
Initializing {
541-
/// Operations waiting for initialization to complete
542-
queued_ops: Vec<QueuedOperation>,
543-
/// When initialization started
544-
started_at: std::time::Instant,
545-
},
546-
}
547-
548529
/// Consumers of the executor are required to poll for new changes in order to be notified
549530
/// of changes or can alternatively use the notification channel.
550531
pub struct Executor<R = Runtime> {
@@ -558,7 +539,7 @@ pub struct Executor<R = Runtime> {
558539
/// Attested contract instances for a given delegate.
559540
delegate_attested_ids: HashMap<DelegateKey, Vec<ContractInstanceId>>,
560541
/// Tracks contracts that are being initialized and operations queued for them
561-
contract_init_state: HashMap<ContractKey, ContractInitState>,
542+
init_tracker: ContractInitTracker,
562543

563544
event_loop_channel: Option<ExecutorToEventLoopChannel<ExecutorHalve>>,
564545
}
@@ -580,7 +561,7 @@ impl<R> Executor<R> {
580561
update_notifications: HashMap::default(),
581562
subscriber_summaries: HashMap::default(),
582563
delegate_attested_ids: HashMap::default(),
583-
contract_init_state: HashMap::default(),
564+
init_tracker: ContractInitTracker::new(),
584565
event_loop_channel,
585566
})
586567
}
@@ -668,3 +649,164 @@ impl<R> Executor<R> {
668649
subscriptions
669650
}
670651
}
652+
653+
/// Test fixtures for creating contract-related test data.
654+
///
655+
/// These helpers make it easier to write unit tests for contract module code
656+
/// by providing convenient constructors for common types.
657+
#[cfg(test)]
658+
pub(crate) mod test_fixtures {
659+
use freenet_stdlib::prelude::*;
660+
661+
/// Create a test contract key with arbitrary but consistent data
662+
pub fn make_contract_key() -> ContractKey {
663+
let code = ContractCode::from(vec![1, 2, 3, 4, 5, 6, 7, 8]);
664+
let params = Parameters::from(vec![10, 20, 30, 40]);
665+
ContractKey::from_params_and_code(&params, &code)
666+
}
667+
668+
/// Create a test contract key with custom code bytes
669+
pub fn make_contract_key_with_code(code_bytes: &[u8]) -> ContractKey {
670+
let code = ContractCode::from(code_bytes.to_vec());
671+
let params = Parameters::from(vec![10, 20, 30, 40]);
672+
ContractKey::from_params_and_code(&params, &code)
673+
}
674+
675+
/// Create a test wrapped state from raw bytes
676+
pub fn make_state(data: &[u8]) -> WrappedState {
677+
WrappedState::new(data.to_vec())
678+
}
679+
680+
/// Create test parameters from raw bytes
681+
pub fn make_params(data: &[u8]) -> Parameters<'static> {
682+
Parameters::from(data.to_vec())
683+
}
684+
685+
/// Create a test state delta from raw bytes
686+
pub fn make_delta(data: &[u8]) -> StateDelta<'static> {
687+
StateDelta::from(data.to_vec())
688+
}
689+
}
690+
691+
#[cfg(test)]
692+
mod tests {
693+
use super::*;
694+
695+
mod executor_error_tests {
696+
use super::*;
697+
698+
#[test]
699+
fn test_executor_error_other_is_not_request() {
700+
let err = ExecutorError::other(anyhow::anyhow!("some error"));
701+
assert!(!err.is_request());
702+
assert!(!err.is_fatal());
703+
}
704+
705+
#[test]
706+
fn test_executor_error_request_is_request() {
707+
let err = ExecutorError::request(StdContractError::Put {
708+
key: test_fixtures::make_contract_key(),
709+
cause: "test".into(),
710+
});
711+
assert!(err.is_request());
712+
assert!(!err.is_fatal());
713+
}
714+
715+
#[test]
716+
fn test_executor_error_internal_error() {
717+
let err = ExecutorError::internal_error();
718+
assert!(!err.is_request());
719+
assert!(!err.is_fatal());
720+
assert!(err.to_string().contains("internal error"));
721+
}
722+
723+
#[test]
724+
fn test_executor_error_display_left() {
725+
let err = ExecutorError::request(StdContractError::Put {
726+
key: test_fixtures::make_contract_key(),
727+
cause: "test cause".into(),
728+
});
729+
let display = err.to_string();
730+
assert!(display.contains("test cause") || display.contains("Put"));
731+
}
732+
733+
#[test]
734+
fn test_executor_error_display_right() {
735+
let err = ExecutorError::other(anyhow::anyhow!("custom error message"));
736+
assert!(err.to_string().contains("custom error message"));
737+
}
738+
739+
#[test]
740+
fn test_executor_error_from_request_error() {
741+
let request_err = RequestError::ContractError(StdContractError::Put {
742+
key: test_fixtures::make_contract_key(),
743+
cause: "from conversion".into(),
744+
});
745+
let err: ExecutorError = request_err.into();
746+
assert!(err.is_request());
747+
}
748+
749+
#[test]
750+
fn test_executor_error_from_boxed_request_error() {
751+
let request_err = Box::new(RequestError::ContractError(StdContractError::Put {
752+
key: test_fixtures::make_contract_key(),
753+
cause: "boxed".into(),
754+
}));
755+
let err: ExecutorError = request_err.into();
756+
assert!(err.is_request());
757+
}
758+
759+
#[test]
760+
fn test_unwrap_request_succeeds_for_request_error() {
761+
let key = test_fixtures::make_contract_key();
762+
let err = ExecutorError::request(StdContractError::Put {
763+
key,
764+
cause: "unwrap test".into(),
765+
});
766+
let _unwrapped = err.unwrap_request(); // Should not panic
767+
}
768+
769+
#[test]
770+
#[should_panic]
771+
fn test_unwrap_request_panics_for_other_error() {
772+
let err = ExecutorError::other(anyhow::anyhow!("not a request"));
773+
let _unwrapped = err.unwrap_request(); // Should panic
774+
}
775+
}
776+
777+
mod test_fixtures_tests {
778+
use super::*;
779+
780+
#[test]
781+
fn test_make_contract_key_is_consistent() {
782+
let key1 = test_fixtures::make_contract_key();
783+
let key2 = test_fixtures::make_contract_key();
784+
assert_eq!(key1, key2);
785+
}
786+
787+
#[test]
788+
fn test_make_contract_key_with_different_code() {
789+
let key1 = test_fixtures::make_contract_key_with_code(&[1, 2, 3]);
790+
let key2 = test_fixtures::make_contract_key_with_code(&[4, 5, 6]);
791+
assert_ne!(key1, key2);
792+
}
793+
794+
#[test]
795+
fn test_make_state() {
796+
let state = test_fixtures::make_state(&[1, 2, 3, 4]);
797+
assert_eq!(state.as_ref(), &[1, 2, 3, 4]);
798+
}
799+
800+
#[test]
801+
fn test_make_params() {
802+
let params = test_fixtures::make_params(&[10, 20]);
803+
assert_eq!(params.as_ref(), &[10, 20]);
804+
}
805+
806+
#[test]
807+
fn test_make_delta() {
808+
let delta = test_fixtures::make_delta(&[100, 200]);
809+
assert_eq!(delta.as_ref(), &[100, 200]);
810+
}
811+
}
812+
}

0 commit comments

Comments
 (0)