Skip to content

Commit 95edbe4

Browse files
committed
Preserve backtraces across secondary error events
Closes #1266
1 parent f035dda commit 95edbe4

8 files changed

Lines changed: 296 additions & 16 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
" 0 begin": "cpu:14488, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
3+
" 1 call symbol_new_from_slice(13)": "cpu:29038, mem:1726, objs:-/2@7826ffba, store:-/2@8f53be5d, foot:2@b5543661",
4+
" 2 ret symbol_new_from_slice -> Ok(Symbol(obj#5))": "cpu:30046, mem:1819, objs:-/3@13db8e81",
5+
" 3 call vec_new_from_slice(2)": "",
6+
" 4 ret vec_new_from_slice -> Ok(Vec(obj#7))": "cpu:31011, mem:1915, objs:-/4@7e1b7d3c",
7+
" 5 call try_call(Address(obj#1), Symbol(obj#5), Vec(obj#7))": "",
8+
" 6 call symbol_len(Symbol(obj#5))": "cpu:31887, mem:1947",
9+
" 7 ret symbol_len -> Ok(U32(13))": "cpu:32009",
10+
" 8 call symbol_copy_to_slice(Symbol(obj#5), U32(0), 13)": "",
11+
" 9 ret symbol_copy_to_slice -> Ok(())": "cpu:32113",
12+
" 10 push TEST: 0:sym#5(Symbol(key), U64(1))": "cpu:41841, mem:3075, objs:-/5@8aed2070, stk:1@7d326773, auth:1@9c77a914/-",
13+
" 11 call symbol_new_from_slice(13)": "",
14+
" 12 ret symbol_new_from_slice -> Ok(Symbol(obj#11))": "cpu:42849, mem:3168, objs:-/6@b7c09f27",
15+
" 13 call symbol_new_from_slice(16)": "",
16+
" 14 ret symbol_new_from_slice -> Ok(Symbol(obj#13))": "cpu:43860, mem:3264, objs:-/7@aade354e",
17+
" 15 call obj_cmp(Symbol(obj#5), Symbol(obj#11))": "",
18+
" 16 ret obj_cmp -> Ok(0)": "cpu:44149",
19+
" 17 call put_contract_data(Symbol(key), U64(1), Temporary)": "",
20+
" 18 ret put_contract_data -> Ok(Void)": "cpu:49489, mem:4294, store:-/3@c95e49e7, foot:3@96664895",
21+
" 19 pop TEST: 0:sym#5 -> Ok(Void)": "",
22+
" 20 ret try_call -> Ok(Void)": " stk:-, auth:-/-",
23+
" 21 call get_ledger_sequence()": "",
24+
" 22 ret get_ledger_sequence -> Ok(U32(0))": "",
25+
" 23 call symbol_new_from_slice(16)": "",
26+
" 24 ret symbol_new_from_slice -> Ok(Symbol(obj#15))": "cpu:50500, mem:4390, objs:-/8@4c996cc6",
27+
" 25 call vec_new_from_slice(3)": "",
28+
" 26 ret vec_new_from_slice -> Ok(Vec(obj#17))": "cpu:51467, mem:4494, objs:-/9@29e56213",
29+
" 27 call try_call(Address(obj#1), Symbol(obj#15), Vec(obj#17))": "",
30+
" 28 call symbol_len(Symbol(obj#15))": "cpu:52345, mem:4534",
31+
" 29 ret symbol_len -> Ok(U32(16))": "cpu:52467",
32+
" 30 call symbol_copy_to_slice(Symbol(obj#15), U32(0), 16)": "",
33+
" 31 ret symbol_copy_to_slice -> Ok(())": "cpu:52572",
34+
" 32 push TEST: 0:sym#15(Symbol(key), U32(1), U32(6311999))": "cpu:62316, mem:5730, objs:-/10@980b5b6c, stk:1@cec2ee86, auth:1@b86cb58d/-",
35+
" 33 call symbol_new_from_slice(13)": "",
36+
" 34 ret symbol_new_from_slice -> Ok(Symbol(obj#21))": "cpu:63324, mem:5823, objs:-/11@a3078f63",
37+
" 35 call symbol_new_from_slice(16)": "",
38+
" 36 ret symbol_new_from_slice -> Ok(Symbol(obj#23))": "cpu:64335, mem:5919, objs:-/12@76cf0cad",
39+
" 37 call obj_cmp(Symbol(obj#15), Symbol(obj#21))": "",
40+
" 38 ret obj_cmp -> Ok(-1)": "cpu:64624",
41+
" 39 call obj_cmp(Symbol(obj#15), Symbol(obj#23))": "",
42+
" 40 ret obj_cmp -> Ok(0)": "cpu:64914",
43+
" 41 call extend_contract_data_ttl(Symbol(key), Temporary, U32(1), U32(6311999))": "",
44+
" 42 call get_ledger_sequence()": "cpu:66241, mem:6055, store:-/-, foot:-",
45+
" 43 ret get_ledger_sequence -> Ok(U32(1000000000))": "",
46+
" 44 ret extend_contract_data_ttl -> Err(Error(Storage, InternalError))": " store:-/3@c95e49e7, foot:3@96664895",
47+
" 45 pop TEST: 0:sym#15 -> Err(Error(Storage, InternalError))": " stk:1@27cbf3bc",
48+
" 46 ret try_call -> Err(Error(Storage, InternalError))": " stk:-, auth:-/-",
49+
" 47 end": "cpu:66241, mem:6055, prngs:-/9b4a753, objs:-/12@76cf0cad, vm:-/-, evt:-, store:-/3@c95e49e7, foot:3@96664895, stk:-, auth:-/-"
50+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
" 0 begin": "cpu:14488, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
3+
" 1 call bytes_new_from_slice(2926)": "cpu:14535",
4+
" 2 ret bytes_new_from_slice -> Ok(Bytes(obj#1))": "cpu:16226, mem:3006, objs:-/1@59b06c07",
5+
" 3 call upload_wasm(Bytes(obj#1))": "",
6+
" 4 ret upload_wasm -> Ok(Bytes(obj#3))": "cpu:1808452, mem:1301182, objs:-/2@4011c09e, store:-/1@39074ab1, foot:1@32a3b76c",
7+
" 5 call bytes_new_from_slice(32)": "cpu:1808892, mem:1301246, objs:-/3@5e827dcf",
8+
" 6 ret bytes_new_from_slice -> Ok(Bytes(obj#7))": "cpu:1809861, mem:1301358, objs:-/4@965f719a",
9+
" 7 call create_contract(Address(obj#5), Bytes(obj#3), Bytes(obj#7))": "",
10+
" 8 call obj_cmp(Address(obj#9), Address(obj#5))": "cpu:1811504, mem:1301536, objs:-/5@102310eb, auth:1@37cdfd8a/-",
11+
" 9 ret obj_cmp -> Ok(0)": "cpu:1811796",
12+
" 10 call get_ledger_network_id()": "cpu:1811846, auth:1@37cdfd8a/1@ca935ef7",
13+
" 11 ret get_ledger_network_id -> Ok(Bytes(obj#11))": "cpu:1812876, mem:1301648, objs:-/6@6f654453",
14+
" 12 ret create_contract -> Ok(Address(obj#13))": "cpu:1831009, mem:1304722, objs:-/7@fe1adc27, store:-/2@7c97e17d, foot:2@35ccd57a, auth:-/1@dce30087",
15+
" 13 call symbol_new_from_slice(13)": " auth:-/-",
16+
" 14 ret symbol_new_from_slice -> Ok(Symbol(obj#15))": "cpu:1832017, mem:1304815, objs:-/8@4c5585d5",
17+
" 15 call vec_new_from_slice(2)": "",
18+
" 16 ret vec_new_from_slice -> Ok(Vec(obj#17))": "cpu:1832982, mem:1304911, objs:-/9@d8153495",
19+
" 17 call try_call(Address(obj#13), Symbol(obj#15), Vec(obj#17))": "",
20+
" 18 call symbol_len(Symbol(obj#15))": "cpu:1833858, mem:1304943",
21+
" 19 ret symbol_len -> Ok(U32(13))": "cpu:1833980",
22+
" 20 call symbol_copy_to_slice(Symbol(obj#15), U32(0), 13)": "",
23+
" 21 ret symbol_copy_to_slice -> Ok(())": "cpu:1834084",
24+
" 22 push VM:fc644715:sym#15(Symbol(key), U64(1))": "cpu:3467614, mem:2603597, objs:-/10@ba8a4b77, vm:1048576@2434eedc/20@26ecda6, stk:1@2048e896, auth:1@b86cb58d/-",
25+
" 23 call symbol_len(Symbol(obj#15))": "cpu:3470091, mem:2603659",
26+
" 24 ret symbol_len -> Ok(U32(13))": "cpu:3470213",
27+
" 25 call symbol_copy_to_slice(Symbol(obj#15), U32(0), 13)": "",
28+
" 26 ret symbol_copy_to_slice -> Ok(())": "cpu:3470317",
29+
" 27 call put_contract_data(Symbol(key), U64(1), Temporary)": " vm:-/-",
30+
" 28 ret put_contract_data -> Ok(Void)": "cpu:3478555, mem:2604689, store:-/3@cff64dbc, foot:3@ec4df205",
31+
" 29 pop VM:fc644715:sym#15 -> Ok(Void)": " vm:1048576@f7f6eb14/20@26ecda6",
32+
" 30 ret try_call -> Ok(Void)": " vm:-/-, stk:-, auth:-/-",
33+
" 31 call get_ledger_sequence()": "",
34+
" 32 ret get_ledger_sequence -> Ok(U32(0))": "",
35+
" 33 call symbol_new_from_slice(16)": "",
36+
" 34 ret symbol_new_from_slice -> Ok(Symbol(obj#21))": "cpu:3479566, mem:2604785, objs:-/11@1eac391c",
37+
" 35 call vec_new_from_slice(3)": "",
38+
" 36 ret vec_new_from_slice -> Ok(Vec(obj#23))": "cpu:3480533, mem:2604889, objs:-/12@4a5a5128",
39+
" 37 call try_call(Address(obj#13), Symbol(obj#21), Vec(obj#23))": "",
40+
" 38 call symbol_len(Symbol(obj#21))": "cpu:3481411, mem:2604929",
41+
" 39 ret symbol_len -> Ok(U32(16))": "cpu:3481533",
42+
" 40 call symbol_copy_to_slice(Symbol(obj#21), U32(0), 16)": "",
43+
" 41 ret symbol_copy_to_slice -> Ok(())": "cpu:3481638",
44+
" 42 push VM:fc644715:sym#21(Symbol(key), U32(1), U32(6311999))": "cpu:5115186, mem:3903659, objs:-/13@ce7efe42, vm:1048576@2434eedc/20@26ecda6, stk:1@afa4f10, auth:1@4b43f04d/-",
45+
" 43 call symbol_len(Symbol(obj#21))": "cpu:5117667, mem:3903737",
46+
" 44 ret symbol_len -> Ok(U32(16))": "cpu:5117789",
47+
" 45 call symbol_copy_to_slice(Symbol(obj#21), U32(0), 16)": "",
48+
" 46 ret symbol_copy_to_slice -> Ok(())": "cpu:5117894",
49+
" 47 call extend_contract_data_ttl(Symbol(key), Temporary, U32(1), U32(6311999))": " vm:-/-",
50+
" 48 call get_ledger_sequence()": "cpu:5120687, mem:3903873, store:-/-, foot:-",
51+
" 49 ret get_ledger_sequence -> Ok(U32(1000000000))": "",
52+
" 50 ret extend_contract_data_ttl -> Err(Error(Storage, InternalError))": " store:-/3@cff64dbc, foot:3@ec4df205",
53+
" 51 pop VM:fc644715:sym#21 -> Err(Error(Storage, InternalError))": " vm:1048576@146376a2/20@26ecda6",
54+
" 52 ret try_call -> Err(Error(Storage, InternalError))": " vm:-/-, stk:-, auth:-/-",
55+
" 53 end": "cpu:5120687, mem:3903873, prngs:-/9b4a753, objs:-/13@ce7efe42, vm:-/-, evt:-, store:-/3@cff64dbc, foot:3@ec4df205, stk:-, auth:-/-"
56+
}

