Skip to content

Commit af49990

Browse files
authored
feat: add try_as_contract test function (#1662)
### What Adds a `try_as_contract` fn that gracefully returns errors like `try_invoke_contract`. ### Why Fixes #1434 ### Known limitations - [x] Marked as draft until the necessary env change is made: stellar/rs-soroban-env#1628
1 parent 9b46efb commit af49990

15 files changed

+1384
-4
lines changed

soroban-sdk/src/env.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,45 @@ impl Env {
15421542
///
15431543
/// Used to write or read contract data, or take other actions in tests for
15441544
/// setting up tests or asserting on internal state.
1545+
///
1546+
/// ### Examples
1547+
/// ```
1548+
/// use soroban_sdk::{contract, contractimpl, Env, Symbol};
1549+
///
1550+
/// #[contract]
1551+
/// pub struct HelloContract;
1552+
///
1553+
/// #[contractimpl]
1554+
/// impl HelloContract {
1555+
/// pub fn set_storage(env: Env, key: Symbol, val: Symbol) {
1556+
/// env.storage().persistent().set(&key, &val);
1557+
/// }
1558+
/// }
1559+
///
1560+
/// #[test]
1561+
/// fn test() {
1562+
/// # }
1563+
/// # fn main() {
1564+
/// let env = Env::default();
1565+
/// let contract_id = env.register(HelloContract, ());
1566+
/// let client = HelloContractClient::new(&env, &contract_id);
1567+
///
1568+
/// let key = Symbol::new(&env, "foo");
1569+
/// let val = Symbol::new(&env, "bar");
1570+
///
1571+
/// // Set storage using the contract
1572+
/// client.set_storage(&key, &val);
1573+
///
1574+
/// // Successfully read the storage key
1575+
/// let result = env.as_contract(&contract_id, || {
1576+
/// env.storage()
1577+
/// .persistent()
1578+
/// .get::<Symbol, Symbol>(&key)
1579+
/// .unwrap()
1580+
/// });
1581+
/// assert_eq!(result, val);
1582+
/// }
1583+
/// ```
15451584
pub fn as_contract<T>(&self, id: &Address, f: impl FnOnce() -> T) -> T {
15461585
let id = id.contract_id();
15471586
let func = Symbol::from_small_str("");
@@ -1555,6 +1594,86 @@ impl Env {
15551594
t.unwrap()
15561595
}
15571596

1597+
/// Run the function as if executed by the given contract ID. Returns an
1598+
/// error if the function execution fails for any reason.
1599+
///
1600+
/// Used to write or read contract data, or take other actions in tests for
1601+
/// setting up tests or asserting on internal state.
1602+
///
1603+
/// ### Examples
1604+
/// ```
1605+
/// use soroban_sdk::{contract, contractimpl, xdr::{ScErrorCode, ScErrorType}, Env, Error, Symbol};
1606+
///
1607+
/// #[contract]
1608+
/// pub struct HelloContract;
1609+
///
1610+
/// #[contractimpl]
1611+
/// impl HelloContract {
1612+
/// pub fn set_storage(env: Env, key: Symbol, val: Symbol) {
1613+
/// env.storage().persistent().set(&key, &val);
1614+
/// }
1615+
/// }
1616+
///
1617+
/// #[test]
1618+
/// fn test() {
1619+
/// # }
1620+
/// # fn main() {
1621+
/// let env = Env::default();
1622+
/// let contract_id = env.register(HelloContract, ());
1623+
/// let client = HelloContractClient::new(&env, &contract_id);
1624+
///
1625+
/// let key = Symbol::new(&env, "foo");
1626+
/// let val = Symbol::new(&env, "bar");
1627+
///
1628+
/// // Set storage using the contract
1629+
/// client.set_storage(&key, &val);
1630+
///
1631+
/// // Successfully read the storage key
1632+
/// let result = env.try_as_contract::<Symbol, Error>(&contract_id, || {
1633+
/// env.storage()
1634+
/// .persistent()
1635+
/// .get::<Symbol, Symbol>(&key)
1636+
/// .unwrap()
1637+
/// });
1638+
/// assert_eq!(result, Ok(val));
1639+
///
1640+
/// // Attempting to extend TTL of a non-existent key throws an error
1641+
/// let new_key = Symbol::new(&env, "baz");
1642+
/// let result = env.try_as_contract(&contract_id, || {
1643+
/// env.storage().persistent().extend_ttl(&new_key, 1, 100);
1644+
/// });
1645+
/// assert_eq!(
1646+
/// result,
1647+
/// Err(Ok(Error::from_type_and_code(
1648+
/// ScErrorType::Storage,
1649+
/// ScErrorCode::MissingValue
1650+
/// )))
1651+
/// );
1652+
/// }
1653+
/// ```
1654+
pub fn try_as_contract<T, E>(
1655+
&self,
1656+
id: &Address,
1657+
f: impl FnOnce() -> T,
1658+
) -> Result<T, Result<E, InvokeError>>
1659+
where
1660+
E: TryFrom<Error>,
1661+
E::Error: Into<InvokeError>,
1662+
{
1663+
let id = id.contract_id();
1664+
let func = Symbol::from_small_str("");
1665+
let mut t: Option<T> = None;
1666+
let result = self.env_impl.try_with_test_contract_frame(id, func, || {
1667+
t = Some(f());
1668+
Ok(().into())
1669+
});
1670+
1671+
match result {
1672+
Ok(_) => Ok(t.unwrap()),
1673+
Err(e) => Err(E::try_from(e.error).map_err(Into::into)),
1674+
}
1675+
}
1676+
15581677
/// Creates a new Env loaded with the [`Snapshot`].
15591678
///
15601679
/// The ledger info and state in the snapshot are loaded into the Env.

soroban-sdk/src/tests/env.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use soroban_sdk_macros::contracterror;
2+
13
use crate::{
24
self as soroban_sdk, contract, contractimpl,
35
env::EnvTestConfig,
4-
testutils::{Address as _, Logs as _},
6+
testutils::{Address as _, Logs},
57
xdr::{ScErrorCode, ScErrorType},
6-
Address, Env, Error,
8+
Address, Env, Error, InvokeError, Symbol,
79
};
810

911
#[test]
@@ -36,6 +38,12 @@ impl Contract {
3638
}
3739
}
3840

41+
#[contracterror]
42+
#[derive(Debug, Eq, PartialEq)]
43+
enum ContractError {
44+
AnError = 1,
45+
}
46+
3947
#[test]
4048
fn default_and_from_snapshot_same_settings() {
4149
let env1 = Env::default();
@@ -212,3 +220,109 @@ fn test_snapshot_file_disabled_after_creation() {
212220
assert!(!p2.exists());
213221
let _ = std::fs::remove_file(&p1);
214222
}
223+
224+
#[test]
225+
fn test_try_as_contract() {
226+
let env = Env::default();
227+
228+
let addr = Address::generate(&env);
229+
env.register_at(&addr, Contract, ());
230+
231+
let key = Symbol::new(&env, "foo");
232+
let val = Symbol::new(&env, "bar");
233+
234+
env.as_contract(&addr, || {
235+
env.storage().persistent().set(&key, &val);
236+
});
237+
238+
let result = env.try_as_contract::<Symbol, Error>(&addr, || {
239+
env.storage()
240+
.persistent()
241+
.get::<Symbol, Symbol>(&key)
242+
.unwrap()
243+
});
244+
assert_eq!(result, Ok(val));
245+
}
246+
247+
#[test]
248+
fn test_try_as_contract_host_error() {
249+
let env = Env::default();
250+
251+
let addr = Address::generate(&env);
252+
env.register_at(&addr, Contract, ());
253+
254+
let key = Symbol::new(&env, "foo");
255+
256+
let result = env.try_as_contract::<_, Error>(&addr, || {
257+
// should error as key doesn't exist in storage
258+
env.storage().persistent().extend_ttl(&key, 1, 100);
259+
});
260+
assert_eq!(
261+
result,
262+
Err(Ok(Error::from_type_and_code(
263+
ScErrorType::Storage,
264+
ScErrorCode::MissingValue
265+
)))
266+
);
267+
}
268+
269+
#[test]
270+
fn test_try_as_contract_host_error_contract_error_expected() {
271+
let env = Env::default();
272+
273+
let addr = Address::generate(&env);
274+
env.register_at(&addr, Contract, ());
275+
276+
let key = Symbol::new(&env, "foo");
277+
278+
let result = env.try_as_contract::<_, ContractError>(&addr, || {
279+
// should error as key doesn't exist in storage
280+
env.storage().persistent().extend_ttl(&key, 1, 100);
281+
});
282+
assert_eq!(result, Err(Err(InvokeError::Abort)));
283+
}
284+
285+
#[test]
286+
fn test_try_as_contract_contract_error() {
287+
let env = Env::default();
288+
289+
let addr = Address::generate(&env);
290+
env.register_at(&addr, Contract, ());
291+
292+
let result = env.try_as_contract::<_, ContractError>(&addr, || {
293+
panic_with_error!(&env, ContractError::AnError);
294+
});
295+
assert_eq!(result, Err(Ok(ContractError::AnError)));
296+
}
297+
298+
#[test]
299+
fn test_try_as_contract_contract_error_unexpected_error() {
300+
let env = Env::default();
301+
302+
let addr = Address::generate(&env);
303+
env.register_at(&addr, Contract, ());
304+
305+
let result = env.try_as_contract::<_, ContractError>(&addr, || {
306+
panic_with_error!(&env, Error::from_contract_error(99));
307+
});
308+
assert_eq!(result, Err(Err(InvokeError::Contract(99))));
309+
}
310+
311+
#[test]
312+
fn test_try_as_contract_panic() {
313+
let env = Env::default();
314+
315+
let addr = Address::generate(&env);
316+
env.register_at(&addr, Contract, ());
317+
318+
let result = env.try_as_contract::<_, Error>(&addr, || {
319+
panic!("please don't do this when writing contracts");
320+
});
321+
assert_eq!(
322+
result,
323+
Err(Ok(Error::from_type_and_code(
324+
ScErrorType::WasmVm,
325+
ScErrorCode::InvalidAction
326+
)))
327+
);
328+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"generators": {
3+
"address": 1,
4+
"nonce": 0,
5+
"mux_id": 0
6+
},
7+
"auth": [
8+
[],
9+
[],
10+
[]
11+
],
12+
"ledger": {
13+
"protocol_version": 25,
14+
"sequence_number": 0,
15+
"timestamp": 0,
16+
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
17+
"base_reserve": 0,
18+
"min_persistent_entry_ttl": 4096,
19+
"min_temp_entry_ttl": 16,
20+
"max_entry_ttl": 6312000,
21+
"ledger_entries": [
22+
{
23+
"entry": {
24+
"last_modified_ledger_seq": 0,
25+
"data": {
26+
"contract_data": {
27+
"ext": "v0",
28+
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
29+
"key": {
30+
"symbol": "foo"
31+
},
32+
"durability": "persistent",
33+
"val": {
34+
"symbol": "bar"
35+
}
36+
}
37+
},
38+
"ext": "v0"
39+
},
40+
"live_until": 4095
41+
},
42+
{
43+
"entry": {
44+
"last_modified_ledger_seq": 0,
45+
"data": {
46+
"contract_data": {
47+
"ext": "v0",
48+
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
49+
"key": "ledger_key_contract_instance",
50+
"durability": "persistent",
51+
"val": {
52+
"contract_instance": {
53+
"executable": {
54+
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
55+
},
56+
"storage": null
57+
}
58+
}
59+
}
60+
},
61+
"ext": "v0"
62+
},
63+
"live_until": 4095
64+
},
65+
{
66+
"entry": {
67+
"last_modified_ledger_seq": 0,
68+
"data": {
69+
"contract_code": {
70+
"ext": "v0",
71+
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
72+
"code": ""
73+
}
74+
},
75+
"ext": "v0"
76+
},
77+
"live_until": 4095
78+
}
79+
]
80+
},
81+
"events": []
82+
}

0 commit comments

Comments
 (0)