Đề 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 đầuThực thiCU còn lại
1200,000-100,000100,000
2200,000-100,000100,000
3200,000-100,000100,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 đầuThực thiCU còn lại
1200,000-100,000100,000
2100,000-100,0000
30FAIL!!!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:

Feature Status Heatmap

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

  1. 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`
  1. Ở một cửa sổ lệnh khác, bạn in kết quả đầu ra:
solana logs
  1. 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

  1. 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`
  1. 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ử.

Press </> button to view full source
#[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
}

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ẻ!

Các nguồn tài liệu khác

scfsdopen in new window

gadget-scfsopen in new window

Last Updated:
Contributors: tuphan-dn