soroban-env-host/src/host.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,12 +697,12 @@ impl EnvBase for Host {
697697
let _ = self.with_current_frame_opt(|f| {
698698
if let Some(Frame::TestContract(frame)) = f {
699699
if let Ok(mut panic) = frame.panic.try_borrow_mut() {
700-
*panic = Some(e.error);
700+
*panic = Some(e.clone());
701701
}
702702
}
703703
Ok(())
704704
});
705-
let escalation = self.error(e.error, "escalating error to panic", &[]);
705+
let escalation = self.secondary_error(e, "escalating error to panic", &[]);
706706
panic!("{:?}", escalation)
707707
}
708708

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,13 @@ impl Host {
254254
/// enriches the returned [Error] with [DebugInfo] in the form of a
255255
/// [Backtrace] and snapshot of the [Events] buffer.
256256
pub(crate) fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError {
257-
let mut he = HostError::from(error);
257+
self.secondary_error(HostError::from(error), msg, args)
258+
}
259+
260+
/// The same as [Host::error] but it will preserve the original
261+
/// backtrace from `error` instead of generating a new one.
262+
pub(crate) fn secondary_error(&self, error: HostError, msg: &str, args: &[Val]) -> HostError {
263+
let mut he = error;
258264
self.with_debug_mode(|| {
259265
// We _try_ to take a mutable borrow of the events buffer refcell
260266
// while building up the event we're going to emit into the events
@@ -264,11 +270,24 @@ impl Host {
264270
// get an error _while performing_ any of the steps needed to record
265271
// an error as an event, below.
266272
if let Ok(mut events_refmut) = self.0.events.try_borrow_mut() {
267-
self.record_err_diagnostics(events_refmut.deref_mut(), error, msg, args);
273+
self.record_err_diagnostics(events_refmut.deref_mut(), he.error, msg, args);
268274
}
275+
#[cfg(not(any(test, feature = "testutils")))]
276+
let info = self.maybe_get_debug_info();
277+
#[cfg(any(test, feature = "testutils"))]
278+
let info = {
279+
let mut info = self.maybe_get_debug_info();
280+
match (&he.info, &mut info) {
281+
(Some(heinfo), Some(ref mut info)) => {
282+
info.backtrace = heinfo.backtrace.clone();
283+
}
284+
_ => {}
285+
}
286+
info
287+
};
269288
he = HostError {
270-
error,
271-
info: self.maybe_get_debug_info(),
289+
error: he.error,
290+
info,
272291
};
273292
Ok(())
274293
});

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub(crate) struct TestContractFrame {
5757
pub(crate) id: Hash,
5858
pub(crate) func: Symbol,
5959
pub(crate) args: Vec<Val>,
60-
pub(crate) panic: Rc<RefCell<Option<Error>>>,
60+
pub(crate) panic: Rc<RefCell<Option<HostError>>>,
6161
pub(crate) instance: ScContractInstance,
6262
}
6363

@@ -68,12 +68,26 @@ impl std::hash::Hash for TestContractFrame {
6868
self.func.hash(state);
6969
self.args.hash(state);
7070
if let Some(panic) = self.panic.borrow().as_ref() {
71-
panic.hash(state);
71+
panic.error.hash(state);
72+
// NB: not hashing the (testutils) debuginfo/backtrace in the error.
73+
// At present this hash is only used for capturing the trace state,
74+
// and a hash of the backtrace would be very expensive and platform-specific.
75+
// This function will be incorrect if used in a HashMap; because of this
76+
// we implement Eq to panic.
7277
}
7378
self.instance.hash(state);
7479
}
7580
}
7681

82+
#[cfg(any(test, feature = "testutils"))]
83+
impl std::cmp::PartialEq<Self> for TestContractFrame {
84+
fn eq(&self, _other: &Self) -> bool {
85+
// This is to prevent accidentally putting TestContractFrame into a HashMap.
86+
// See comments in impl Hash.
87+
unreachable!();
88+
}
89+
}
90+
7791
#[cfg(any(test, feature = "testutils"))]
7892
impl TestContractFrame {
7993
pub fn new(id: Hash, func: Symbol, args: Vec<Val>, instance: ScContractInstance) -> Self {
@@ -781,9 +795,9 @@ impl Host {
781795
// panicked.
782796
//
783797
// If it was a panic generated by a Env-upgraded
784-
// HostError, it had its `Error` captured by
798+
// HostError, it had its error captured by
785799
// `VmCallerEnv::escalate_error_to_panic`: fish the
786-
// `Error` stored in the frame back out and
800+
// `HostError` stored in the frame back out and
787801
// propagate it.
788802
//
789803
// If it was a panic generated by user code calling
@@ -795,14 +809,15 @@ impl Host {
795809
// trap-unreachable code). It's a little weird
796810
// because we're not actually running a VM, but we
797811
// prioritize emulation fidelity over honesty here.
798-
let mut error: Error =
799-
Error::from(wasmi::core::TrapCode::UnreachableCodeReached);
812+
let mut error: HostError = HostError::from(Error::from(
813+
wasmi::core::TrapCode::UnreachableCodeReached,
814+
));
800815

801816
let mut recovered_error_from_panic_refcell = false;
802817
if let Ok(panic) = panic.try_borrow() {
803-
if let Some(err) = *panic {
818+
if let Some(ref err) = *panic {
804819
recovered_error_from_panic_refcell = true;
805-
error = err;
820+
error = err.clone();
806821
}
807822
}
808823

@@ -833,7 +848,7 @@ impl Host {
833848
Ok(())
834849
})
835850
}
836-
Err(self.error(error, "caught error from function", &[]))
851+
Err(self.secondary_error(error, "caught error from function", &[]))
837852
}
838853
}
839854
});

soroban-env-host/src/test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub(crate) mod observe;
22

33
mod address;
44
mod auth;
5+
#[cfg(feature = "testutils")]
6+
mod backtrace;
57
mod basic;
68
mod budget_metering;
79
mod bytes;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use std::cmp::Ordering;
2+
use std::io::Write;
3+
use std::rc::Rc;
4+
5+
use crate::events::diagnostic::DiagnosticLevel;
6+
use crate::{xdr, ContractFunctionSet, Host, StorageType, Val};
7+
use soroban_env_common::Compare;
8+
use soroban_env_common::{Env, Symbol, TryFromVal, TryIntoVal};
9+
use soroban_test_wasms::CONTRACT_STORAGE;
10+
11+
fn storage_fn_name(host: &Host, fn_name: &str, storage: &str) -> Symbol {
12+
Symbol::try_from_val(host, &format!("{}_{}", fn_name, storage).as_str()).unwrap()
13+
}
14+
15+
// This is testing that an error (triggered in Storage::extend_ttl) generates
16+
// a backtrace, and that backtrace is carried all the way to the contract call
17+
// site. This involves handing the backtrace off from the original error to
18+
// a secondary error event.
19+
#[test]
20+
fn test_backtrace_wasm() {
21+
let host = observe_host!(Host::test_host_with_recording_footprint());
22+
host.set_diagnostic_level(DiagnosticLevel::Debug).unwrap();
23+
let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
24+
25+
// Create some storage
26+
let key = Symbol::try_from_small_str("key").unwrap();
27+
let r = host.try_call(
28+
contract_id,
29+
storage_fn_name(&host, "put", "temporary"),
30+
test_vec![&*host, key, 1_u64].into(),
31+
);
32+
33+
assert!(r.is_ok());
34+
35+
// Advance the ledger but leave the expired storage in the host.
36+
// This will trigger an error about "accessing no-longer-live entry".
37+
let max_live_until_ledger: u32 = host.max_live_until_ledger().unwrap().into();
38+
let ledger_seq: u32 = host.get_ledger_sequence().unwrap().into();
39+
let max_extend = max_live_until_ledger - ledger_seq;
40+
let threshold: u32 = 1;
41+
let _ = host.with_mut_ledger_info(|ledger| {
42+
ledger.sequence_number += 1000000000;
43+
ledger.timestamp += 10000000;
44+
});
45+
46+
let r = host.try_call(
47+
contract_id,
48+
storage_fn_name(&host, "extend", "temporary"),
49+
test_vec![&*host, key, threshold, max_extend].into(),
50+
);
51+
52+
// The error should contain Storage::extend_ttl in the backtrace
53+
let err = r.unwrap_err();
54+
let mut buf = Vec::<u8>::new();
55+
let _ = writeln!(&mut buf, "{:?}", err);
56+
let buf = String::from_utf8_lossy(&buf);
57+
let expected_frame = buf.contains("Storage::extend_ttl");
58+
assert!(expected_frame);
59+
}
60+
61+
struct Contract;
62+
63+
use soroban_env_common::EnvBase;
64+
65+
impl ContractFunctionSet for Contract {
66+
fn call(&self, func: &Symbol, host: &Host, args: &[Val]) -> Option<Val> {
67+
let put_temporary = Symbol::try_from_val(host, &"put_temporary").unwrap();
68+
let extend_temporary = Symbol::try_from_val(host, &"extend_temporary").unwrap();
69+
70+
if host.compare(func, &put_temporary).unwrap() == Ordering::Equal {
71+
host.put_contract_data(args[0], args[1], StorageType::Temporary)
72+
.unwrap();
73+
Some(().into())
74+
} else if host.compare(func, &extend_temporary).unwrap() == Ordering::Equal {
75+
let r = host.extend_contract_data_ttl(
76+
args[0],
77+
StorageType::Temporary,
78+
args[1].try_into().unwrap(),
79+
args[2].try_into().unwrap(),
80+
);
81+
if let Err(e) = r {
82+
// This is what the SDK does when it generates a HostError while
83+
// calling a contract, and this code path needs to preserve
84+
// the backtrace across the panic.
85+
host.escalate_error_to_panic(e);
86+
}
87+
Some(().into())
88+
} else {
89+
panic!()
90+
}
91+
}
92+
}
93+
94+
#[test]
95+
fn test_backtrace_native() {
96+
let host = observe_host!(Host::test_host_with_recording_footprint());
97+
host.set_diagnostic_level(DiagnosticLevel::Debug).unwrap();
98+
let contract_id = host
99+
.add_host_object(xdr::ScAddress::Contract(xdr::Hash([0; 32])))
100+
.unwrap();
101+
host.register_test_contract(contract_id, Rc::new(Contract))
102+
.unwrap();
103+
104+
// Create some storage
105+
let key = Symbol::try_from_small_str("key").unwrap();
106+
let r = host.try_call(
107+
contract_id,
108+
storage_fn_name(&host, "put", "temporary"),
109+
test_vec![&*host, key, 1_u64].into(),
110+
);
111+
112+
assert!(r.is_ok());
113+
114+
// Advance the ledger but leave the expired storage in the host.
115+
// This will trigger an error about "accessing no-longer-live entry".
116+
let max_live_until_ledger: u32 = host.max_live_until_ledger().unwrap().into();
117+
let ledger_seq: u32 = host.get_ledger_sequence().unwrap().into();
118+
let max_extend = max_live_until_ledger - ledger_seq;
119+
let threshold: u32 = 1;
120+
let _ = host.with_mut_ledger_info(|ledger| {
121+
ledger.sequence_number += 1000000000;
122+
ledger.timestamp += 10000000;
123+
});
124+
125+
let r = host.try_call(
126+
contract_id,
127+
storage_fn_name(&host, "extend", "temporary"),
128+
test_vec![&*host, key, threshold, max_extend].into(),
129+
);
130+
131+
// The error should contain Storage::extend_ttl in the backtrace
132+
let err = r.unwrap_err();
133+
let mut buf = Vec::<u8>::new();
134+
let _ = writeln!(&mut buf, "{:?}", err);
135+
let buf = String::from_utf8_lossy(&buf);
136+
let expected_frame = buf.contains("Storage::extend_ttl");
137+
assert!(expected_frame);
138+
}

0 commit comments

Comments
 (0)