-
Notifications
You must be signed in to change notification settings - Fork 828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhance XCM Debugging with Log Capture in Unit Tests #7594
base: master
Are you sure you want to change the base?
Changes from 33 commits
624c15a
513b177
489247b
b52a1e6
ce94bff
9bd8d15
58f22d5
7db41c4
bd00fb3
6500072
c4572f2
286cd2f
b8aa377
ca1da9b
1e82b9e
bcb56d5
617fced
4a37d17
7dd0846
17ff9ee
4ff842f
5c44734
24c09af
c5c4050
e8c40c0
ab31f4b
f0d0d56
595de4c
fed1747
5d50f0f
01d8548
d93c6e1
654e004
e709419
2411944
d1fc37f
4aff24f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -136,6 +136,28 @@ fn reserve_transfer() { | |
relay_chain::Balances::free_balance(&child_account_id(1)), | ||
INITIAL_BALANCE + withdraw_amount | ||
); | ||
// Ensure expected events were emitted | ||
let events = relay_chain::System::events(); | ||
let attempted_count = events | ||
.iter() | ||
.filter(|e| { | ||
matches!( | ||
e.event, | ||
relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { .. }) | ||
) | ||
}) | ||
.count(); | ||
let sent_count = events | ||
.iter() | ||
.filter(|e| { | ||
matches!( | ||
e.event, | ||
relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }) | ||
) | ||
}) | ||
.count(); | ||
assert_eq!(attempted_count, 1, "Expected one XcmPallet::Attempted event"); | ||
assert_eq!(sent_count, 1, "Expected one XcmPallet::Sent event"); | ||
}); | ||
|
||
ParaA::execute_with(|| { | ||
|
@@ -147,6 +169,58 @@ fn reserve_transfer() { | |
}); | ||
} | ||
|
||
#[test] | ||
fn reserve_transfer_with_error() { | ||
use sp_tracing::{test_log_capture, tracing::Level}; | ||
|
||
// Reset the test network | ||
MockNet::reset(); | ||
|
||
// Execute XCM Transfer and Capture Logs | ||
test_log_capture::capture_with_max_level(Level::ERROR, || { | ||
let invalid_dest = Box::new(Parachain(9999).into()); | ||
let withdraw_amount = 123; | ||
|
||
Relay::execute_with(|| { | ||
let result = RelayChainPalletXcm::limited_reserve_transfer_assets( | ||
relay_chain::RuntimeOrigin::signed(ALICE), | ||
invalid_dest, | ||
Box::new(AccountId32 { network: None, id: ALICE.into() }.into()), | ||
Box::new((Here, withdraw_amount).into()), | ||
0, | ||
Unlimited, | ||
); | ||
|
||
// Ensure an error occurred | ||
assert!(result.is_err(), "Expected an error due to invalid destination"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Could we move the |
||
|
||
// Verify that XcmPallet::Attempted was NOT emitted (rollback happened) | ||
let events = relay_chain::System::events(); | ||
let xcm_attempted_emitted = events.iter().any(|e| { | ||
matches!( | ||
e.event, | ||
relay_chain::RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { .. }) | ||
) | ||
}); | ||
assert!( | ||
!xcm_attempted_emitted, | ||
"Expected no XcmPallet::Attempted event due to rollback, but it was emitted" | ||
); | ||
}); | ||
|
||
// Ensure no balance change due to the error | ||
ParaA::execute_with(|| { | ||
assert_eq!( | ||
pallet_balances::Pallet::<parachain::Runtime>::free_balance(&ALICE), | ||
INITIAL_BALANCE | ||
); | ||
}); | ||
}); | ||
|
||
// Assertions on Captured Logs | ||
assert!(test_log_capture::logs_contain("XCM validate_send failed")); | ||
} | ||
|
||
#[test] | ||
fn remote_locking_and_unlocking() { | ||
MockNet::reset(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
title: Improve XCM Debugging by Capturing Logs in Unit Tests | ||
doc: | ||
- audience: Runtime Dev | ||
description: |- | ||
This PR introduces a lightweight log-capturing mechanism for XCM unit tests, making it easier to troubleshoot failures when needed. | ||
crates: | ||
- name: pallet-xcm | ||
bump: patch | ||
- name: xcm-simulator-example | ||
bump: patch | ||
- name: sp-tracing | ||
bump: patch |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -255,3 +255,113 @@ macro_rules! enter_span { | |
$crate::enter_span!($crate::span!($lvl, $name)) | ||
}; | ||
} | ||
|
||
/// `tracing-test` can be used for general logging capture but doesn't support logging within | ||
/// scoped functions like `Relay::execute_with`. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use tracing_test::traced_test; | ||
/// use tracing::info; | ||
/// | ||
/// #[traced_test] | ||
/// fn test_logging() { | ||
/// info!("This is a test log message"); | ||
/// assert!(logs_contain("test log message")); | ||
/// } | ||
/// ``` | ||
/// | ||
/// For more details, see [`tracing-test`](https://crates.io/crates/tracing-test). | ||
#[cfg(feature = "std")] | ||
pub use tracing_test; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would move this inside the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that makes sense. Maybe we can remove |
||
|
||
/// A module for capturing and asserting logs in tests. | ||
/// | ||
/// This module provides utilities for capturing logs during test execution | ||
/// and verifying expected log output. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use sp_tracing::test_log_capture; | ||
/// | ||
/// test_log_capture::capture(|| { | ||
/// tracing::info!("This is a test log message"); | ||
/// }); | ||
/// | ||
/// assert!(test_log_capture::logs_contain("test log message")); | ||
/// ``` | ||
#[cfg(feature = "std")] | ||
pub mod test_log_capture { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would guard this under a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
use tracing::subscriber; | ||
use tracing_core::LevelFilter; | ||
use tracing_test::internal::{global_buf, MockWriter}; | ||
|
||
/// Runs a test block with logging enabled and captures logs for assertions. | ||
/// | ||
/// This function sets up a `tracing_subscriber` that redirects logs | ||
/// to an internal buffer, which can later be queried. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use sp_tracing::test_log_capture; | ||
/// | ||
/// test_log_capture::capture(|| { | ||
/// tracing::warn!("Captured warning message"); | ||
/// }); | ||
/// | ||
/// assert!(test_log_capture::logs_contain("Captured warning message")); | ||
/// ``` | ||
pub fn capture<F: FnOnce()>(f: F) { | ||
capture_with_max_level(LevelFilter::DEBUG, f); | ||
} | ||
|
||
/// Runs a test block with logging enabled and captures logs for assertions | ||
/// while allowing the maximum log level to be specified. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use sp_tracing::test_log_capture; | ||
/// use sp_tracing::tracing::Level; | ||
/// | ||
/// test_log_capture::capture_with_max_level(Level::INFO, || { | ||
/// tracing::info!("This will be captured at INFO level"); | ||
/// tracing::debug!("This will be captured at DEBUG level"); | ||
/// }); | ||
/// | ||
/// assert!(test_log_capture::logs_contain("INFO level")); | ||
/// assert!(!test_log_capture::logs_contain("DEBUG level")); | ||
Comment on lines
+330
to
+336
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I played with this a bit and in my experiments indeed it works when I do Maybe I'm doing something wrong. But just saying in case it's worth looking into. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven’t tested with |
||
/// ``` | ||
pub fn capture_with_max_level<F: FnOnce()>(max_level: impl Into<LevelFilter>, f: F) { | ||
let log_capture = MockWriter::new(&global_buf()); | ||
Comment on lines
+338
to
+339
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally I think it would be a bit more flexible if we avoided using a closure and if we avoided using a global buffer. I would do something like:
(not sure about the naming) The current There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this approach won’t work unless we implement our own Writer, similar to what we did here: I'm okay with removing tracing-test completely if we go with this approach. |
||
let subscriber = tracing_subscriber::fmt() | ||
.with_max_level(max_level) | ||
.with_writer(log_capture) | ||
.finish(); | ||
subscriber::with_default(subscriber, f); | ||
} | ||
|
||
/// Checks whether a captured log message contains a given substring. | ||
/// | ||
/// This function retrieves all captured logs and checks if the provided | ||
/// string is present. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use sp_tracing::test_log_capture; | ||
/// | ||
/// test_log_capture::capture(|| { | ||
/// tracing::debug!("Debug log for testing"); | ||
/// }); | ||
/// | ||
/// assert!(test_log_capture::logs_contain("Debug log")); | ||
/// ``` | ||
pub fn logs_contain(value: &str) -> bool { | ||
let logs = String::from_utf8(global_buf().lock().unwrap().clone()).unwrap(); | ||
logs.contains(value) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we plan to count the number of logs in other places as well it would be worth deduplicating this logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK