From 3abfe7829ff374ed91c9372ee06bc088eb115597 Mon Sep 17 00:00:00 2001 From: Mathieu Baudet <1105398+ma2bd@users.noreply.github.com> Date: Mon, 20 May 2024 15:27:04 -0400 Subject: [PATCH] Update to SDK v0.11.1 (#105) * Rename "initialize" to "instantiate" Use the new terminology used by the SDK. * Remove `.cargo/config.toml` reference It is not created by the SDK anymore, and specifying the Wasm target should be done explicitly when building. * Remove obsolete associated types from ABI They were moved to the `Contract` and `Service` traits instead. * Update "Contract" section Tweak the example Counter contract to use the new SDK version. * Update the "Service" section Tweak the example Counter service to use the new SDK version. * Update "Cross-Chain Messages" section Tweak the description and examples to use the new SDK version. * Tweak "Calling other Applications" section Clarify which application is the dependency and add links to the mentioned examples. * Update the "Writing Tests" section Remove all the now obsolete information about running tests for specific platforms, and update the examples to use the updated SDK. * Update the "Contract Finalization" section Use the updated SDK and the new `load`/`store` terminology. * Update "Applications that Handle Assets" example Use the updated `Contract` trait implementation. * Update `linera-protocol` submodule to 0.11.1 And update the release branch and version files. * Add symlink to Cargo.lock + improve README --------- Co-authored-by: Janito Vaqueiro Ferreira Filho --- Cargo.lock | 1 + README.md | 2 +- RELEASE_BRANCH | 2 +- RELEASE_VERSION | 2 +- linera-protocol | 2 +- src/advanced_topics/assets.md | 4 +- src/advanced_topics/contract_finalize.md | 67 +++------- src/core_concepts/applications.md | 6 +- src/sdk/abi.md | 6 +- src/sdk/composition.md | 15 ++- src/sdk/contract.md | 107 +++++++-------- src/sdk/creating_a_project.md | 2 - src/sdk/messages.md | 33 +++-- src/sdk/service.md | 71 +++++----- src/sdk/testing.md | 163 +++++------------------ 15 files changed, 165 insertions(+), 318 deletions(-) create mode 120000 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 120000 index 00000000..726c5f5f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1 @@ +linera-protocol/Cargo.lock \ No newline at end of file diff --git a/README.md b/README.md index 033539ce..f175763f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ cd linera-protocol git fetch origin git checkout $(git rev-parse $REMOTE_BRANCH) cargo clean -cargo build --locked -p linera-sdk +cargo build --locked -p linera-sdk --features test,wasmer cd .. mdbook test -L linera-protocol/target/debug/deps git commit -a diff --git a/RELEASE_BRANCH b/RELEASE_BRANCH index d293b329..8fa02d71 100644 --- a/RELEASE_BRANCH +++ b/RELEASE_BRANCH @@ -1 +1 @@ -devnet_2024_03_26 \ No newline at end of file +devnet_2024_05_07 \ No newline at end of file diff --git a/RELEASE_VERSION b/RELEASE_VERSION index 56e9df10..027934ea 100644 --- a/RELEASE_VERSION +++ b/RELEASE_VERSION @@ -1 +1 @@ -0.10.3 \ No newline at end of file +0.11.1 \ No newline at end of file diff --git a/linera-protocol b/linera-protocol index 687bb939..d9e35246 160000 --- a/linera-protocol +++ b/linera-protocol @@ -1 +1 @@ -Subproject commit 687bb93985b6806ff2686f57cac968db0c7513dd +Subproject commit d9e352463e0a572b3e6ec84dcf0bb9a55fab2d49 diff --git a/src/advanced_topics/assets.md b/src/advanced_topics/assets.md index 07d7d376..6a5d066b 100644 --- a/src/advanced_topics/assets.md +++ b/src/advanced_topics/assets.md @@ -24,7 +24,7 @@ The does this: ```rust,ignore - async fn execute_operation(&mut self, operation: Operation) -> Result<(), Self::Error> { + async fn execute_operation(&mut self, operation: Operation) -> Self::Response { match operation { // ... Operation::CloseChain => { @@ -38,7 +38,7 @@ does this: } self.runtime .close_chain() - .map_err(|_| MatchingEngineError::CloseChainError)?; + .expect("The application does not have permissions to close the chain."); } } } diff --git a/src/advanced_topics/contract_finalize.md b/src/advanced_topics/contract_finalize.md index a23d3f3f..fdb3deb0 100644 --- a/src/advanced_topics/contract_finalize.md +++ b/src/advanced_topics/contract_finalize.md @@ -1,29 +1,16 @@ # Contract Finalization When a transaction finishes executing successfully, there's a final step where -all loaded application contracts are allowed to `finalize`, similarly to -executing a destructor. The default implementation of `finalize` just persists -the application's state: - -```rust,ignore - /// Finishes the execution of the current transaction. - async fn finalize(&mut self) -> Result<(), Self::Error> { - Self::Storage::store(self.state_mut()).await; - Ok(()) - } -``` - -Applications may want to override the `finalize` method, which allows performing -some clean-up operations after execution finished. While finalizing, contracts -may send messages, read and write to the state, but are not allowed to call -other applications, because they are all also finalizing. - -> If `finalize` is overriden, the default implementation is **not** executed, so -> the developer must ensure that the application's state is persisted correctly. - -While finalizing, contracts can force the transaction to fail by panicking or -returning an error. The block is then rejected, even if the entire transaction's -operation had succeeded before `finalize` was called. This allows a contract to +all loaded application contracts have their `Contract::store` implementation +called. This can be seen to be similar to executing a destructor. In that sense, +applications may want to perform some final operations after execution finished. +While finalizing, contracts may send messages, read and write to the state, but +are not allowed to call other applications, because they are all also in the +process of finalizing. + +While finalizing, contracts can force the transaction to fail by panicking. The +block is then rejected, even if the entire transaction's operation had succeeded +before the application's `Contract::store` was called. This allows a contract to reject transactions if other applications don't follow any required constraints it establishes after it responds to a cross-application call. @@ -38,14 +25,16 @@ pub struct MyContract { active_sessions: HashSet; } -#[async_trait] impl Contract for MyContract { - type Error = MyError; - type State = MyState; - type Storage = ViewStateStorage; type Message = (); + type InstantiationArgument = (); + type Parameters = (); + + async fn load(runtime: ContractRuntime) -> Self { + let state = MyState::load(ViewStorageContext::from(runtime.key_value_store())) + .await + .expect("Failed to load state"); - async fn new(state: Self::State, runtime: ContractRuntime) -> Result { MyContract { state, runtime, @@ -53,21 +42,9 @@ impl Contract for MyContract { } } - fn state_mut(&mut self) -> &mut Self::State { - &mut self.state - } - - async fn initialize( - &mut self, - argument: Self::InitializationArgument, - ) -> Result<(), Self::Error> { - Ok(()) - } + async fn instantiate(&mut self, (): Self::InstantiationArgument) {} - async fn execute_operation( - &mut self, - operation: Self::Operation, - ) -> Result { + async fn execute_operation(&mut self, operation: Self::Operation) -> Self::Response { let caller = self.runtime .authenticated_caller_id() .expect("Missing caller ID"); @@ -92,15 +69,13 @@ impl Contract for MyContract { unreachable!("This example doesn't support messages"); } - async fn finalize(&mut self) -> Result<(), Self::Error> { + async fn store(&mut self) { assert!( self.active_sessions.is_empty(), "Some sessions have not ended" ); - Self::Storage::store(self.state_mut()).await; - - Ok(()) + self.state.save().await.expect("Failed to save state"); } } ``` diff --git a/src/core_concepts/applications.md b/src/core_concepts/applications.md index 2687ac46..2b41ba05 100644 --- a/src/core_concepts/applications.md +++ b/src/core_concepts/applications.md @@ -8,7 +8,7 @@ execute user applications. Currently, the [Linera SDK](../sdk.md) is focused on the [Rust](https://www.rust-lang.org/) programming language. Linera applications are structured using the familiar notion of **Rust crate**: -the external interfaces of an application (including initialization parameters, +the external interfaces of an application (including instantiation parameters, operations and messages) generally go into the library part of its crate, while the core of each application is compiled into binary files for the Wasm architecture. @@ -26,7 +26,7 @@ flexible: 2. The bytecode is published to the network on a microchain, and assigned an identifier. 3. A user can create a new application instance, by providing the bytecode - identifier and initialization arguments. This process returns an application + identifier and instantiation arguments. This process returns an application identifier which can be used to reference and interact with the application. 4. The same bytecode identifier can be used as many times needed by as many users needed to create distinct applications. @@ -38,7 +38,7 @@ and an application can be published with a single command: linera publish-and-create ``` -This will publish the bytecode as well as initialize the application for you. +This will publish the bytecode as well as instantiate the application for you. ## Anatomy of an Application diff --git a/src/sdk/abi.md b/src/sdk/abi.md index 9c8116bf..c651be1e 100644 --- a/src/sdk/abi.md +++ b/src/sdk/abi.md @@ -41,16 +41,13 @@ contract. Each type represents a specific part of the contract's behavior: All these types must implement the `Serialize`, `DeserializeOwned`, `Send`, `Sync`, `Debug` traits, and have a `'static` lifetime. -In our example, we would like to change our `InitializationArgument`, -`Operation` to `u64`, like so: +In our example, we would like to change our `Operation` to `u64`, like so: ```rust # extern crate linera_base; # use linera_base::abi::ContractAbi; # struct CounterAbi; impl ContractAbi for CounterAbi { - type InitializationArgument = u64; - type Parameters = (); type Operation = u64; type Response = (); } @@ -79,6 +76,5 @@ For our Counter example, we'll be using GraphQL to query our application so our impl ServiceAbi for CounterAbi { type Query = async_graphql::Request; type QueryResponse = async_graphql::Response; - type Parameters = (); } ``` diff --git a/src/sdk/composition.md b/src/sdk/composition.md index ffe57dc1..5298814b 100644 --- a/src/sdk/composition.md +++ b/src/sdk/composition.md @@ -39,15 +39,15 @@ they are refunded. If Alice used the `fungible` example to create a Pugecoin application (with an impressionable pug as its mascot), then Bob can create a `crowd-funding` application, use Pugecoin's application ID as `CrowdFundingAbi::Parameters`, and -specify in `CrowdFundingAbi::InitializationArgument` that his campaign will run +specify in `CrowdFundingAbi::InstantiationArgument` that his campaign will run for one week and has a target of 1000 Pugecoins. Now let's say Carol wants to pledge 10 Pugecoin tokens to Bob's campaign. First she needs to make sure she has his crowd-funding application on her chain, e.g. using the `linera request-application` command. This will automatically -also register Alice's application on her chain, because it is a dependency of -Bob's. +also register Alice's Pugecoin application on her chain, because it is a +dependency of Bob's. Now she can make her pledge by running the `linera service` and making a query to Bob's application: @@ -94,5 +94,10 @@ indirectly by signing her block. The crowd-funding application now makes a note in its application state on Bob's chain that Carol has pledged 10 Pugecoin tokens. -For the complete code please take a look at the `crowd-funding` and `fungible` -applications in the `examples` folder in `linera-protocol`. +For the complete code please take a look at the +[`crowd-funding`](https://github.com/linera-io/linera-protocol/blob/{{#include +../../.git/modules/linera-protocol/HEAD}}/examples/crowd-funding/src/contract.rs) +and the +[`fungible`](https://github.com/linera-io/linera-protocol/blob/{{#include +../../.git/modules/linera-protocol/HEAD}}/examples/fungible/src/contract.rs) +application contracts in the `examples` folder in `linera-protocol`. diff --git a/src/sdk/contract.md b/src/sdk/contract.md index ac8ef6f0..adc89292 100644 --- a/src/sdk/contract.md +++ b/src/sdk/contract.md @@ -7,46 +7,31 @@ To create a contract, we need to create a new type and implement the `Contract` trait for it, which is as follows: ```rust,ignore -#[async_trait] -pub trait Contract: WithContractAbi + ContractAbi + Send + Sized { - /// The type used to report errors to the execution environment. - type Error: Error + From + From + 'static; - - /// The type used to store the persisted application state. - type State: Sync; - - /// The desired storage backend used to store the application's state. - type Storage: ContractStateStorage + Send + 'static; - +pub trait Contract: WithContractAbi + ContractAbi + Sized { /// The type of message executed by the application. - type Message: Serialize + DeserializeOwned + Send + Sync + Debug + 'static; + type Message: Serialize + DeserializeOwned + Debug; - /// Creates an in-memory instance of the contract handler from the application's `state`. - async fn new(state: Self::State, runtime: ContractRuntime) -> Result; + /// Immutable parameters specific to this application (e.g. the name of a token). + type Parameters: Serialize + DeserializeOwned + Clone + Debug; - /// Returns the current state of the application so that it can be persisted. - fn state_mut(&mut self) -> &mut Self::State; + /// Instantiation argument passed to a new application on the chain that created it + /// (e.g. an initial amount of tokens minted). + type InstantiationArgument: Serialize + DeserializeOwned + Debug; - /// Initializes the application on the chain that created it. - async fn initialize( - &mut self, - argument: Self::InitializationArgument, - ) -> Result<(), Self::Error>; + /// Creates a in-memory instance of the contract handler. + async fn load(runtime: ContractRuntime) -> Self; + + /// Instantiates the application on the chain that created it. + async fn instantiate(&mut self, argument: Self::InstantiationArgument); /// Applies an operation from the current block. - async fn execute_operation( - &mut self, - operation: Self::Operation, - ) -> Result; + async fn execute_operation(&mut self, operation: Self::Operation) -> Self::Response; /// Applies a message originating from a cross-chain message. - async fn execute_message(&mut self, message: Self::Message) -> Result<(), Self::Error>; + async fn execute_message(&mut self, message: Self::Message); /// Finishes the execution of the current transaction. - async fn finalize(&mut self) -> Result<(), Self::Error> { - Self::Storage::store(self.state_mut()).await; - Ok(()) - } + async fn store(self); } ``` @@ -57,7 +42,7 @@ The full trait definition can be found There's quite a bit going on here, so let's break it down and take one method at a time. -For this application, we'll be using the `initialize` and `execute_operation` +For this application, we'll be using the `load`, `execute_operation` and `store` methods. ## The Contract Lifecycle @@ -78,55 +63,56 @@ things. Other fields can be added, and they can be used to store volatile data that only exists while the current transaction is being executed, and discarded afterwards. -When a transaction is executed, first the application's state is loaded, then -the contract type is created by calling the `Contract::new` method. This method -receives the state and a handle to the runtime that the contract can use. For -our implementation, we will just store the received parameters: +When a transaction is executed, the contract type is created through a call to +`Contract::load` method. This method receives a handle to the runtime that the +contract can use, and should use it to load the application state. For our +implementation, we will load the state and create the `CounterContract` +instance: ```rust,ignore - async fn new(state: Counter, runtime: ContractRuntime) -> Result { + async fn load(runtime: ContractRuntime) -> Self { + let state = Counter::load(ViewStorageContext::from(runtime.key_value_store())) + .await + .expect("Failed to load state"); CounterContract { state, runtime } } ``` When the transaction finishes executing successfully, there's a final step where -all loaded application contracts are allowed to `finalize`, similarly to -executing a destructor. The default implementation of `finalize` just persists -the application's state, and that's why we must provide it access to the state -through the `state_mut` method: +all loaded application contracts are called in order to do any final checks and +persist its state to storage. That final step is a call to the `Contract::store` +method, which can be thought of as similar to executing a destructor. In our +implementation we will persist the state back to storage: ```rust,ignore - fn state_mut(&mut self) -> &mut Self::State { - &mut self.state + async fn store(mut self) { + self.state.save().await.expect("Failed to save state"); } ``` -Applications may want to override the `finalize` method in more advanced -scenarios, but they must ensure they don't forget to _persist_ their state if -they do so. For more information see the -[Contract finalization section](../advanced_topics/contract_finalize.md). +It's possible to do more than just saving the state, and the +[Contract finalization section](../advanced_topics/contract_finalize.md) +provides more details on that. -## Initializing our Application +## Instantiating our Application The first thing that happens when an application is created from a bytecode is -that it is initialized. This is done by calling the contract's -`Contract::initialize` method. +that it is instantiated. This is done by calling the contract's +`Contract::instantiate` method. -`Contract::initialize` is only called once when the application is created and +`Contract::instantiate` is only called once when the application is created and only on the microchain that created the application. -Deployment on other microchains will use the `Default` implementation of the -application state if `SimpleStateStorage` is used, or the `Default` value of all -sub-views in the state if the `ViewStateStorage` is used. +Deployment on other microchains will use the `Default` value of all sub-views in +the state if the state uses the view paradigm. For our example application, we'll want to initialize the state of the application to an arbitrary number that can be specified on application creation -using its initialization parameters: +using its instatiation parameters: ```rust,ignore - async fn initialize(&mut self, value: u64) -> Result<(), Self::Error> { + async fn instantiate(&mut self, value: u64) { self.state.value.set(value); - Ok(()) } ``` @@ -136,15 +122,14 @@ Now that we have our counter's state and a way to initialize it to any value we would like, we need a way to increment our counter's value. Execution requests from block proposers or other applications are broadly called 'operations'. -To create a new operation, we need to use the method -`Contract::execute_operation`. In the counter's case, it will be receiving a -`u64` which is used to increment the counter: +To handle an operation, we need to implement the `Contract::execute_operation` +method. In the counter's case, the operation it will be receiving is a `u64` +which is used to increment the counter by that value: ```rust,ignore - async fn execute_operation(&mut self, operation: u64) -> Result<(), Self::Error> { + async fn execute_operation(&mut self, operation: u64) { let current = self.value.get(); self.value.set(current + operation); - Ok(()) } ``` diff --git a/src/sdk/creating_a_project.md b/src/sdk/creating_a_project.md index 0b78681f..5092e78e 100644 --- a/src/sdk/creating_a_project.md +++ b/src/sdk/creating_a_project.md @@ -19,5 +19,3 @@ files: contract bytecode; - `src/service.rs`: the application's service, and the binary target for the service bytecode. -- `.cargo/config.toml`: modifies the default target used by `cargo` to be - `wasm32-unknown-unknown` diff --git a/src/sdk/messages.md b/src/sdk/messages.md index 343e6c8c..2925f1c0 100644 --- a/src/sdk/messages.md +++ b/src/sdk/messages.md @@ -9,9 +9,9 @@ handling code is guaranteed to be the same as the sending code, but the state may be different. For your application, you can specify any serializable type as the `Message` -type in your `ContractAbi` implementation. To send a message, use the +type in your `Contract` implementation. To send a message, use the [`ContractRuntime`](https://docs.rs/linera-sdk/latest/linera_sdk/struct.ContractRuntime.html) -made available as an argument to the contract's [`Contract::new`] constructor. +made available as an argument to the contract's [`Contract::load`] constructor. The runtime is usually stored inside the contract object, as we did when [writing the contract binary](./contract.md). We can then call [`ContractRuntime::prepare_message`](https://docs.rs/linera-sdk/latest/linera_sdk/struct.ContractRuntime.html#prepare_message) @@ -39,9 +39,9 @@ contract's `execute_message` method gets called on their chain. While preparing the message to be sent, it is possible to enable authentication forwarding and/or tracking. Authentication forwarding means that the message is -executed with the same authenticated signer as the sender of the message, while -tracking means that the message is sent back to the sender if the receiver skips -it. The example below enables both flags: +executed by the receiver with the same authenticated signer as the sender of the +message, while tracking means that the message is sent back to the sender if the +receiver rejects it. The example below enables both flags: ```rust,ignore self.runtime @@ -53,17 +53,15 @@ it. The example below enables both flags: ## Example: Fungible Token -In the -[`fungible` example application](https://github.com/linera-io/linera-protocol/tree/main/examples/fungible), -such a message can be the transfer of tokens from one chain to another. If the -sender includes a `Transfer` operation on their chain, it decreases their -account balance and sends a `Credit` message to the recipient's chain: +In the [`fungible` example +application](https://github.com/linera-io/linera-protocol/tree/{{#include +../../.git/modules/linera-protocol/HEAD}}/examples/fungible), such a message can +be the transfer of tokens from one chain to another. If the sender includes a +`Transfer` operation on their chain, it decreases their account balance and +sends a `Credit` message to the recipient's chain: ```rust,ignore -async fn execute_operation( - &mut self, - operation: Self::Operation, -) -> Result { +async fn execute_operation(&mut self, operation: Self::Operation) -> Self::Response { match operation { // ... Operation::Transfer { @@ -75,7 +73,7 @@ async fn execute_operation( self.state.debit(owner, amount).await?; self.finish_transfer_to_account(amount, target_account, owner) .await; - Ok(FungibleResponse::Ok) + FungibleResponse::Ok } // ... } @@ -98,6 +96,7 @@ async fn finish_transfer_to_account( self.runtime .prepare_message(message) .with_authentication() + .with_tracking() .send_to(target_account.chain_id); } } @@ -107,7 +106,7 @@ On the recipient's chain, `execute_message` is called, which increases their account balance. ```rust,ignore -async fn execute_message(&mut self, message: Message) -> Result<(), Self::Error> { +async fn execute_message(&mut self, message: Message) { match message { Message::Credit { amount, @@ -119,7 +118,5 @@ async fn execute_message(&mut self, message: Message) -> Result<(), Self::Error> } // ... } - - Ok(()) } ``` diff --git a/src/sdk/service.md b/src/sdk/service.md index dcebffbe..72a90e6f 100644 --- a/src/sdk/service.md +++ b/src/sdk/service.md @@ -14,28 +14,21 @@ The `Service` trait is how you define the interface into your application. The `Service` trait is defined as follows: ```rust,ignore -/// The service interface of a Linera application. -#[async_trait] -pub trait Service: WithServiceAbi + ServiceAbi { - /// Type used to report errors to the execution environment. - type Error: Error + From; - - /// The type used to store the persisted application state. - type State; - - /// The desired storage backend used to store the application's state. - type Storage: ServiceStateStorage; +pub trait Service: WithServiceAbi + ServiceAbi + Sized { + /// Immutable parameters specific to this application. + type Parameters: Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static; - /// Creates a in-memory instance of the service handler from the application's `state`. - async fn new(state: Self::State, runtime: ServiceRuntime) -> Result; + /// Creates a in-memory instance of the service handler. + async fn new(runtime: ServiceRuntime) -> Self; /// Executes a read-only query on the state of this application. - async fn handle_query(&self, query: Self::Query) -> Result; + async fn handle_query(&self, query: Self::Query) -> Self::QueryResponse; } ``` The full service trait definition can be found -[here](https://github.com/linera-io/linera-protocol/blob/main/linera-sdk/src/lib.rs). +[here](https://github.com/linera-io/linera-protocol/blob/{{#include +../../.git/modules/linera-protocol/HEAD}}/linera-sdk/src/lib.rs). Let's implement `Service` for our counter application. @@ -49,7 +42,8 @@ pub struct CounterService { Just like with the `CounterContract` type, this type usually has two types: the application `state` and the `runtime`. We can omit the fields if we don't use -them, so in this example we're omitting the `runtime` field. +them, so in this example we're omitting the `runtime` field, since its only used +when constructing the `CounterService` type. We need to generate the necessary boilerplate for implementing the service [WIT interface](https://component-model.bytecodealliance.org/design/wit.html), @@ -62,39 +56,33 @@ linera_sdk::service!(CounterService); ``` Next, we need to implement the `Service` trait for `CounterService` type. The -first step is to define the `Service`'s associated types: +first step is to define the `Service`'s associated type, which is the global +parameters specified when the application is instantiated. In our case, the +global paramters aren't used, so we can just specify the unit type: ```rust,ignore #[async_trait] impl Service for CounterService { - type Error = Error; - type Storage = ViewStateStorage; - type State = Counter; + type Parameters = (); } ``` -The only type specified above that isn't yet defined is the `Error` type, so -let's create it below the trait implementation: +Also like in contracts, we must implement a `load` constructor when implementing +the `Service` trait. The constructor receives the runtime handle and should use +it to load the application state: ```rust,ignore -/// An error that can occur during the contract execution. -#[derive(Debug, Error)] -pub enum Error { - /// Invalid query argument; could not deserialize GraphQL request. - #[error("Invalid query argument; could not deserialize GraphQL request")] - InvalidQuery(#[from] serde_json::Error), -} -``` - -Also like in contracts, we must implement a `new` constructor when implementing -the `Service` trait. The constructor receives the state and the runtime handle: - -```rust,ignore - async fn new(state: Self::State, _runtime: ServiceRuntime) -> Result { + async fn load(runtime: ServiceRuntime) -> Self { + let state = Counter::load(ViewStorageContext::from(runtime.key_value_store())) + .await + .expect("Failed to load state"); Ok(CounterService { state }) } ``` +Services don't have a `store` method because they are read-only and can't +persist any changes back to the storage. + The actual functionality of the service starts in the `handle_query` method. We will accept GraphQL queries and handle them using the [`async-graphql` crate](https://github.com/async-graphql/async-graphql). To @@ -102,7 +90,7 @@ forward the queries to custom GraphQL handlers we will implement in the next section, we use the following code: ```rust,ignore - async fn handle_query(&mut self, request: Request) -> Result { + async fn handle_query(&mut self, request: Request) -> Response { let schema = Schema::build( // implemented in the next section QueryRoot { value: *self.value.get() }, @@ -111,7 +99,7 @@ section, we use the following code: EmptySubscription, ) .finish(); - Ok(schema.execute(request).await) + schema.execute(request).await } } ``` @@ -163,6 +151,7 @@ impl MutationRoot { We haven't included the imports in the above code; they are left as an exercise to the reader (but remember to import `async_graphql::Object`). If you want the -full source code and associated tests check out the -[examples section](https://github.com/linera-io/linera-protocol/blob/main/examples/counter/src/service.rs) -on GitHub. +full source code and associated tests check out the [examples +section](https://github.com/linera-io/linera-protocol/blob/{{#include +../../.git/modules/linera-protocol/HEAD}}/examples/counter/src/service.rs) on +GitHub. diff --git a/src/sdk/testing.md b/src/sdk/testing.md index a4734b8e..123ef207 100644 --- a/src/sdk/testing.md +++ b/src/sdk/testing.md @@ -1,171 +1,68 @@ # Writing Tests -Linera applications can be tested using unit tests or integration tests. Both -are a bit different than usual Rust tests. Unit tests are executed inside a -WebAssembly virtual machine in an environment that simulates a single microchain -and a single application. System APIs are only available if they are mocked -using helper functions from `linera_sdk::test`. - -Integration tests run outside a WebAssembly virtual machine, and use a simulated -validator for testing. This allows creating chains and adding blocks to them in -order to test interactions between multiple microchains and multiple -applications. +Linera applications can be tested using normal Rust unit tests or integration +tests. Unit tests use a mock runtime for execution, so it's useful for testing +the application as if it were running by itself on a single chain. Integration +tests use a simulated validator for testing. This allows creating chains and +adding blocks to them in order to test interactions between multiple microchains +and multiple applications. Applications should consider having both types of tests. Unit tests should be used to focus on the application's internals and core functionality. Integration tests should be used to test how the application behaves on a more complex environment that's closer to the real network. -> In most cases, the simplest way to run both unit tests and integration tests -> is to call `linera project test` from the project's directory. +> For Rust tests, the `cargo test` command can be used to run both the unit and +> integration tests. ## Unit tests Unit tests are written beside the application's source code (i.e., inside the -`src` directory of the project). There are several differences to normal Rust -unit tests: - -- the target `wasm32-unknown-unknown` must be selected; - -- the custom test runner `linera-wasm-test-runner` must be used; - -- the - [`#[webassembly_test]`](https://docs.rs/webassembly-test/latest/webassembly_test/) - attribute must be used instead of the usual `#[test]` attribute. - -The first two items are done automatically by `linera project test`. - -Alternatively, one may set up the environment and run `cargo test` directly as -described [below](#manually-configuring-the-environment). +`src` directory of the project). The main purpose of a unit test is to test +parts of the application in an isolated environment. Anything that's external is +usually mocked. When the `linera-sdk` is compiled with the `test` feature +enabled, the `ContractRuntime` and `SystemRuntime` types are actually mock +runtimes, and can be configured to return specific values for different tests. ### Example -A simple unit test is shown below, which tests if the application's +A simple unit test is shown below, which tests if the application contract's `do_something` method changes the application state. ```rust,ignore #[cfg(test)] mod tests { - use crate::state::ApplicationState; - use webassembly_test::webassembly_test; + use crate::{ApplicationContract, ApplicationState}; + use linera_sdk::{util::BlockingWait, ContractRuntime}; - #[webassembly_test] + #[test] fn test_do_something() { - let mut application = ApplicationState { - // Configure the application's initial state - ..ApplicationState::default() - }; + let runtime = ContractRuntime::new(); + let mut contract = ApplicationContract::load(runtime).blocking_wait(); - let result = application.do_something(); + let result = contract.do_something(); + // Check that `do_something` succeeded assert!(result.is_ok()); - assert_eq!(application, ApplicationState { + // Check that the state in memory was updated + assert_eq!(contract.state, ApplicationState { // Define the application's expected final state ..ApplicationState::default() }); - } -} -``` - -### Mocking System APIs - -Unit tests run in a constrained environment, so things like access to the -key-value store, cross-chain messages and cross-application calls can't be -executed. However, they can be simulated using mock APIs. The `linera-sdk::test` -module provides some helper functions to mock the system APIs. - -Here's an example mocking the key-value store. - -```rust,ignore -#[cfg(test)] -mod tests { - use crate::state::ApplicationState; - use linera_sdk::test::mock_key_value_store; - use webassembly_test::webassembly_test; - - #[webassembly_test] - fn test_state_is_not_persisted() { - let mut storage = mock_key_value_store(); - - // Assuming the application uses views - let mut application = ApplicationState::load(storage.clone()) - .now_or_never() - .expect("Mock key-value store returns immediately") - .expect("Failed to load view from mock key-value store"); - - // Assuming `do_something` changes the view, but does not persist it - let result = application.do_something(); - - assert!(result.is_ok()); - // Check that the state in memory is different from the state in storage - assert_ne!(application, ApplicatonState::load(storage)); + assert_ne!( + contract.state, + ApplicatonState::load(ViewStorageContext::from(runtime.key_value_store())) + ); } } ``` -### Running Unit Tests with `cargo test` - -Running `linera project test` is easier, but if there's a need to run -`cargo test` explicitly to run the unit tests, Cargo must be configured to use -the custom test runner `linera-wasm-test-runner`. This binary can be built from -the repository or installed with `cargo install --locked linera-sdk`. - -```bash -cd linera-protocol -cargo build -p linera-sdk --bin linera-wasm-test-runner --release -``` - -The steps above build the `linera-wasm-test-runner` and places the resulting -binary at `linera-protocol/target/release/linera-wasm-test-runner`. - -With the binary available, the last step is to configure Cargo. There are a few -ways to do this. A quick way is to set the -`CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER` environment variable to the path of -the binary. - -A more persistent way is to change one of -[Cargo's configuration files](https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure). -As an example, the following file can be placed inside the project's directory -at `PROJECT_DIR/.cargo/config.toml`: - -```ignore -[target.wasm32-unknown-unknown] -runner = "PATH_TO/linera-wasm-test-runner" -``` - -After configuring the test runner, unit tests can be executed with - -```bash -cargo test --target wasm32-unknown-unknown -``` - -Optionally, `wasm32-unknown-unknown` can be made the default build target with -the following lines in `PROJECT_DIR/.cargo/config.toml`: - -```ignore -[build] -target = "wasm32-unknown-unknown" -``` - ## Integration Tests Integration tests are usually written separately from the application's source code (i.e., inside a `tests` directory that's beside the `src` directory). -Integration tests are normal Rust integration tests, and they are compiled to -the **host** target instead of the `wasm32-unknown-unknown` target used for unit -tests. This is because unit tests run inside a WebAssembly virtual machine and -integration tests run outside a virtual machine, starting isolated virtual -machines to run each operation of each block added to each chain. - -> Integration tests can be run with `linera project test` or simply -> `cargo test`. - -If you wish to use `cargo test` and have overridden your default target to be in -`wasm32-unknown-unknown` in `.cargo/config.toml`, you will have to pass a native -target to `cargo`, for instance `cargo test --target aarch64-apple-darwin`. - Integration tests use the helper types from `linera_sdk::test` to set up a simulated Linera network, and publish blocks to microchains in order to execute the application. @@ -178,7 +75,11 @@ chains is shown below. ```rust,ignore #[tokio::test] async fn test_cross_chain_message() { - let (validator, application_id) = TestValidator::with_current_application(vec![], vec![]).await; + let parameters = vec![]; + let instantiation_argument = vec![]; + + let (validator, application_id) = + TestValidator::with_current_application(parameters, instantiation_argument).await; let mut sender_chain = validator.get_chain(application_id.creation.chain_id).await; let mut receiver_chain = validator.new_chain().await;