Skip to content

Commit

Permalink
tests: improve test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
Daanvdplas committed Aug 18, 2024
1 parent 6adf1b1 commit 38c35e5
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 288 deletions.
42 changes: 13 additions & 29 deletions extension/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,12 @@ use sp_std::vec::Vec;
mod tests;
mod v0;

/// Logging target for categorizing messages from the Pop API extension module.
// Logging target for categorizing messages from the Pop API extension module.
const LOG_TARGET: &str = "pop-api::extension";

const DECODING_FAILED_ERROR: DispatchError = DispatchError::Other("DecodingFailed");
// TODO: issue #93, we can also encode the `pop_primitives::Error::UnknownCall` which means we do use
// `Error` in the runtime and it should stay in primitives. Perhaps issue #91 will also influence
// here. Should be looked at together.
const DECODING_FAILED_ERROR_ENCODED: [u8; 4] = [255u8, 0, 0, 0];
const UNKNOWN_CALL_ERROR: DispatchError = DispatchError::Other("UnknownCall");
// TODO: see above.
const UNKNOWN_CALL_ERROR_ENCODED: [u8; 4] = [254u8, 0, 0, 0];

type ContractSchedule<T> = <T as pallet_contracts::Config>::Schedule;
Expand Down Expand Up @@ -248,7 +244,7 @@ enum VersionedDispatch<RuntimeCall: Decode> {
/// The `FuncId` specifies the available functions that can be called through the Pop API. Each
/// variant corresponds to a specific functionality provided by the API, facilitating the
/// interaction between smart contracts and the runtime.
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum FuncId {
/// Represents a function call to dispatch a runtime call.
Dispatch,
Expand Down Expand Up @@ -287,8 +283,17 @@ impl TryFrom<u8> for FuncId {
///
/// - `error`: The `DispatchError` encountered during contract execution.
/// - `version`: The version of the chain extension, used to determine the known errors.
pub(crate) fn convert_to_status_code(error: DispatchError, version: u8) -> u32 {
let mut encoded_error: [u8; 4] = match error {
fn convert_to_status_code(error: DispatchError, version: u8) -> u32 {
let mut encoded_error: [u8; 4] = encode_error(error);
match version {
0 => v0::handle_unknown_error(&mut encoded_error),
_ => encoded_error = UNKNOWN_CALL_ERROR_ENCODED,
}
u32::from_le_bytes(encoded_error)
}

fn encode_error(error: DispatchError) -> [u8; 4] {
match error {
// "UnknownCall" and "DecodingFailed" are mapped to specific errors in the API and will
// never change.
UNKNOWN_CALL_ERROR => UNKNOWN_CALL_ERROR_ENCODED,
Expand All @@ -299,26 +304,5 @@ pub(crate) fn convert_to_status_code(error: DispatchError, version: u8) -> u32 {
encoded_error.resize(4, 0);
encoded_error.try_into().expect("qed, resized to 4 bytes line above")
},
};
match version {
// If an unknown variant of the `DispatchError` is detected the error needs to be converted
// into the encoded value of `Error::Other`. This conversion is performed by shifting the bytes one
// position forward (discarding the last byte as it is not used) and setting the first byte to the
// encoded value of `Other` (0u8). This ensures the error is correctly categorized as an `Other`
// variant which provides all the necessary information to debug which error occurred in the runtime.
//
// Byte layout explanation:
// - Byte 0: index of the variant within `Error`
// - Byte 1:
// - Must be zero for `UNIT_ERRORS`.
// - Represents the nested error in `SINGLE_NESTED_ERRORS`.
// - Represents the first level of nesting in `DOUBLE_NESTED_ERRORS`.
// - Byte 2:
// - Represents the second level of nesting in `DOUBLE_NESTED_ERRORS`.
// - Byte 3:
// - Unused or represents further nested information.
0 => v0::handle_unknown_error(&mut encoded_error),
_ => encoded_error = UNKNOWN_CALL_ERROR_ENCODED,
}
u32::from_le_bytes(encoded_error)
}
137 changes: 123 additions & 14 deletions extension/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use super::{
encode_error, DispatchError::*, FuncId, DECODING_FAILED_ERROR, DECODING_FAILED_ERROR_ENCODED,
UNKNOWN_CALL_ERROR, UNKNOWN_CALL_ERROR_ENCODED,
};
use codec::{Decode, Encode};
use sp_runtime::{
ArithmeticError::*, DispatchError, ModuleError, TokenError::*, TransactionalError::*,
};

// Test ensuring `func_id()` and `ext_id()` work as expected, i.e. extracting the first two
// bytes and the last two bytes, respectively, from a 4 byte array.
// TODO: #110
// Test ensuring `func_id()` and `ext_id()` work as expected. I.e. extracting the first two
// bytes and the last two bytes from a 4 byte array, respectively.
#[test]
fn test_byte_extraction() {
use rand::Rng;
Expand Down Expand Up @@ -42,6 +50,112 @@ fn test_byte_extraction() {
}
}

// Assert encoded `DispatchError` with expected encoding.
pub(crate) fn assert_encoding_matches(dispatch_error: DispatchError, expected_encoding: [u8; 4]) {
let encoding = encode_error(dispatch_error);
assert_eq!(encoding, expected_encoding);
}

// Assert all unit error possibilities with expected encoding.
#[test]
fn encode_error_unit_variants_works() {
let test_cases = vec![
(Other(""), [0, 0, 0, 0]),
(CannotLookup, [1, 0, 0, 0]),
(BadOrigin, [2, 0, 0, 0]),
(ConsumerRemaining, [4, 0, 0, 0]),
(NoProviders, [5, 0, 0, 0]),
(TooManyConsumers, [6, 0, 0, 0]),
(Exhausted, [10, 0, 0, 0]),
(Corruption, [11, 0, 0, 0]),
(Unavailable, [12, 0, 0, 0]),
(RootNotAllowed, [13, 0, 0, 0]),
(UNKNOWN_CALL_ERROR, UNKNOWN_CALL_ERROR_ENCODED),
(DECODING_FAILED_ERROR, DECODING_FAILED_ERROR_ENCODED),
];
for (dispatch_error, expected_encoding) in test_cases {
assert_encoding_matches(dispatch_error, expected_encoding);
}
}

// Assert all single nested error possibilities with expected encoding.
#[test]
fn encode_error_single_nested_variants_works() {
// TokenError.
let test_cases = vec![
(FundsUnavailable, 0),
(OnlyProvider, 1),
(BelowMinimum, 2),
(CannotCreate, 3),
(UnknownAsset, 4),
(Frozen, 5),
(Unsupported, 6),
(CannotCreateHold, 7),
(NotExpendable, 8),
(Blocked, 9),
];
for (error, index) in test_cases {
assert_encoding_matches(Token(error), [7, index, 0, 0]);
}

// ArithmeticError.
let test_cases = vec![(Underflow, 0), (Overflow, 1), (DivisionByZero, 2)];
for (error, index) in test_cases {
assert_encoding_matches(Arithmetic(error), [8, index, 0, 0]);
}

// TransactionalError.
let test_cases = vec![(LimitReached, 0), (NoLayer, 1)];
for (error, index) in test_cases {
assert_encoding_matches(Transactional(error), [9, index, 0, 0]);
}
}

// Assert all module error possibilities with expected encoding.
#[test]
fn encode_error_module_error_works() {
let test_cases = vec![
(
Module(ModuleError { index: 1, error: [2, 0, 0, 0], message: Some("hallo") }),
[3, 1, 2, 0],
),
(
Module(ModuleError { index: 1, error: [2, 3, 0, 0], message: Some("hallo") }),
[3, 1, 2, 3],
),
(
Module(ModuleError { index: 1, error: [2, 3, 4, 0], message: Some("hallo") }),
[3, 1, 2, 3],
),
(
Module(ModuleError { index: 1, error: [2, 3, 4, 5], message: Some("hallo") }),
[3, 1, 2, 3],
),
(Module(ModuleError { index: 1, error: [2, 3, 4, 5], message: None }), [3, 1, 2, 3]),
];
for (dispatch_error, expected_encoding) in test_cases {
let encoding = encode_error(dispatch_error);
assert_eq!(encoding, expected_encoding);
}
}

#[test]
fn func_id_try_from_works() {
let test_cases = [
(0u8, Ok(FuncId::Dispatch)),
(1, Ok(FuncId::ReadState)),
(2, Err(UNKNOWN_CALL_ERROR)),
(3, Err(UNKNOWN_CALL_ERROR)),
(100, Err(UNKNOWN_CALL_ERROR)),
(u8::MAX, Err(UNKNOWN_CALL_ERROR)),
];

for (input_value, expected_result) in test_cases {
let actual_result: Result<FuncId, DispatchError> = input_value.try_into();
assert_eq!(actual_result, expected_result, "Failed on input: {}", input_value);
}
}

// Test showing all the different type of variants and its encoding.
#[test]
fn encoding_of_enum() {
Expand Down Expand Up @@ -103,31 +217,26 @@ fn encoding_of_enum() {

#[test]
fn encoding_decoding_dispatch_error() {
use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError};

let error = DispatchError::Module(ModuleError {
index: 255,
error: [2, 0, 0, 0],
message: Some("error message"),
});
let error =
Module(ModuleError { index: 255, error: [2, 0, 0, 0], message: Some("error message") });
let encoded = error.encode();
let decoded = DispatchError::decode(&mut &encoded[..]).unwrap();
assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]);
assert_eq!(
decoded,
// `message` is skipped for encoding.
DispatchError::Module(ModuleError { index: 255, error: [2, 0, 0, 0], message: None })
Module(ModuleError { index: 255, error: [2, 0, 0, 0], message: None })
);

// Example DispatchError::Token
let error = DispatchError::Token(TokenError::UnknownAsset);
// Example Token
let error = Token(UnknownAsset);
let encoded = error.encode();
let decoded = DispatchError::decode(&mut &encoded[..]).unwrap();
assert_eq!(encoded, vec![7, 4]);
assert_eq!(decoded, error);

// Example DispatchError::Arithmetic
let error = DispatchError::Arithmetic(ArithmeticError::Overflow);
// Example Arithmetic
let error = Arithmetic(Overflow);
let encoded = error.encode();
let decoded = DispatchError::decode(&mut &encoded[..]).unwrap();
assert_eq!(encoded, vec![8, 1]);
Expand Down
Loading

0 comments on commit 38c35e5

Please sign in to comment.