Skip to content

Commit e198925

Browse files
mootz12dmkozh
andauthored
feat: add try fn for with test contract frame (#1628)
### What Add a function that returns errors when executing a closure for a test contract frame ### Why stellar/rs-soroban-sdk#1434 ### Known limitations None --------- Co-authored-by: Dmytro Kozhevin <dmytro@stellar.org>
1 parent 248e424 commit e198925

8 files changed

+274
-38
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
" 0 begin": "cpu:14488, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
3+
" 1 call vec_new()": "cpu:14930, mem:80, objs:-/1@6e27cef",
4+
" 2 ret vec_new -> Ok(Vec(obj#3))": "cpu:15433, mem:160, objs:-/2@19dff6ca",
5+
" 3 call symbol_new_from_slice(13)": "cpu:36979, mem:2038, objs:-/3@52e71cc, store:-/2@7bc024e1, foot:2@d6be0e71",
6+
" 4 ret symbol_new_from_slice -> Ok(Symbol(obj#7))": "cpu:37989, mem:2147, objs:-/4@d72f5dc1",
7+
" 5 push TEST: 0:sym#7()": "cpu:48533, mem:3291, objs:-/5@af7c85bb, stk:1@8e5c3d6a, auth:1@8b9c58a5/-",
8+
" 6 call symbol_new_from_slice(13)": "",
9+
" 7 ret symbol_new_from_slice -> Ok(Symbol(obj#11))": "cpu:49543, mem:3400, objs:-/6@6c5fdc1a",
10+
" 8 call obj_cmp(Symbol(obj#11), Symbol(obj#7))": "",
11+
" 9 ret obj_cmp -> Ok(0)": "cpu:49832",
12+
" 10 pop TEST: 0:sym#7 -> Ok(Void)": "",
13+
" 11 push TEST: 0:test()": "cpu:54601, mem:4360, objs:-/7@e5e0c6fc, stk:1@6c0a26dc, auth:1@ff08e997/-",
14+
" 12 call symbol_new_from_slice(11)": "",
15+
" 13 ret symbol_new_from_slice -> Ok(Symbol(obj#15))": "cpu:55611, mem:4467, objs:-/8@75aa5756",
16+
" 14 call get_contract_data(Symbol(obj#15), Persistent)": "",
17+
" 15 ret get_contract_data -> Err(Error(Storage, MissingValue))": "cpu:59507, mem:4941, store:-/3@d3f11b10, foot:3@a65ae30e",
18+
" 16 pop TEST: 0:test -> Err(Error(Storage, MissingValue))": "",
19+
" 17 end": "cpu:59507, mem:4941, prngs:-/9b4a753, objs:-/8@75aa5756, vm:-/-, evt:-, store:-/2@7bc024e1, foot:3@a65ae30e, stk:-, auth:-/-"
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
" 0 begin": "cpu:14488, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
3+
" 1 call vec_new()": "cpu:14930, mem:80, objs:-/1@6e27cef",
4+
" 2 ret vec_new -> Ok(Vec(obj#3))": "cpu:15433, mem:160, objs:-/2@19dff6ca",
5+
" 3 call symbol_new_from_slice(13)": "cpu:36979, mem:2038, objs:-/3@52e71cc, store:-/2@7bc024e1, foot:2@d6be0e71",
6+
" 4 ret symbol_new_from_slice -> Ok(Symbol(obj#7))": "cpu:37989, mem:2147, objs:-/4@d72f5dc1",
7+
" 5 push TEST: 0:sym#7()": "cpu:48533, mem:3291, objs:-/5@af7c85bb, stk:1@8e5c3d6a, auth:1@8b9c58a5/-",
8+
" 6 call symbol_new_from_slice(13)": "",
9+
" 7 ret symbol_new_from_slice -> Ok(Symbol(obj#11))": "cpu:49543, mem:3400, objs:-/6@6c5fdc1a",
10+
" 8 call obj_cmp(Symbol(obj#11), Symbol(obj#7))": "",
11+
" 9 ret obj_cmp -> Ok(0)": "cpu:49832",
12+
" 10 pop TEST: 0:sym#7 -> Ok(Void)": "",
13+
" 11 push TEST: 0:test()": "cpu:54601, mem:4360, objs:-/7@e5e0c6fc, stk:1@6c0a26dc, auth:1@ff08e997/-",
14+
" 12 pop TEST: 0:test -> Err(Error(WasmVm, InvalidAction))": "",
15+
" 13 end": "cpu:54601, mem:4360, prngs:-/9b4a753, objs:-/7@e5e0c6fc, vm:-/-, evt:-, store:-/2@7bc024e1, foot:2@d6be0e71, stk:-, auth:-/-"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
" 0 begin": "cpu:14488, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
3+
" 1 call vec_new()": "cpu:14930, mem:80, objs:-/1@6e27cef",
4+
" 2 ret vec_new -> Ok(Vec(obj#3))": "cpu:15433, mem:160, objs:-/2@19dff6ca",
5+
" 3 call symbol_new_from_slice(13)": "cpu:36979, mem:2038, objs:-/3@52e71cc, store:-/2@7bc024e1, foot:2@d6be0e71",
6+
" 4 ret symbol_new_from_slice -> Ok(Symbol(obj#7))": "cpu:37989, mem:2147, objs:-/4@d72f5dc1",
7+
" 5 push TEST: 0:sym#7()": "cpu:48533, mem:3291, objs:-/5@af7c85bb, stk:1@8e5c3d6a, auth:1@8b9c58a5/-",
8+
" 6 call symbol_new_from_slice(13)": "",
9+
" 7 ret symbol_new_from_slice -> Ok(Symbol(obj#11))": "cpu:49543, mem:3400, objs:-/6@6c5fdc1a",
10+
" 8 call obj_cmp(Symbol(obj#11), Symbol(obj#7))": "",
11+
" 9 ret obj_cmp -> Ok(0)": "cpu:49832",
12+
" 10 pop TEST: 0:sym#7 -> Ok(Void)": "",
13+
" 11 push TEST: 0:test()": "cpu:54601, mem:4360, objs:-/7@e5e0c6fc, stk:1@6c0a26dc, auth:1@ff08e997/-",
14+
" 12 pop TEST: 0:test -> Ok(Void)": "",
15+
" 13 end": "cpu:54601, mem:4360, prngs:-/9b4a753, objs:-/7@e5e0c6fc, vm:-/-, evt:-, store:-/2@7bc024e1, foot:2@d6be0e71, stk:-, auth:-/-"
16+
}

