Skip to content

Commit

Permalink
[Docs] Added additional testing documentation (Move docs) and package…
Browse files Browse the repository at this point in the history
… publishing description (Wallet docs)
  • Loading branch information
awelc authored and sblackshear committed Mar 1, 2022
1 parent 1caf721 commit b7e81b4
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 4 deletions.
179 changes: 178 additions & 1 deletion doc/move.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ current_directory
```

For convenience, please also make sure the path to Sui binaries
(sui/target/release) is part of your system path.
(sui/target/release), including the sui-move command used throughout
this tutorial, is part of your system path.

We can now proceed to creating a package directory structure and an
empty manifest file following the Move code organization described
Expand Down Expand Up @@ -526,3 +527,179 @@ Running Move unit tests
Test result: OK. Total tests: 1; passed: 1; failed: 0
```
The testing example we have seen so far is largely "pure" Move and has
little to do with Sui beyond using some Sui packages, such as
`Sui::TxContext` and `Sui::Transfer`. While this style of testing is
already very useful for developers writing Move code for Sui, they may
also want to test additional Sui-specific features. In particular, a
Move call in Sui is encapsulated in a Sui
[transaction](https://github.com/MystenLabs/fastnft/blob/main/doc/transactions.md),
and a developer may wish to test interactions between different
transactions within a single test (e.g. one transaction creating an
object and the other one transferring it).
### Sui-specific testing
Sui-specific testing is supported via the `TestScenario`
[module](../sui_programmability/framework/sources/TestScenario.move)
that provides Sui-related testing functionality otherwise unavailable
in "pure" Move and its testing
[framework](https://github.com/diem/move/blob/main/language/documentation/book/src/unit-testing.md).
The main concept in the `TestScenario` is a scenario which emulates a
series of Sui transactions, each executed by a (potentially) different
user. At a high level, a developer writing a test starts the first
transaction using the `TestScenario::begin` function which takes an
address of the user executing this transaction as the first an only
argument, and return an instance of the `Scenario` struct representing
a scenario. An instance of the `Scenario` struct contains a
per-address object pool emulating Sui's object storage, with helper
functions provided to manipulate objects in the pool. Once the first
transaction is finished, subsequent transactions can be started using
the `TestScenario::next_tx` function that takes an instance of the
`Scenario` struct representing the current scenario and an address of
a (new) user as arguments.
Let us extend our running example with a multi-transaction test that
uses the `TestScenario` to test sword creation and transfer from the
point of view of Sui developer. First, let us create entry
[functions](#entry-functions) callable from Sui that implement sword
creation and transfer and put the into the M1.move file:
``` rust
public fun sword_create(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
use Sui::Transfer;
use Sui::TxContext;
// create a sword
let sword = Sword {
id: TxContext::new_id(ctx),
magic: magic,
strength: strength,
};
// transfer the sword
Transfer::transfer(sword, recipient);
}
public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
use Sui::Transfer;
// transfer the sword
Transfer::transfer(sword, recipient);
}
```
The code of the new functions is self-explanatory and uses struct
creation and Sui-internal modules (`TxContext` and `Transfer`) in a
way similar to what we have seen in the previous sections. The
important part is for the entry functions to have correct signatures
as described [earlier](#entry-functions). In order for this code to
build, we need to add an additional import line at the module level
(as the first line in the module's main code block right before the
existing module-wide `ID` module import) to make `TxContext` struct
available for function definitions:
``` rust
use Sui::TxContext::TxContext;
```
We can now build the module extended with the new functions, but still
have only one test defined. Let use change that by adding another test
function:
``` rust
#[test]
public fun test_sword_transactions() {
use Sui::TestScenario;
let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;
// first transaction executed by admin
let scenario = &mut TestScenario::begin(&admin);
{
// create the sword and transfer it to the initial owner
sword_create(42, 7, initial_owner, TestScenario::ctx(scenario));
};
// second transaction executed by the initial sword owner
TestScenario::next_tx(scenario, &initial_owner);
{
// extract the sword owned by the initial owner
let sword = TestScenario::remove_object<Sword>(scenario);
// transfer the sword to the final owner
sword_transfer(sword, final_owner, TestScenario::ctx(scenario));
};
// third transaction executed by the final sword owner
TestScenario::next_tx(scenario, &final_owner);
{
// extract the sword owned by the final owner
let sword = TestScenario::remove_object<Sword>(scenario);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be simply "dropped")
TestScenario::return_object(scenario, sword)
}
}
```
Let us now dive into some details of the new testing function. The
first thing we do is to create some addresses that represent users
participating in the testing scenario (we assume that we have one game
admin user and two regular users representing players). We then create
a scenario by starting the first transaction on behalf of the admin
address that creates a sword and transfers its ownership to the
initial owner.
The second transaction is executed by the initial owner (passed as
argument to the `TestScenario::next_tx` function ) who then transfers
the sword it now owns the its final owner. Please note that in "pure"
Move we do not have the notion of Sui storage and, consequently, no
easy way for the emulated Sui transaction to retrieve it from
storage. This is where the `TestScenario` module comes to help - its
`remove_object` function makes an object of a given type (in this case
of type `Sword`) owned by an address executing the current transaction
available for manipulation by the Move code (for now we assume that
there is only one such object). In this case, the object retrieved
from storage is transferred to another address.
The final transaction is executed by the final owner - it retrieves
the sword object from storage and checks if it has the expected
properties. Please remember that, as described
[earlier](#testing-a-package) in the "pure" Move testing scenario,
once an object is available in Move code (e.g., after its created or,
in this case, retrieved from emulated storage), it cannot simply
disappear. In the "pure" Move testing function we handled this problem
by transferring the sword object to the fake address but the
`TestScenario` package gives us a more elegant solution which is
closer to what happens when Move code is actually executed in the
context of Sui - we can simply return the sword to the object pool
using `TestScenario::return_object` function.
We can now run the test command again and see that we now have two
successful tests for our module:
``` shell
BUILDING MoveStdlib
BUILDING Sui
BUILDING MyMovePackage
Running Move unit tests
[ PASS ] 0x0::M1::test_sword_create
[ PASS ] 0x0::M1::test_sword_transactions
Test result: OK. Total tests: 2; passed: 2; failed: 0
```
## Publishing a package
For functions in a Move package to actually be callable from Sui
(rather than for Sui execution scenario to be emulated), the package
has to be _published_ to Sui's [distributed
ledger](SUMMARY.md#architecture)
where it is represented as a Sui object. At this point, however, the
sui-move command does not support package publishing. In fact it is
not clear if it even makes sense to accommodate package publishing,
which happens once per package creation, in the context of a unit
testing framework. Instead, one can use a sample Sui wallet to
[publish](wallet.md#package-publishing) Move code and to
[call](wallet.md#calling-move-code). Please see the wallet
[documentation](wallet.md#package-publishing) for a description on how
to publish the package we have [written](#writing-a-package) as as
part of this tutorial.
40 changes: 37 additions & 3 deletions doc/wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ directory.
```

The genesis command creates 4 authorities, 5 user accounts each with 5
gas objects, which are Sui objects used to pay for Sui
[transactions](https://github.com/MystenLabs/fastnft/blob/main/doc/transactions.md#transaction-metadata),
gas objects, which are Sui [objects](objects.md) used to pay for Sui
[transactions](transactions.md#transaction-metadata),
such other object transfers or smart contract (Move) calls. These
numbers represent a sample configuration and have been chosen somewhat
arbitrarily, but the process of generating the genesis state can be
Expand Down Expand Up @@ -331,8 +331,42 @@ DC5530627AFBFFBB1F52B81F273A7B666B31CB85: ...

## Package Publishing

TBD
In order for user-written code to be available in Sui, it must be
_published_ to to Sui's [distributed
ledger](SUMMARY.md#architecture). Please see Move developer
[documentation](move.md) for a
[description](move.md#writing-a-package) on how to write a simple Move
code package, which we can publish using Sui wallet's publish command.

In order to show how to publish user-defined Move package, let us
continue where we left off in the previous
[section](#calling-move-code) describing how to call an existing Move
function. The publish command requires us to specify a directory where
user-defined package lives (it's a path to the `my_move_package` as
per package creation [description](move.md#writing-a-package)), a gas
object that will be used to pay for publishing the package (we use the
same gas object we used to pay for the function call in the previous
[section](#calling-move-code)), and gas budget to put an upper limit
(we use 1000 as our gas budget. The whole command looks as follows:

``` shell
./wallet --no-shell publish --path "$PACKAGE_PATH"/my_move_package --gas 1FD8DA0C56694229761E9A3DCE50C49AF2EA5DB1 1000
```

The (abbreviated) result of running this command should show that one
object (package object) was created and one object (gas object) was
modified:

``` shell
Created Objects:
BCC2E80B02B08226BF95522B0D4B59E687C5900E
Mutated Objects:
1FD8DA0C56694229761E9A3DCE50C49AF2EA5DB
```

From now on, we can use package object id in the Sui wallet's call
command just like we used 0x2 for built-in packages in the previous
[section](#calling-move-code).

## Genesis customization

Expand Down

0 comments on commit b7e81b4

Please sign in to comment.