Fonctionnalités Avancées
Mollusk offre des options d'initialisation flexibles pour s'adapter à différents scénarios de test. Vous pouvez créer des instances préchargées avec votre programme ou démarrer avec un environnement minimal et ajouter des composants selon vos besoins.
Lorsque vous testez un programme en particulier, lancez Mollusk avec votre programme pré-chargé :
use mollusk_svm::Mollusk;
use solana_sdk::pubkey::Pubkey;
const ID: Pubkey = solana_sdk::pubkey!("22222222222222222222222222222222222222222222");
#[test]
fn test() {
let mollusk = Mollusk::new(&ID, "target/deploy/program");
}
Cette approche charge automatiquement votre programme compilé et le rend disponible pour les tests, ce qui simplifie le processus de configuration des suites de tests spécifiques au programme.
Pour des scénarios de test plus larges ou lorsque vous devez ajouter des programmes de manière dynamique, commencez par l'instance par défaut :
use mollusk_svm::Mollusk;
#[test]
fn test() {
// System Program, ...
let mollusk = Mollusk::default();
}
L'instance par défaut comprend des programmes intégrés essentiels tels que le programme système qui constituent la base de la plupart des opérations sur Solana sans surcharge liée aux programmes dont vous n'avez pas besoin.
Lorsque vos tests nécessitent le programme système, Mollusk fournit des fonctions d'aide pour générer les références de compte nécessaires :
let (system_program, system_program_account) = keyed_account_for_system_program();
Pour reproduire la fonctionnalité de chargement de programme ou pour charger des programmes personnalisés qui ne sont pas présents par défaut, vous pouvez utiliser ces fonctions d'aides :
use mollusk_svm::Mollusk;
use mollusk_svm::program::create_program_account_loader_v3;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Get the account that you need
let program = &ID; // ID of the program we're trying to load into mollusk
let program_account = create_program_account_loader_v3(&ID);
// Load the program into your mollusk instance
mollusk.add_program(
&ID,
"target/deploy/program",
&mollusk_svm::program::loader_keys::LOADER_V3
);
}
Programme de Jetons
Les fonctions d'aide pour le programme de jetons de Mollusk simplifient considérablement les scénarios de test impliquant des jetons SPL. La crate mollusk-svm-programs-token
fournit une prise en charge préconfigurée pour les programmes Token
, Token2022
et Associated Token
.
Après avoir inclus la crate d'aide aux jetons, ajoutez les programmes de jetons spécifiques requis par vos tests :
use mollusk_svm::Mollusk;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Add the SPL Token Program
mollusk_svm_programs_token::token::add_program(&mut mollusk);
// Add the Token2022 Program
mollusk_svm_programs_token::token2022::add_program(&mut mollusk);
// Add the Associated Token Program
mollusk_svm_programs_token::associated_token::add_program(&mut mollusk);
}
Et créez les références de compte nécessaires pour vos scénarios de test :
// SPL Token Program
let (token_program, token_program_account) =
mollusk_svm_programs_token::token::keyed_account();
// Token2022 Program
let (token2022_program, token2022_program_account) =
mollusk_svm_programs_token::token2022::keyed_account();
// Associated Token Program
let (associated_token_program, associated_token_program_account) =
mollusk_svm_programs_token::associated_token::keyed_account();
Ces aides garantissent que les tests liés aux jetons ont accès aux comptes de programme corrects avec une configuration appropriée, ce qui permet de tester de manière exhaustive les opérations liées aux jetons sans configuration manuelle du programme.
Si nous voulions ensuite créer des comptes de State
à partir du programme de jetons déjà initialisés, nous devrions les créer à l'aide de la méthode Account
et les charger dans la structure de compte de l'instance Mollusk.
La création manuelle de comptes de jetons initialisés implique un travail fastidieux pour sérialiser les données des comptes et calculer l'exonération de rente. Voici des fonctions d'aide qui simplifient ce processus :
use spl_token::{state::{Mint, Account as TokenAccount, AccountState}, ID as token};
use spl_associated_token_account::get_associated_token_address_with_program_id;
// Create a Keyed Account for a Mint with default data
#[allow(dead_code)]
pub fn keyed_account_for_mint_default(
mollusk: &Mollusk,
authority: &Pubkey,
decimals: u8,
pubkey: Option<Pubkey>,
token_program: Option<Pubkey>,
) -> (Pubkey, Account) {
let mint_data = Mint {
mint_authority: Some(*authority).into(),
supply: 0,
decimals,
is_initialized: true,
freeze_authority: None.into(),
};
let mut data = vec![0u8; Mint::LEN];
Mint::pack(mint_data, &mut data).unwrap();
let account = Account {
lamports: mollusk.sysvars.rent.minimum_balance(Mint::LEN),
data,
owner: token_program.unwrap_or(token::ID),
executable: false,
rent_epoch: 0,
};
(pubkey.unwrap_or(Pubkey::new_unique()), account)
}
// Create a Keyed Account for a Token Account with default data
#[allow(dead_code)]
pub fn keyed_account_for_token_account_default(
mollusk: &Mollusk,
mint: &Pubkey,
owner: &Pubkey,
amount: u64,
pubkey: Option<Pubkey>,
token_program: Option<Pubkey>,
) -> (Pubkey, Account) {
let account_data = TokenAccount {
mint: *mint,
owner: *owner,
amount,
delegate: None.into(),
state: AccountState::Initialized,
is_native: None.into(),
delegated_amount: 0,
close_authority: None.into(),
};
let mut data = vec![0u8; TokenAccount::LEN];
TokenAccount::pack(account_data, &mut data).unwrap();
let account = Account {
lamports: mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN),
data,
owner: token_program.unwrap_or(token::ID),
executable: false,
rent_epoch: 0,
};
(pubkey.unwrap_or(Pubkey::new_unique()), account)
}
// Create a Keyed Account for an Associated Token Account with default data
#[allow(dead_code)]
pub fn keyed_account_for_associated_token_account_default(
mollusk: &Mollusk,
mint: &Pubkey,
owner: &Pubkey,
amount: u64,
token_program: Option<Pubkey>,
) -> (Pubkey, Account) {
let associated_token_address = get_associated_token_address_with_program_id(
owner,
mint,
&token_program.unwrap_or(token::ID),
);
keyed_account_for_token_account_default(
mollusk,
mint,
owner,
amount,
Some(associated_token_address),
Some(token_program.unwrap_or(token::ID)),
)
}
Benchmark des Unités de Calcul
Mollusk comprend un système dédié de benchmarking des unités de calcul (Compute Units) qui permet de mesurer et de suivre avec précision l'efficacité de calcul de votre programme. MolluskComputeUnitBencher
fournit une API simplifiée pour créer des benchmarks complets qui surveillent la consommation des unités de calcul dans différents scénarios d'instructions.
Ce système de benchmarking est particulièrement utile pour l'optimisation des performances, car il génère des rapports détaillés indiquant à la fois l'utilisation actuelle des unités de calcul et les écarts par rapport aux exécutions précédentes.
Cela vous permet de voir immédiatement l'impact des modifications apportées au code sur l'efficacité de votre programme, ce qui vous aide à optimiser les goulots d'étranglement critiques en matière de performances.
Le bencher s'intègre parfaitement à votre configuration de test Mollusk existante :
use {
mollusk_svm_bencher::MolluskComputeUnitBencher,
mollusk_svm::Mollusk,
/* ... */
};
// Optionally disable logging.
solana_logger::setup_with("");
/* Instruction & accounts setup ... */
let mollusk = Mollusk::new(&program_id, "my_program");
MolluskComputeUnitBencher::new(mollusk)
.bench(("bench0", &instruction0, &accounts0))
.bench(("bench1", &instruction1, &accounts1))
.bench(("bench2", &instruction2, &accounts2))
.bench(("bench3", &instruction3, &accounts3))
.must_pass(true)
.out_dir("../target/benches")
.execute();
Options de Configuration
Le bencher offre plusieurs options de configuration :
must_pass(true)
: Déclenche une alerte si un test de performance échoue, garantissant ainsi la validité de vos tests de performance malgré les modifications apportées au codeout_dir("../target/benches")
: Spécifie l'emplacement où le rapport sera généré, permettant l'intégration avec les systèmes CI/CD et les workflows de documentation.
Intégration avec Cargo
Pour exécuter des benchmarks à l'aide de cargo bench
, ajoutez une configuration de benchmark à votre fichier Cargo.toml
:
[[bench]]
name = "compute_units"
harness = false
Rapports de Benchmark
Le bencher génère des rapports markdown qui fournissent à la fois les indicateurs de performance actuels et une comparaison historique :
| Name | CUs | Delta |
|--------|-------|--------|
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
Le format du rapport comprend :
- Name: L'identifiant de benchmark que vous avez spécifié
- CUs: Consommation d'unités de calcul actuelle pour ce scénario
- Delta: Changement par rapport à l'exécution précédente du benchmark (une valeur positive indique une augmentation de l'utilisation, une valeur négative indique une optimisation)
Syscalls Personnalisés
Mollusk prend en charge la création et le test de syscalls personnalisés, vous permettant d'étendre la Machine Virtuelle Solana avec des fonctionnalités spécialisées pour les scénarios de test.
Cette fonctionnalité est particulièrement utile pour tester la création de nouveaux Syscall pouvant être ajoutés via SIMD en simulant des comportements d'exécution spécifiques ou en créant des environnements contrôlés pour les tests.
Définition de Syscalls Personnalisés
Les syscalls personnalisés sont définis à l'aide de la macro declare_builtin_function!
, qui crée un syscall pouvant être enregistré dans l'environnement d'exécution de Mollusk :
use {
mollusk_svm::{result::Check, Mollusk},
solana_instruction::Instruction,
solana_program_runtime::{
invoke_context::InvokeContext,
solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping},
},
solana_pubkey::Pubkey,
};
declare_builtin_function!(
/// A custom syscall to burn compute units for testing
SyscallBurnCus,
fn rust(
invoke_context: &mut InvokeContext,
to_burn: u64,
_arg2: u64,
_arg3: u64,
_arg4: u64,
_arg5: u64,
_memory_mapping: &mut MemoryMapping,
) -> Result<u64, Box<dyn std::error::Error>> {
// Consume the specified number of compute units
invoke_context.consume_checked(to_burn)?;
Ok(0)
}
);
La signature de la fonction syscall suit un modèle précis :
invoke_context
: Fournit l'accès au contexte d'exécution et à l'état d'exécution- Arguments 1-5: Jusqu'à cinq arguments de 64 bits peuvent être passés depuis le programme
- memory_mapping: Permet d'accéder à l'espace mémoire du programme.
- Return value: Un
Result<u64, Box<dyn std::error::Error>>
indiquant le succès ou l'échec
Enregistrement de Syscalls Personnalisés
Une fois définis, les syscalls personnalisés doivent être enregistrés dans l'environnement d'exécution du programme de Mollusk avant de pouvoir être utilisés :
#[test]
fn test_custom_syscall() {
std::env::set_var("SBF_OUT_DIR", "../target/deploy");
let program_id = Pubkey::new_unique();
let mollusk = {
let mut mollusk = Mollusk::default();
// Register the custom syscall with a specific name
mollusk
.program_cache
.program_runtime_environment
.register_function("sol_burn_cus", SyscallBurnCus::vm)
.unwrap();
// Add your program that uses the custom syscall
mollusk.add_program(
&program_id,
"test_program_custom_syscall",
&mollusk_svm::program::loader_keys::LOADER_V3,
);
mollusk
};
}
Le syscall est enregistré avec un nom (dans cet exemple "sol_burn_cus") auquel votre programme peut faire référence lorsqu'il effectue le syscall.
Test du Comportement des Syscalls Personnalisés
Les syscalls personnalisés peuvent être testés comme n'importe quelle autre fonctionnalité du programme, avec l'avantage supplémentaire d'un contrôle précis sur leur comportement :
fn instruction_burn_cus(program_id: &Pubkey, to_burn: u64) -> Instruction {
Instruction::new_with_bytes(*program_id, &to_burn.to_le_bytes(), vec![])
}
#[test]
fn test_custom_syscall() {
// ... mollusk setup ...
// Establish baseline compute unit usage
let base_cus = mollusk
.process_and_validate_instruction(
&instruction_burn_cus(&program_id, 0),
&[],
&[Check::success()],
)
.compute_units_consumed;
// Test different compute unit consumption levels
for to_burn in [100, 1_000, 10_000] {
mollusk.process_and_validate_instruction(
&instruction_burn_cus(&program_id, to_burn),
&[],
&[
Check::success(),
Check::compute_units(base_cus + to_burn), // Verify exact CU consumption
],
);
}
}
Méthodes de Configuration
Mollusk offre des options de configuration complètes qui vous permettent de personnaliser l'environnement d'exécution afin de répondre à des exigences de test précises, comme le montre le Context
de Mollusk :
/// Instruction context fixture.
pub struct Context {
/// The compute budget to use for the simulation.
pub compute_budget: ComputeBudget,
/// The feature set to use for the simulation.
pub feature_set: FeatureSet,
/// The runtime sysvars to use for the simulation.
pub sysvars: Sysvars,
/// The program ID of the program being invoked.
pub program_id: Pubkey,
/// Accounts to pass to the instruction.
pub instruction_accounts: Vec<AccountMeta>,
/// The instruction data.
pub instruction_data: Vec<u8>,
/// Input accounts with state.
pub accounts: Vec<(Pubkey, Account)>,
}
Ces méthodes de configuration permettent un contrôle précis des budgets de calcul, de la disponibilité des fonctionnalités et des variables système, ce qui permet de tester les programmes dans diverses conditions d'exécution.
Configuration de Base
use mollusk_svm::Mollusk;
use solana_sdk::feature_set::FeatureSet;
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Configure compute budget for performance testing
mollusk.set_compute_budget(200_000);
// Configure feature set to enable/disable specific Solana features
mollusk.set_feature_set(FeatureSet::all_enabled());
// Sysvars are handled automatically but can be customized if needed
}
Le budget de calcul détermine le nombre d'unités de calcul disponibles pour l'exécution du programme. Ceci est crucial pour tester les programmes qui approchent ou dépassent les limites de calcul :
// Test with standard compute budget
mollusk.set_compute_budget(200_000);
L'ensemble de fonctionnalités de Solana contrôle quelles fonctionnalités de la blockchain sont actives pendant l'exécution du programme. Mollusk vous permet de configurer ces fonctionnalités afin de tester la compatibilité entre différents états du réseau :
use solana_sdk::feature_set::FeatureSet;
// Enable all features (latest functionality)
mollusk.set_feature_set(FeatureSet::all_enabled());
// All features disabled
mollusk.set_feature_set(FeatureSet::default());
Pour obtenir la liste complète des fonctionnalités disponibles, consultez la documentation de la crate agave-feature-set
qui détaille toutes les fonctionnalités configurables de la blockchain et leurs implications.
Mollusk donne accès à toutes les variables système (sysvars) que les programmes peuvent interroger pendant leur exécution. Bien que ces paramètres soient automatiquement configurés avec des valeurs par défaut raisonnables, vous pouvez les personnaliser pour des scénarios de test précis :
/// Mollusk sysvars wrapper for easy manipulation
pub struct Sysvars {
pub clock: Clock, // Current slot, epoch, and timestamp
pub epoch_rewards: EpochRewards, // Epoch reward distribution info
pub epoch_schedule: EpochSchedule, // Epoch timing and slot configuration
pub last_restart_slot: LastRestartSlot, // Last validator restart information
pub rent: Rent, // Rent calculation parameters
pub slot_hashes: SlotHashes, // Recent slot hash history
pub stake_history: StakeHistory, // Historical stake activation data
}
Vous pouvez personnaliser certains sysvars pour tester la logique dépendante du temps, les calculs de rente ou d'autres comportements dépendants du système, ou utiliser certaines fonctions d'aide :
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Customize clock for time-based testing
mollusk.sysvars.clock.epoch = 10;
mollusk.sysvars.clock.unix_timestamp = 1234567890;
// Jump to Slot 1000
warp_to_slot(&mut Mollusk, 1000)
}