soroban-env-host/observations/25/test__lifecycle__test_contract_wasm_update.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@
8282
" 80 ret update_current_contract_wasm -> Ok(Void)": "cpu:7091002, mem:9026990, objs:1@8099ea12/34@d3d5f07b, evt:2@951fccd4, store:1@f369ddeb/3@63e40642",
8383
" 81 pop VM:7013ae53:update -> Ok(I32(123))": "cpu:7095920, mem:9027762, vm:1048576@740599e7/6@c559546a, store:1@f369ddeb/3@d5018297",
8484
" 82 ret call -> Ok(I32(123))": " objs:-/34@d3d5f07b, vm:-/-, store:-/3@d5018297, stk:-, auth:-/-",
85-
" 83 push TEST:ba863dea:()": "cpu:7101804, mem:9028953, objs:-/35@3aed3f2c, stk:1@c72009c2, auth:1@2a4a1617/-",
85+
" 83 push TEST:ba863dea:check_foo()": "cpu:7101804, mem:9028953, objs:-/35@3aed3f2c, stk:1@b527bc58, auth:1@72792528/-",
8686
" 84 call get_contract_data(Symbol(foo), Instance)": "",
87-
" 85 ret get_contract_data -> Ok(I32(111))": "cpu:7102475, mem:9029009, store:1@f369ddeb/3@d5018297, stk:1@fed36e41",
88-
" 86 pop TEST:ba863dea: -> Ok(Void)": "",
87+
" 85 ret get_contract_data -> Ok(I32(111))": "cpu:7102475, mem:9029009, store:1@f369ddeb/3@d5018297, stk:1@cf598e77",
88+
" 86 pop TEST:ba863dea:check_foo -> Ok(Void)": "",
8989
" 87 call vec_new_from_slice(2)": "cpu:7112562, mem:9030691, store:-/3@d5018297, stk:-, auth:-/-",
9090
" 88 ret vec_new_from_slice -> Ok(Vec(obj#71))": "cpu:7113529, mem:9030803, objs:-/36@7643ae9e",
9191
" 89 call call(Address(obj#17), Symbol(add), Vec(obj#71))": "",

