Skip to content

Commit d189f9e

Browse files
Enhance XCM Debugging with Log Capture in Unit Tests (#7594)
# Description This PR introduces a lightweight log-capturing mechanism for XCM unit tests, simplifying debugging by enabling structured log assertions. It partially addresses #6119 and #6125, offering an optional way to verify logs in tests while remaining unobtrusive in normal execution. # Key Changes * [x] Introduces a log capture utility in `sp_tracing`. * [x] Adds XCM test examples demonstrating how and when to use log capturing. # Review Notes: * The log capture mechanism is opt-in and does not affect existing tests unless explicitly used. * The implementation is minimal and does not add complexity to existing test setups. * It provides a structured alternative to [`sp_tracing::init_for_tests()`](https://paritytech.github.io/polkadot-sdk/master/sp_tracing/fn.init_for_tests.html) for log verification in automated tests. --------- Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent cf52a0d commit d189f9e

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed

polkadot/xcm/xcm-simulator/example/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ runtime-benchmarks = [
5252
"xcm-executor/runtime-benchmarks",
5353
"xcm/runtime-benchmarks",
5454
]
55+
56+
[dev-dependencies]
57+
sp-tracing = { features = ["test-utils"], workspace = true }

polkadot/xcm/xcm-simulator/example/src/tests.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ fn reserve_transfer() {
136136
relay_chain::Balances::free_balance(&child_account_id(1)),
137137
INITIAL_BALANCE + withdraw_amount
138138
);
139+
// Ensure expected events were emitted
140+
let events = relay_chain::System::events();
141+
let attempted_count = count_relay_chain_events(&events, |event| {
142+
matches!(
143+
event,
144+
relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { .. })
145+
)
146+
});
147+
let sent_count = count_relay_chain_events(&events, |event| {
148+
matches!(event, relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }))
149+
});
150+
assert_eq!(attempted_count, 1, "Expected one XcmPallet::Attempted event");
151+
assert_eq!(sent_count, 1, "Expected one XcmPallet::Sent event");
139152
});
140153

141154
ParaA::execute_with(|| {
@@ -147,6 +160,62 @@ fn reserve_transfer() {
147160
});
148161
}
149162

163+
#[test]
164+
fn reserve_transfer_with_error() {
165+
use sp_tracing::{
166+
test_log_capture::init_log_capture,
167+
tracing::{subscriber, Level},
168+
};
169+
170+
// Reset the test network
171+
MockNet::reset();
172+
173+
// Execute XCM Transfer and Capture Logs
174+
let (log_capture, subscriber) = init_log_capture(Level::ERROR);
175+
subscriber::with_default(subscriber, || {
176+
let invalid_dest = Box::new(Parachain(9999).into());
177+
let withdraw_amount = 123;
178+
179+
Relay::execute_with(|| {
180+
let result = RelayChainPalletXcm::limited_reserve_transfer_assets(
181+
relay_chain::RuntimeOrigin::signed(ALICE),
182+
invalid_dest,
183+
Box::new(AccountId32 { network: None, id: ALICE.into() }.into()),
184+
Box::new((Here, withdraw_amount).into()),
185+
0,
186+
Unlimited,
187+
);
188+
189+
// Ensure an error occurred
190+
assert!(result.is_err(), "Expected an error due to invalid destination");
191+
192+
// Assert captured logs
193+
assert!(log_capture.contains("XCM validate_send failed"));
194+
195+
// Verify that XcmPallet::Attempted was NOT emitted (rollback happened)
196+
let events = relay_chain::System::events();
197+
let xcm_attempted_emitted = events.iter().any(|e| {
198+
matches!(
199+
e.event,
200+
relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { .. })
201+
)
202+
});
203+
assert!(
204+
!xcm_attempted_emitted,
205+
"Expected no XcmPallet::Attempted event due to rollback, but it was emitted"
206+
);
207+
});
208+
209+
// Ensure no balance change due to the error
210+
ParaA::execute_with(|| {
211+
assert_eq!(
212+
pallet_balances::Pallet::<parachain::Runtime>::free_balance(&ALICE),
213+
INITIAL_BALANCE
214+
);
215+
});
216+
});
217+
}
218+
150219
#[test]
151220
fn remote_locking_and_unlocking() {
152221
MockNet::reset();
@@ -511,3 +580,13 @@ fn query_holding() {
511580
);
512581
});
513582
}
583+
584+
fn count_relay_chain_events<F>(
585+
events: &[frame_system::EventRecord<relay_chain::RuntimeEvent, sp_core::H256>],
586+
predicate: F,
587+
) -> usize
588+
where
589+
F: Fn(&relay_chain::RuntimeEvent) -> bool,
590+
{
591+
events.iter().filter(|e| predicate(&e.event)).count()
592+
}

prdoc/pr_7594.prdoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
title: Improve XCM Debugging by Capturing Logs in Unit Tests
2+
doc:
3+
- audience: Runtime Dev
4+
description: |-
5+
This PR introduces a lightweight log-capturing mechanism for XCM unit tests, making it easier to troubleshoot failures when needed.
6+
crates:
7+
- name: pallet-xcm
8+
bump: patch
9+
- name: xcm-simulator-example
10+
bump: patch
11+
- name: sp-tracing
12+
bump: minor

substrate/primitives/tracing/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ std = [
4040
"tracing/std",
4141
"with-tracing",
4242
]
43+
test-utils = ["std"]

substrate/primitives/tracing/src/lib.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,189 @@ macro_rules! enter_span {
255255
$crate::enter_span!($crate::span!($lvl, $name))
256256
};
257257
}
258+
259+
#[cfg(feature = "test-utils")]
260+
pub mod test_log_capture {
261+
use std::{
262+
io::Write,
263+
sync::{Arc, Mutex},
264+
};
265+
use tracing::level_filters::LevelFilter;
266+
use tracing_subscriber::fmt::{
267+
format::{DefaultFields, Format},
268+
MakeWriter, Subscriber,
269+
};
270+
271+
/// A reusable log capturing struct for unit tests.
272+
/// Captures logs written during test execution for assertions.
273+
///
274+
/// # Examples
275+
/// ```
276+
/// use sp_tracing::test_log_capture::LogCapture;
277+
/// use std::io::Write;
278+
///
279+
/// let mut log_capture = LogCapture::new();
280+
/// writeln!(log_capture, "Test log message").unwrap();
281+
/// assert!(log_capture.contains("Test log message"));
282+
/// ```
283+
pub struct LogCapture {
284+
buffer: Arc<Mutex<Vec<u8>>>,
285+
}
286+
287+
impl LogCapture {
288+
/// Creates a new `LogCapture` instance with an internal buffer.
289+
///
290+
/// # Examples
291+
/// ```
292+
/// use sp_tracing::test_log_capture::LogCapture;
293+
///
294+
/// let log_capture = LogCapture::new();
295+
/// assert!(log_capture.get_logs().is_empty());
296+
/// ```
297+
pub fn new() -> Self {
298+
LogCapture { buffer: Arc::new(Mutex::new(Vec::new())) }
299+
}
300+
301+
/// Checks if the captured logs contain a specific substring.
302+
///
303+
/// # Examples
304+
/// ```
305+
/// use sp_tracing::test_log_capture::LogCapture;
306+
/// use std::io::Write;
307+
///
308+
/// let mut log_capture = LogCapture::new();
309+
/// writeln!(log_capture, "Hello, world!").unwrap();
310+
/// assert!(log_capture.contains("Hello"));
311+
/// assert!(!log_capture.contains("Goodbye"));
312+
/// ```
313+
pub fn contains(&self, expected: &str) -> bool {
314+
let logs = self.get_logs();
315+
logs.contains(expected)
316+
}
317+
318+
/// Retrieves the captured logs as a `String`.
319+
///
320+
/// # Examples
321+
/// ```
322+
/// use sp_tracing::test_log_capture::LogCapture;
323+
/// use std::io::Write;
324+
///
325+
/// let mut log_capture = LogCapture::new();
326+
/// writeln!(log_capture, "Log entry").unwrap();
327+
/// assert_eq!(log_capture.get_logs().trim(), "Log entry");
328+
/// ```
329+
pub fn get_logs(&self) -> String {
330+
String::from_utf8(self.buffer.lock().unwrap().clone()).unwrap()
331+
}
332+
333+
/// Returns a clone of the internal buffer for use in `MakeWriter`.
334+
pub fn writer(&self) -> Self {
335+
LogCapture { buffer: Arc::clone(&self.buffer) }
336+
}
337+
}
338+
339+
impl Write for LogCapture {
340+
/// Writes log data into the internal buffer.
341+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
342+
let mut logs = self.buffer.lock().unwrap();
343+
logs.extend_from_slice(buf);
344+
Ok(buf.len())
345+
}
346+
347+
/// Flushes the internal buffer (no-op in this implementation).
348+
fn flush(&mut self) -> std::io::Result<()> {
349+
Ok(())
350+
}
351+
}
352+
353+
impl<'a> MakeWriter<'a> for LogCapture {
354+
type Writer = Self;
355+
356+
/// Provides a `MakeWriter` implementation for `tracing_subscriber`.
357+
fn make_writer(&'a self) -> Self::Writer {
358+
self.writer()
359+
}
360+
}
361+
362+
/// Initialises a log capture utility for testing.
363+
///
364+
/// This function sets up a `LogCapture` instance to capture logs during test execution.
365+
/// It also configures a `tracing_subscriber` with the specified maximum log level
366+
/// and a writer that directs logs to `LogCapture`.
367+
///
368+
/// # Arguments
369+
///
370+
/// * `max_level` - The maximum log level to capture, which can be converted into `LevelFilter`.
371+
///
372+
/// # Returns
373+
///
374+
/// A tuple containing:
375+
/// - `LogCapture`: The log capture instance.
376+
/// - `Subscriber`: A configured `tracing_subscriber` that captures logs.
377+
///
378+
/// # Examples
379+
///
380+
/// ```
381+
/// use sp_tracing::{
382+
/// test_log_capture::init_log_capture,
383+
/// tracing::{info, subscriber, Level},
384+
/// };
385+
///
386+
/// let (log_capture, subscriber) = init_log_capture(Level::INFO);
387+
/// subscriber::with_default(subscriber, || {
388+
/// info!("This log will be captured");
389+
/// assert!(log_capture.contains("This log will be captured"));
390+
/// });
391+
/// ```
392+
pub fn init_log_capture(
393+
max_level: impl Into<LevelFilter>,
394+
) -> (LogCapture, Subscriber<DefaultFields, Format, LevelFilter, LogCapture>) {
395+
// Create a new log capture instance
396+
let log_capture = LogCapture::new();
397+
398+
// Configure a tracing subscriber to use the log capture as the writer
399+
let subscriber = tracing_subscriber::fmt()
400+
.with_max_level(max_level) // Set the max log level
401+
.with_writer(log_capture.writer()) // Use LogCapture as the writer
402+
.finish();
403+
404+
(log_capture, subscriber)
405+
}
406+
407+
/// Macro for capturing logs during test execution.
408+
///
409+
/// It sets up a log subscriber with an optional maximum log level and captures the output.
410+
///
411+
/// # Examples
412+
/// ```
413+
/// use sp_tracing::{
414+
/// capture_test_logs,
415+
/// tracing::{info, warn, Level},
416+
/// };
417+
///
418+
/// let log_capture = capture_test_logs!(Level::WARN, {
419+
/// info!("Captured info message");
420+
/// warn!("Captured warning");
421+
/// });
422+
///
423+
/// assert!(!log_capture.contains("Captured log message"));
424+
/// assert!(log_capture.contains("Captured warning"));
425+
/// ```
426+
#[macro_export]
427+
macro_rules! capture_test_logs {
428+
// Case when max_level is provided
429+
($max_level:expr, $test:block) => {{
430+
let (log_capture, subscriber) =
431+
sp_tracing::test_log_capture::init_log_capture($max_level);
432+
433+
sp_tracing::tracing::subscriber::with_default(subscriber, || $test);
434+
435+
log_capture
436+
}};
437+
438+
// Case when max_level is omitted (defaults to DEBUG)
439+
($test:block) => {{
440+
capture_test_logs!(sp_tracing::tracing::Level::DEBUG, $test)
441+
}};
442+
}
443+
}

0 commit comments

Comments
 (0)