Đề xuất Kiểm thử Parity
Khi kiểm thử chương trình, chúng ta muốn chắc rằng chương trình sẽ chạy như nhau trên mọi môi trường nhằm đảm bảo về cả chất lượng sản phẩm cũng như ta tạo giá trị kỳ vọng.
Có thể bạn chưa biết
Những điều có thể bạn chưa biết
- Đề xuất là không bắt buộc cho các validator trên Solana và cần được validator kích hoạt để có thể được sử dụng.
- Đề xuất có thể được kích hoạt bởi một mạng (ví dụ như testnet) trong khi vẫn vô hiệu trên mạng khác (ví dụ như mainnet-beta).
- Tuy nhiên, khi chạy chế độ mặc định
solana-test-validator
dưới máy, tất cả các đề xuất sẽ được tự động kích hoặt và sẵn sàng thực thi. Kết quả là khi kiểm thử trên máy có thể sẽ sai khác với khi triển khai chương trình và kiểm thử trên các mạng Solana khác!
Tình huống giả định
Giả sử bạn có một Transaction chưa 3 chỉ thị và mỗi chỉ thị sẽ tiêu tốn xấp xỉ khoảng 100,000 đơn vị tính toán (CU). Khi chạy trên phiên bản Solana 1.8.x, bạn sẽ thấy mức tiêu thụ CU của các chỉ thị như sau:
Chỉ thị | CU lúc bắt đầu | Thực thi | CU còn lại |
---|---|---|---|
1 | 200,000 | -100,000 | 100,000 |
2 | 200,000 | -100,000 | 100,000 |
3 | 200,000 | -100,000 | 100,000 |
Trong phiên bản Solana 1.9.2, có một đề xuất được gọi là 'transaction wide compute cap'. Để xuất này nói rằng một Transaction bắt đầu với 200,000 CU mặc định và tất cả các chỉ thị trong Transaction sẽ tiêu thu cộng dồn ngân sách CU đó. Thử chạy lại cùng Transaction nhưng với phiên bản mới sẽ cho ra kết quả rất khác:
Chỉ thị | CU lúc bắt đầu | Thực thi | CU còn lại |
---|---|---|---|
1 | 200,000 | -100,000 | 100,000 |
2 | 100,000 | -100,000 | 0 |
3 | 0 | FAIL!!! | FAIL!!! |
Vãi! Nếu bạn không biết cái này thì có khả năng cao là bạn sẽ cảm thấy cực dị khi mà bạn chả thay đổi gì trong Transction nhưng devnet thì hoạt động ngon lành còn trên máy thì toàn là lỗi?!?
Thực ra vẫn có các để tăng ngân sách CU cho một Transaction, giả dụ như là 300,000 CU, để làm giải pháp tình thế. Nhưng điều đó cho thấy đề xuất Kiểm thử Parity sẽ cho phép bạn chủ động tránh những phiền hà trên.
There is the ability to increase the overall Transaction budget, to lets say 300_000 CU, and salvage your sanity but this demonstrates why testing with Feature Parity provides a proactive way to avoid any confusion.
Đề xuất Trạng thái
Rất dễ để kiểm tra những đề xuất nào đang được kích hoạt cho từng môi trường với câu lệnh solana feature status
.
solana feature status -ud // Displays by feature status for devnet
solana feature status -ut // Displays for testnet
solana feature status -um // Displays for mainnet-beta
solana feature status -ul // Displays for local, requires running solana-test-validator
Khác hơn, bạn cũng có thể sử dụng công cụ scfsd để quan sát cùng lúc trạng thái của tất cả các môi trường. Bên dưới là một phần kết quả trả ra, bạn cũng không cần solana-test-validator
để chạy công cụ trên:
Kiểm thử Parity
Như đã lưu ý bên trên, solana-test-validator
sẽ tự động kích hoạt tất cả các để xuất. Để trả lời cho câu hỏi "Làm thế nào tôi có thể kiểm thử chương trình dưới máy với môi trường địa phương như là môi trường devnet, testnet, hay kể cả mainnet-beta?".
Lời đáp: Một cài đặt trong phiên bản Solana 1.9.6 cho phép bạn vô hiệu hoá các đề xuất:
solana-test-validator --deactivate-feature <FEATURE_PUBKEY> ...
Ví dụ đơn giản
Giả sử bạn có một chương trình đơn giản chỉ in ra những gì nó nhận. Bạn đa kiểm thử một Transaction với 2 chỉ thị cho chường trình đó.
Khi tất cả các đề xuất đều kích hoạt
- Bạn khởi chạy một validator trên của sổ lệnh:
solana config set -ul
solana-test-validator -l ./ledger --bpf-program target/deploy/PROGNAME.so --reset`
- Ở một cửa sổ lệnh khác, bạn in kết quả đầu ra:
solana logs
- Sau đó bạn thử gửi một Transaction. Bạn sẽ thấy kết quả in ra ở cửa sổ lệnh tương tự như sau (đã được điều chỉnh để dễ đọc hơn):
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[1]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 187157 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success[
Bởi vì đề xuất 'transaction wide compute cap' được mặc định tự động bật, bạn sẽ thấy rằng mỗi chỉ thị sẽ trừ tiếp vào CU từ ngân sách 200,000 CU cho một Transaction từ đầu.
Vô hiệu một vài đề xuất
- Trong lần này, chúng ta muốn chạy với cơ chế quản lý CU giống y với chạy trên devnet. Sử dụng công cụ được mô tả trong Feature Status để vô hiệu hoá
transaction wide compute cap
và gán cờ--deactivate-feature
trong lúc khởi chạy validator.
solana-test-validator -l ./ledger --deactivate-feature 5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9 --bpf-program target/deploy/PROGNAME.so --reset`
- Giờ chúng ta sẽ thấy kết quả trả ra cho từng chỉ thị sẽ có ngân sách CU riêng và bằng 200,000 CU (đã được điều chỉnh để dễ dọc) và cũng chính là cài đặt hiện hành trên các môi trường khác.
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc invoke [1]
Program log: process_instruction: PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc: 0 accounts, data=[0]
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc consumed 12843 of 200000 compute units
Program PWDnx8LkjJUn9bAVzG6Fp6BuvB41x7DkBZdo9YLMGcc success
Kiểm thử Parity Đầy đủ
Bạn có thể cài đặt Parity Đầy đủ với một môi trường cụ thể bằng cách chỉ rõ đề xuất nào sẽ bị vô hiệu với cờ --deactivate-feature <FEATURE_PUBKEY>
cho mỗi lần chạy solana-test-validator
:
solana-test-validator --deactivate-feature PUBKEY_1 --deactivate-feature PUBKEY_2 ...
Cách khác, scfsd có cung cấp các câu lệnh để chuyển đổi để giúp nhanh chóng vô hiệu hoá một tập các đề xuất cho một môi trường và truyền vào trực tiếp lúc solana-test-validator
khởi chạy:
solana-test-validator -l ./.ledger $(scfsd -c devnet -k -t)
Nếu bạn mở một cửa sổ lệnh khác và chạy solana feature status
trong khi validator đang chạy thì bạn sẽ thấy các đề xuất bị tắt sẽ giống với cài đặt đè xuất trên môi trường devnet.
Cài đặt tự động cho Kiểm thử Parity Đầy đủ
Với những lập trình viên hay kiểm soát quá trình khởi chạy validator để kiểm thử bằng code, việc chỉnh sửa các cài đặt đề xuất cho validator là khả thi với TestValidatorGenesis. Với phiên bản Solana 1.9.6, một chức năng đã được thêm vào trong validator để hỗ trợ điều đó.
Tại thư mục gố của chương trình, tạo một thư mục với tên tests
và thêm tập tin parity_test.rs
. Bên dưới là một vài hàm mẫu bạn có thể sử dụng chúng cho mỗi bài kiểm thử.
#[cfg(test)]
mod tests {
use std::{error, path::PathBuf, str::FromStr};
// Use gadget-scfs to get interegate feature lists from clusters
// must have `gadgets-scfs = "0.2.0" in Cargo.toml [dev-dependencies] to use
use gadgets_scfs::{ScfsCriteria, ScfsMatrix, SCFS_DEVNET};
use solana_client::rpc_client::RpcClient;
use solana_program::{instruction::Instruction, message::Message, pubkey::Pubkey};
use solana_sdk::{
// Added in Solana 1.9.2
compute_budget::ComputeBudgetInstruction,
pubkey,
signature::{Keypair, Signature},
signer::Signer,
transaction::Transaction,
};
// Extended in Solana 1.9.6
use solana_test_validator::{TestValidator, TestValidatorGenesis};
/// Location/Name of ProgramTestGenesis ledger
const LEDGER_PATH: &str = "./.ledger";
/// Path to BPF program (*.so) change if needed
const PROG_PATH: &str = "target/deploy/";
/// Program name from program Cargo.toml
/// FILL IN WITH YOUR PROGRAM_NAME
const PROG_NAME: &str = "PROGRAM_NAME";
/// Program public key
/// FILL IN WITH YOUR PROGRAM'S PUBLIC KEY str
const PROG_KEY: Pubkey = pubkey!("PROGRAMS_PUBLIC_KEY_BASE58_STRING");
/// 'transaction wide compute cap' public key
const TXWIDE_LIMITS: Pubkey = pubkey!("5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9");
/// Setup the test validator passing features
/// you want to deactivate before running transactions
pub fn setup_validator(
invalidate_features: Vec<Pubkey>,
) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
// Extend environment variable to include our program location
std::env::set_var("BPF_OUT_DIR", PROG_PATH);
// Instantiate the test validator
let mut test_validator = TestValidatorGenesis::default();
// Once instantiated, TestValidatorGenesis configuration functions follow
// a builder pattern enabling chaining of settings function calls
let (test_validator, kp) = test_validator
// Set the ledger path and name
// maps to `solana-test-validator --ledger <DIR>`
.ledger_path(LEDGER_PATH)
// Load our program. Ignored if reusing ledger
// maps to `solana-test-validator --bpf-program <ADDRESS_OR_PATH BPF_PROGRAM.SO>`
.add_program(PROG_NAME, PROG_KEY)
// Identify features to deactivate. Ignored if reusing ledger
// maps to `solana-test-validator --deactivate-feature <FEATURE_PUBKEY>`
.deactivate_features(&invalidate_features)
// Start the test validator
.start();
Ok((test_validator, kp))
}
/// Convenience function to remove existing ledger before TestValidatorGenesis setup
/// maps to `solana-test-validator ... --reset`
pub fn clean_ledger_setup_validator(
invalidate_features: Vec<Pubkey>,
) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
if PathBuf::from_str(LEDGER_PATH).unwrap().exists() {
std::fs::remove_dir_all(LEDGER_PATH).unwrap();
}
setup_validator(invalidate_features)
}
/// Submits a transaction with programs instruction
/// Boiler plate
fn submit_transaction(
rpc_client: &RpcClient,
wallet_signer: &dyn Signer,
instructions: Vec<Instruction>,
) -> Result<Signature, Box<dyn std::error::Error>> {
let mut transaction =
Transaction::new_unsigned(Message::new(&instructions, Some(&wallet_signer.pubkey())));
let recent_blockhash = rpc_client
.get_latest_blockhash()
.map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
transaction
.try_sign(&vec![wallet_signer], recent_blockhash)
.map_err(|err| format!("error: failed to sign transaction: {}", err))?;
let signature = rpc_client
.send_and_confirm_transaction(&transaction)
.map_err(|err| format!("error: send transaction: {}", err))?;
Ok(signature)
}
// UNIT TEST FOLLOWS
}
/// Setup the test validator passing features
/// you want to deactivate before running transactions
pub fn setup_validator(
invalidate_features: Vec<Pubkey>,
) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
// Extend environment variable to include our program location
std::env::set_var("BPF_OUT_DIR", PROG_PATH);
// Instantiate the test validator
let mut test_validator = TestValidatorGenesis::default();
// Once instantiated, TestValidatorGenesis configuration functions follow
// a builder pattern enabling chaining of settings function calls
let (test_validator, kp) = test_validator
// Set the ledger path and name
// maps to `solana-test-validator --ledger <DIR>`
.ledger_path(LEDGER_PATH)
// Load our program. Ignored if reusing ledger
// maps to `solana-test-validator --bpf-program <ADDRESS_OR_PATH BPF_PROGRAM.SO>`
.add_program(PROG_NAME, PROG_KEY)
// Identify features to deactivate. Ignored if reusing ledger
// maps to `solana-test-validator --deactivate-feature <FEATURE_PUBKEY>`
.deactivate_features(&invalidate_features)
// Start the test validator
.start();
Ok((test_validator, kp))
}
/// Convenience function to remove existing ledger before TestValidatorGenesis setup
/// maps to `solana-test-validator ... --reset`
pub fn clean_ledger_setup_validator(
invalidate_features: Vec<Pubkey>,
) -> Result<(TestValidator, Keypair), Box<dyn error::Error>> {
if PathBuf::from_str(LEDGER_PATH).unwrap().exists() {
std::fs::remove_dir_all(LEDGER_PATH).unwrap();
}
setup_validator(invalidate_features)
}
/// Submits a transaction with programs instruction
/// Boiler plate
fn submit_transaction(
rpc_client: &RpcClient,
wallet_signer: &dyn Signer,
instructions: Vec<Instruction>,
) -> Result<Signature, Box<dyn std::error::Error>> {
let mut transaction =
Transaction::new_unsigned(Message::new(&instructions, Some(&wallet_signer.pubkey())));
let recent_blockhash = rpc_client
.get_latest_blockhash()
.map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
transaction
.try_sign(&vec![wallet_signer], recent_blockhash)
.map_err(|err| format!("error: failed to sign transaction: {}", err))?;
let signature = rpc_client
.send_and_confirm_transaction(&transaction)
.map_err(|err| format!("error: send transaction: {}", err))?;
Ok(signature)
}
Chúng ta có thể thêm các bài kiểm thử bên trong mod test {...}
để chạy với cài đặt mặc định của validator (với tất cả các đề xuất được kích hoạt) và sau đó có thể vô hiệu hoá đề xuất transaction wide compute cap
với mỗi ví dụ chạy solana-test-validator
bằng lệnh.
#[test]
fn test_base_pass() {
// Run with all features activated (default for TestValidatorGenesis)
let inv_feat = vec![];
// Start validator with clean (new) ledger
let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
// Get the RpcClient
let connection = test_validator.get_rpc_client();
// Capture our programs log statements
solana_logger::setup_with_default("solana_runtime::message=debug");
// This example doesn't require sending any accounts to program
let accounts = &[];
// Build instruction array and submit transaction
let txn = submit_transaction(
&connection,
&main_payer,
// Add two (2) instructions to transaction to demonstrate
// that each instruction CU draws down from default Transaction CU (200_000)
// Replace with instructions that make sense for your program
[
Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
]
.to_vec(),
);
assert!(txn.is_ok());
}
#[test]
fn test_deactivate_tx_cu_pass() {
// Run with all features activated except 'transaction wide compute cap'
let inv_feat = vec![TXWIDE_LIMITS];
// Start validator with clean (new) ledger
let (test_validator, main_payer) = clean_ledger_setup_validator(inv_feat).unwrap();
// Get the RpcClient
let connection = test_validator.get_rpc_client();
// Capture our programs log statements
solana_logger::setup_with_default("solana_runtime::message=debug");
// This example doesn't require sending any accounts to program
let accounts = &[];
// Build instruction array and submit transaction
let txn = submit_transaction(
&connection,
&main_payer,
[
// This instruction adds CU to transaction budget (1.9.2) but does nothing
// when we deactivate the 'transaction wide compute cap' feature
ComputeBudgetInstruction::request_units(400_000u32),
// Add two (2) instructions to transaction
// Replace with instructions that make sense for your program
// You will see that each instruction has the 1.8.x 200_000 CU per budget
Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
]
.to_vec(),
);
assert!(txn.is_ok());
}
Ngoài ra, scfs engine gadget có thể tạo ra một vec-tơ hoàn chỉnh để vô hiệu hoá các đề xuất cho một môi trường cụ thể. Ví dụ bên dưới cho thấy cơ chế để liệt kê toàn bộ các để xuất đã bị vô hiệu trên devnet.
#[test]
fn test_devnet_parity_pass() {
// Use gadget-scfs to get all deactivated features from devnet
// must have `gadgets-scfs = "0.2.0" in Cargo.toml to use
// Here we setup for a run that samples features only
// from devnet
let mut my_matrix = ScfsMatrix::new(Some(ScfsCriteria {
clusters: Some(vec![SCFS_DEVNET.to_string()]),
..Default::default()
}))
.unwrap();
// Run the sampler matrix
assert!(my_matrix.run().is_ok());
// Get all deactivated features
let deactivated = my_matrix
.get_features(Some(&ScfsMatrix::any_inactive))
.unwrap();
// Confirm we have them
assert_ne!(deactivated.len(), 0);
// Setup test validator and logging while deactivating all
// features that are deactivated in devnet
let (test_validator, main_payer) = clean_ledger_setup_validator(deactivated).unwrap();
let connection = test_validator.get_rpc_client();
solana_logger::setup_with_default("solana_runtime::message=debug");
let accounts = &[];
let txn = submit_transaction(
&connection,
&main_payer,
[
// Add two (2) instructions to transaction
// Replace with instructions that make sense for your program
Instruction::new_with_borsh(PROG_KEY, &0u8, accounts.to_vec()),
Instruction::new_with_borsh(PROG_KEY, &1u8, accounts.to_vec()),
]
.to_vec(),
);
assert!(txn.is_ok());
}
Chúc bạn có thời gian kiểm thử vui vẻ!