soroban-env-host/src/host/frame.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,14 +648,92 @@ impl Host {
648648
F: FnOnce() -> Result<Val, HostError>,
649649
{
650650
let _invocation_meter_scope = self.maybe_meter_invocation(
651-
crate::host::invocation_metering::MeteringInvocation::CreateContractEntryPoint,
651+
crate::host::invocation_metering::MeteringInvocation::contract_invocation(
652+
self, &id, func,
653+
),
652654
);
653655
self.with_frame(
654656
Frame::TestContract(self.create_test_contract_frame(id, func, vec![])?),
655657
f,
656658
)
657659
}
658660

661+
/// Pushes a test contract [`Frame`], runs a closure, and then pops the
662+
/// frame, rolling back if the closure returned an error. Returns the result
663+
/// that the closure returned (or any error that occurred during the closure
664+
/// or the frame push/pop). Used for testing.
665+
#[cfg(any(test, feature = "testutils"))]
666+
pub fn try_with_test_contract_frame<F>(
667+
&self,
668+
id: ContractId,
669+
func: Symbol,
670+
f: F,
671+
) -> Result<Val, HostError>
672+
where
673+
F: FnOnce() -> Result<Val, HostError>,
674+
{
675+
let _invocation_meter_scope = self.maybe_meter_invocation(
676+
crate::host::invocation_metering::MeteringInvocation::contract_invocation(
677+
self, &id, func,
678+
),
679+
);
680+
681+
// Code taken from `call_n_internal` to handle panics inside the closure `f`.
682+
// Modified to run as a closure within a test contract frame instead of invoking
683+
// a contract function.
684+
let frame = self.create_test_contract_frame(id.clone(), func, vec![])?;
685+
let panic = frame.panic.clone();
686+
self.with_frame(Frame::TestContract(frame), || {
687+
use std::any::Any;
688+
use std::panic::AssertUnwindSafe;
689+
type PanicVal = Box<dyn Any + Send>;
690+
691+
let closure = AssertUnwindSafe(move || f());
692+
let res: Result<Result<Val, HostError>, PanicVal> =
693+
crate::testutils::call_with_suppressed_panic_hook(closure);
694+
match res {
695+
Ok(res) => res,
696+
Err(panic_payload) => {
697+
let mut error: Error =
698+
Error::from(wasmi::core::TrapCode::UnreachableCodeReached);
699+
700+
let mut recovered_error_from_panic_refcell = false;
701+
if let Ok(panic) = panic.try_borrow() {
702+
if let Some(err) = *panic {
703+
recovered_error_from_panic_refcell = true;
704+
error = err;
705+
}
706+
}
707+
708+
if !recovered_error_from_panic_refcell {
709+
self.with_debug_mode(|| {
710+
// only include func in log if a non-empty name is provided
711+
let func_str = match format!("{:?}", func).as_str() {
712+
"Symbol()" => String::new(),
713+
formatted => format!(" with fn name '{}'", formatted),
714+
};
715+
if let Some(str) = panic_payload.downcast_ref::<&str>() {
716+
let msg: String = format!(
717+
"caught panic '{}' from test contract frame{}",
718+
str, func_str
719+
);
720+
let _ = self.log_diagnostics(&msg, &[]);
721+
} else if let Some(str) = panic_payload.downcast_ref::<String>() {
722+
let msg: String = format!(
723+
"caught panic '{}' from test contract frame{}",
724+
str, func_str
725+
);
726+
let _ = self.log_diagnostics(&msg, &[]);
727+
};
728+
Ok(())
729+
})
730+
}
731+
Err(self.error(error, "caught error from test contract frame", &[]))
732+
}
733+
}
734+
})
735+
}
736+
659737
#[cfg(any(test, feature = "testutils"))]
660738
fn create_test_contract_frame(
661739
&self,

soroban-env-host/src/test/frame.rs

Lines changed: 127 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1+
use soroban_env_common::{Env, StorageType};
2+
13
use crate::{
24
host::HostError,
3-
xdr::{ContractId, Hash, ScAddress},
4-
Compare, ContractFunctionSet, EnvBase, Host, Symbol, Val,
5+
xdr::{ContractId, Hash, ScAddress, ScErrorCode, ScErrorType},
6+
Compare, ContractFunctionSet, EnvBase, Error, Host, Symbol, Val,
57
};
68

79
use std::rc::Rc;
810

9-
#[test]
10-
fn has_frame() -> Result<(), HostError> {
11-
struct NoopContractFunctionSet;
12-
impl ContractFunctionSet for NoopContractFunctionSet {
13-
fn call(&self, func: &Symbol, host: &Host, _args: &[Val]) -> Option<Val> {
14-
if host
15-
.compare(
16-
&host.symbol_new_from_slice(b"__constructor").unwrap().into(),
17-
func,
18-
)
19-
.unwrap()
20-
.is_ne()
21-
{
22-
None
23-
} else {
24-
Some(().into())
25-
}
11+
struct NoopContractFunctionSet;
12+
impl ContractFunctionSet for NoopContractFunctionSet {
13+
fn call(&self, func: &Symbol, host: &Host, _args: &[Val]) -> Option<Val> {
14+
if host
15+
.compare(
16+
&host.symbol_new_from_slice(b"__constructor").unwrap().into(),
17+
func,
18+
)
19+
.unwrap()
20+
.is_ne()
21+
{
22+
None
23+
} else {
24+
Some(().into())
2625
}
2726
}
27+
}
2828

29+
#[test]
30+
fn has_frame() -> Result<(), HostError> {
2931
let host = observe_host!(Host::test_host_with_recording_footprint());
3032

3133
// Host has no frame outside of executing a contract.
@@ -35,14 +37,115 @@ fn has_frame() -> Result<(), HostError> {
3537
let id = [0u8; 32];
3638
let address = host.add_host_object(ScAddress::Contract(ContractId(Hash(id))))?;
3739
host.register_test_contract(address, Rc::new(NoopContractFunctionSet))?;
38-
let func = Symbol::try_from_small_str("")?;
39-
host.with_test_contract_frame(ContractId(Hash(id)), func, || {
40-
assert!(host.has_frame()?);
41-
Ok(().into())
42-
})?;
40+
host.with_test_contract_frame(
41+
ContractId(Hash(id)),
42+
Symbol::try_from_small_str("").unwrap(),
43+
|| {
44+
assert!(host.has_frame()?);
45+
Ok(().into())
46+
},
47+
)?;
48+
// Host has no frame outside of executing a contract.
49+
assert!(!host.has_frame()?);
50+
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn try_with_test_contract_frame_has_frame() -> Result<(), HostError> {
56+
let host = observe_host!(Host::test_host_with_recording_footprint());
4357

4458
// Host has no frame outside of executing a contract.
4559
assert!(!host.has_frame()?);
4660

61+
// Host has a frame when executing a contract.
62+
let id = [0u8; 32];
63+
let address = host.add_host_object(ScAddress::Contract(ContractId(Hash(id))))?;
64+
host.register_test_contract(address, Rc::new(NoopContractFunctionSet))?;
65+
host.try_with_test_contract_frame(
66+
ContractId(Hash(id)),
67+
Symbol::try_from_small_str("test").unwrap(),
68+
|| {
69+
assert!(host.has_frame()?);
70+
Ok(().into())
71+
},
72+
)?;
73+
74+
// Host has no frame outside of executing a contract.
75+
assert!(!host.has_frame()?);
76+
77+
Ok(())
78+
}
79+
80+
#[test]
81+
fn try_with_test_contract_frame_catches_host_error() -> Result<(), HostError> {
82+
// Setup host and contract
83+
let host = observe_host!(Host::test_host_with_recording_footprint());
84+
let id = [0u8; 32];
85+
let address = host.add_host_object(ScAddress::Contract(ContractId(Hash(id))))?;
86+
host.register_test_contract(address, Rc::new(NoopContractFunctionSet))?;
87+
88+
// Cause a HostError inside the contract frame
89+
let result = host.try_with_test_contract_frame(
90+
ContractId(Hash(id)),
91+
Symbol::try_from_small_str("test").unwrap(),
92+
|| {
93+
let key = host.symbol_new_from_slice(b"nonexistent")?;
94+
host.get_contract_data(key.as_val().clone(), StorageType::Persistent)?;
95+
Ok(().into())
96+
},
97+
);
98+
assert!(result.is_err());
99+
assert_eq!(
100+
result.unwrap_err(),
101+
HostError::from(Error::from_type_and_code(
102+
ScErrorType::Storage,
103+
ScErrorCode::MissingValue
104+
))
105+
);
106+
107+
Ok(())
108+
}
109+
110+
#[test]
111+
fn try_with_test_contract_frame_catches_panic() -> Result<(), HostError> {
112+
// Setup host and contract
113+
let host = observe_host!(Host::test_host_with_recording_footprint());
114+
host.set_diagnostic_level(crate::DiagnosticLevel::Debug)?;
115+
let id = [0u8; 32];
116+
let address = host.add_host_object(ScAddress::Contract(ContractId(Hash(id))))?;
117+
host.register_test_contract(address, Rc::new(NoopContractFunctionSet))?;
118+
119+
// Cause a panic inside the contract frame
120+
let panic_str = "intentional panic for testing";
121+
let result = host.try_with_test_contract_frame(
122+
ContractId(Hash(id)),
123+
Symbol::try_from_small_str("test").unwrap(),
124+
|| {
125+
panic!("{panic_str}");
126+
},
127+
);
128+
assert!(result.is_err());
129+
130+
// Validate that a diagnostic event was logged with the contents of the panic
131+
let events = host.get_events()?.0;
132+
let mut has_diagnostic_event = false;
133+
for (_, event) in events.iter().enumerate() {
134+
let as_str = format!("{:?}", event);
135+
if as_str.contains(panic_str) {
136+
has_diagnostic_event = true;
137+
break;
138+
}
139+
}
140+
assert!(has_diagnostic_event);
141+
142+
assert_eq!(
143+
result.unwrap_err(),
144+
HostError::from(Error::from_type_and_code(
145+
ScErrorType::WasmVm,
146+
ScErrorCode::InvalidAction
147+
))
148+
);
149+
47150
Ok(())
48151
}

soroban-env-host/src/test/lifecycle.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,11 @@ fn test_contract_wasm_update() {
431431
// Make sure execution continued after the update and we've got the function
432432
// return value.
433433
assert_eq!(res, 123);
434+
434435
// Verify that instance storage has been updated as well.
435436
host.with_test_contract_frame(
436437
host.contract_id_from_address(contract_addr_obj).unwrap(),
437-
Symbol::try_from_small_str("").unwrap(),
438+
Symbol::try_from_small_str("check_foo").unwrap(),
438439
|| {
439440
let stored_value: i32 = host
440441
.get_contract_data(

soroban-env-host/src/testutils.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -355,16 +355,18 @@ impl Host {
355355
panic!()
356356
};
357357

358-
let test = SymbolSmall::try_from_str("test").unwrap();
359-
360358
// First step: insert all the data values in question into the storage map.
361-
host.with_test_contract_frame(contract_hash.clone(), test.into(), || {
362-
for (k, (t, _)) in data_keys.iter() {
363-
let v = host.to_host_val(k).unwrap();
364-
host.put_contract_data(v, v, *t).unwrap();
365-
}
366-
Ok(Val::VOID.into())
367-
})
359+
host.with_test_contract_frame(
360+
contract_hash.clone(),
361+
SymbolSmall::try_from_str("test").unwrap().into(),
362+
|| {
363+
for (k, (t, _)) in data_keys.iter() {
364+
let v = host.to_host_val(k).unwrap();
365+
host.put_contract_data(v, v, *t).unwrap();
366+
}
367+
Ok(Val::VOID.into())
368+
},
369+
)
368370
.unwrap();
369371

370372
// Second step: generate some accounts to sign things with.

0 commit comments

Comments
 (0)