Skip to content

Commit

Permalink
docs: Add mollusk docs (#3530)
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec authored Feb 4, 2025
1 parent b470788 commit 6909a0c
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"---Core Concepts---",
"basics",
"clients",
"testing",
"features",

"---SPL Tokens---",
Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/quickstart/local.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ describe("my-project", () => {
</Accordions>

If you prefer Rust for testing, initialize your project with the
`--test-template rust` flag.
`--test-template rust` ([Anchor Rust client](/docs/clients/rust)) or
`--test-template mollusk` ([Mollusk test library](/docs/testing/mollusk)) flag.

```shell
anchor init --test-template rust my-program
Expand Down
7 changes: 7 additions & 0 deletions docs/content/docs/testing/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Testing
description:
Learn how to test Anchor programs using various test frameworks in TypeScript
and Rust.
index: true
---
4 changes: 4 additions & 0 deletions docs/content/docs/testing/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Testing Libraries",
"pages": ["mollusk"]
}
338 changes: 338 additions & 0 deletions docs/content/docs/testing/mollusk.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
---
title: Mollusk
description: Write tests for Solana programs in Rust using Mollusk.
---

[Mollusk](https://github.com/anza-xyz/mollusk) is a lightweight test harness for
Solana programs. It provides a simple interface for testing Solana program
executions in a minified Solana Virtual Machine (SVM) environment.

```rust
mollusk.process_and_validate_instruction(
&instruction, // <-- Instruction to test
&accounts, // <-- Account states
&checks, // <-- Checks to run on the instruction result
);
```

It does not create any semblance of a validator runtime, but instead provisions
a program execution pipeline directly from lower-level SVM components.

In summary, the main processor - `process_instruction` - creates minified
instances of Agave's program cache, transaction context, and invoke context. It
uses these components to directly execute the provided program's ELF using the
BPF Loader.

Because it does not use AccountsDB, Bank, or any other large Agave components,
the harness is exceptionally fast. However, it does require the user to provide
an explicit list of accounts to use, since it has nowhere to load them from.

The test environment can be further configured by adjusting the compute budget,
feature set, or sysvars. These configurations are stored directly on the test
harness (the `Mollusk` struct), but can be manipulated through a handful of
helpers.

Four main API methods are offered:

- `process_instruction`: Process an instruction and return the result.
- `process_and_validate_instruction`: Process an instruction and perform a
series of checks on the result, panicking if any checks fail.
- `process_instruction_chain`: Process a chain of instructions and return the
result.
- `process_and_validate_instruction_chain`: Process a chain of instructions and
perform a series of checks on each result, panicking if any checks fail.

## Single Instructions

Both `process_instruction` and `process_and_validate_instruction` deal with
single instructions. The former simply processes the instruction and returns the
result, while the latter processes the instruction and then performs a series of
checks on the result. In both cases, the result is also returned.

```rust
use {
mollusk_svm::Mollusk,
solana_sdk::{account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey},
};

let program_id = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let key2 = Pubkey::new_unique();

let instruction = Instruction::new_with_bytes(
program_id,
&[],
vec![
AccountMeta::new(key1, false),
AccountMeta::new_readonly(key2, false),
],
);

let accounts = vec![
(key1, Account::default()),
(key2, Account::default()),
];

let mollusk = Mollusk::new(&program_id, "my_program");

// Execute the instruction and get the result.
let result = mollusk.process_instruction(&instruction, &accounts);
```

To apply checks via `process_and_validate_instruction`, developers can use the
`Check` enum, which provides a set of common checks.

```rust
use {
mollusk_svm::{Mollusk, result::Check},
solana_sdk::{
account::Account,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey
system_instruction,
system_program,
},
};

let sender = Pubkey::new_unique();
let recipient = Pubkey::new_unique();

let base_lamports = 100_000_000u64;
let transfer_amount = 42_000u64;

let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);
let accounts = [
(
sender,
Account::new(base_lamports, 0, &system_program::id()),
),
(
recipient,
Account::new(base_lamports, 0, &system_program::id()),
),
];
let checks = vec![
Check::success(),
Check::compute_units(system_processor::DEFAULT_COMPUTE_UNITS),
Check::account(&sender)
.lamports(base_lamports - transfer_amount)
.build(),
Check::account(&recipient)
.lamports(base_lamports + transfer_amount)
.build(),
];

Mollusk::default().process_and_validate_instruction(
&instruction,
&accounts,
&checks,
);
```

Note: `Mollusk::default()` will create a new `Mollusk` instance without adding
any provided BPF programs. It will still contain a subset of the default builtin
programs. For more builtin programs, you can add them yourself or use the
`all-builtins` feature.

## Instruction Chains

Both `process_instruction_chain` and `process_and_validate_instruction_chain`
deal with chains of instructions. The former processes each instruction in the
chain and returns the final result, while the latter processes each instruction
in the chain and then performs a series of checks on each result. In both cases,
the final result is also returned.

```rust
use {
mollusk_svm::Mollusk,
solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
};

let mollusk = Mollusk::default();

let alice = Pubkey::new_unique();
let bob = Pubkey::new_unique();
let carol = Pubkey::new_unique();
let dave = Pubkey::new_unique();

let starting_lamports = 500_000_000;

let alice_to_bob = 100_000_000;
let bob_to_carol = 50_000_000;
let bob_to_dave = 50_000_000;

mollusk.process_instruction_chain(
&[
system_instruction::transfer(&alice, &bob, alice_to_bob),
system_instruction::transfer(&bob, &carol, bob_to_carol),
system_instruction::transfer(&bob, &dave, bob_to_dave),
],
&[
(alice, system_account_with_lamports(starting_lamports)),
(bob, system_account_with_lamports(starting_lamports)),
(carol, system_account_with_lamports(starting_lamports)),
(dave, system_account_with_lamports(starting_lamports)),
],
);
```

Just like with `process_and_validate_instruction`, developers can use the
`Check` enum to apply checks via `process_and_validate_instruction_chain`.
Notice that `process_and_validate_instruction_chain` takes a slice of tuples,
where each tuple contains an instruction and a slice of checks. This allows the
developer to apply specific checks to each instruction in the chain. The result
returned by the method is the final result of the last instruction in the chain.

```rust
use {
mollusk_svm::{Mollusk, result::Check},
solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
};

let mollusk = Mollusk::default();

let alice = Pubkey::new_unique();
let bob = Pubkey::new_unique();
let carol = Pubkey::new_unique();
let dave = Pubkey::new_unique();

let starting_lamports = 500_000_000;

let alice_to_bob = 100_000_000;
let bob_to_carol = 50_000_000;
let bob_to_dave = 50_000_000;

mollusk.process_and_validate_instruction_chain(
&[
(
// 0: Alice to Bob
&system_instruction::transfer(&alice, &bob, alice_to_bob),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Alice pays
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob) // Bob receives
.build(),
Check::account(&carol)
.lamports(starting_lamports) // Unchanged
.build(),
Check::account(&dave)
.lamports(starting_lamports) // Unchanged
.build(),
],
),
(
// 1: Bob to Carol
&system_instruction::transfer(&bob, &carol, bob_to_carol),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Unchanged
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob - bob_to_carol) // Bob pays
.build(),
Check::account(&carol)
.lamports(starting_lamports + bob_to_carol) // Carol receives
.build(),
Check::account(&dave)
.lamports(starting_lamports) // Unchanged
.build(),
],
),
(
// 2: Bob to Dave
&system_instruction::transfer(&bob, &dave, bob_to_dave),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Unchanged
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave) // Bob pays
.build(),
Check::account(&carol)
.lamports(starting_lamports + bob_to_carol) // Unchanged
.build(),
Check::account(&dave)
.lamports(starting_lamports + bob_to_dave) // Dave receives
.build(),
],
),
],
&[
(alice, system_account_with_lamports(starting_lamports)),
(bob, system_account_with_lamports(starting_lamports)),
(carol, system_account_with_lamports(starting_lamports)),
(dave, system_account_with_lamports(starting_lamports)),
],
);
```

It's important to understand that instruction chains _should not_ be considered
equivalent to Solana transactions. Mollusk does not impose constraints on
instruction chains, such as loaded account keys or size. Developers should
recognize that instruction chains are primarily used for testing program
execution.

## Benchmarking Compute Units

The Mollusk Compute Unit Bencher can be used to benchmark the compute unit usage
of Solana programs. It provides a simple API for developers to write benchmarks
for their programs, which can be checked while making changes to the program.

A markdown file is generated, which captures all of the compute unit benchmarks.
If a benchmark has a previous value, the delta is also recorded. This can be
useful for developers to check the implications of changes to the program on
compute unit usage.

```rust
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();

```

The `must_pass` argument can be provided to trigger a panic if any defined
benchmark tests do not pass. `out_dir` specifies the directory where the
markdown file will be written.

Developers can invoke this benchmark test with `cargo bench`. They may need to
add a bench to the project's `Cargo.toml`.

```toml
[[bench]]
name = "compute_units"
harness = false
```

The markdown file will contain entries according to the defined benchmarks.

```markdown
| Name | CUs | Delta |
| ------ | ----- | ------ |
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
```

0 comments on commit 6909a0c

Please sign in to comment.